// Package mcpformat formats inbound AgentTeam MCP messages into the text blocks
// shown to agents. It has no dependency on *Protocol or other wire internals,
// only on the API client and shared types.
package mcpformat

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

// ---------------------------------------------------------------------------
// Message formatting
// ---------------------------------------------------------------------------

func FormatReceiveMessages(messages []types.InboxMessage, api *apiclient.Client) string {
	var blocks []string
	for _, msg := range messages {
		if reactionBlock, ok := formatReactionReceiveMessage(msg); ok {
			blocks = append(blocks, reactionBlock)
			continue
		}

		timeText := FormatTimestamp(msg.Timestamp)
		messageID := "-"
		if strings.TrimSpace(msg.MessageID) != "" {
			messageID = msg.MessageID
		}

		content := trimReceiveRunes(msg.Content, receiveMessageContentMaxRunes)

		var sb strings.Builder
		sb.WriteString("[Message]\n")
		if target := strings.TrimSpace(msg.Target); target != "" {
			fmt.Fprintf(&sb, "target: %s\n", target)
		}
		fmt.Fprintf(&sb, "channel: %s\n", msg.ChannelName)
		if displayName := trimmedDisplayName(msg.ChannelDisplayName, msg.ChannelName); displayName != "" {
			fmt.Fprintf(&sb, "channelDisplayName: %s\n", displayName)
		}
		if parentDisplayName := trimmedDisplayName(msg.ParentChannelDisplayName, msg.ParentChannelName); parentDisplayName != "" {
			fmt.Fprintf(&sb, "parentChannelDisplayName: %s\n", parentDisplayName)
		}
		fmt.Fprintf(&sb, "messageId: %s\n", messageID)
		writePromptAuthorLines(&sb, msg.AuthorIdentity())
		fmt.Fprintf(&sb, "time: %s\n", timeText)
		sb.WriteString("content:\n")
		sb.WriteString(content)
		if guard := promptcompiler.RenderThreadContextGuidance(msg.Target, "", false); guard != "" {
			sb.WriteString("\n")
			sb.WriteString(guard)
		}
		if rootContext := formatReplyThreadRootReceiveDetails(msg); rootContext != "" {
			sb.WriteString("\n")
			sb.WriteString(rootContext)
		}

		if details := formatTeamContextReceiveDetails(msg.TeamContexts); details != "" {
			sb.WriteString("\n")
			sb.WriteString(details)
		}

		if details := formatTaskDispatchReceiveDetails(msg); details != "" {
			sb.WriteString("\n")
			sb.WriteString(details)
		}

		if details := formatTaskScopeDiscussionReceiveDetails(msg); details != "" {
			sb.WriteString("\n")
			sb.WriteString(details)
		}

		if strings.TrimSpace(msg.AttachmentIDs) != "" {
			fmt.Fprintf(&sb, "\nattachments: %s", trimReceiveRunes(msg.AttachmentIDs, receiveAttachmentLineMaxRunes))
			if strings.TrimSpace(msg.AttachmentNames) != "" {
				fmt.Fprintf(&sb, " (%s)", trimReceiveRunes(msg.AttachmentNames, receiveAttachmentLineMaxRunes))
			}
		}

		blocks = append(blocks, sb.String())
	}
	return strings.Join(blocks, "\n\n")
}

func formatReplyThreadRootReceiveDetails(msg types.InboxMessage) string {
	root, ok := msg.ReplyThreadRoot()
	if !ok {
		return ""
	}
	timeText := FormatTimestamp(root.Timestamp)
	messageID := "-"
	if trimmed := strings.TrimSpace(root.MessageID); trimmed != "" {
		messageID = trimmed
	}
	var lines []string
	lines = append(lines, "threadRoot:")
	lines = append(lines, "messageId: "+messageID)
	if authorLines := promptAuthorLines(root.AuthorIdentity()); len(authorLines) > 0 {
		lines = append(lines, authorLines...)
	}
	if strings.TrimSpace(timeText) != "" {
		lines = append(lines, "time: "+timeText)
	}
	if messageType := strings.TrimSpace(root.MessageType); messageType != "" {
		lines = append(lines, "messageType: "+messageType)
	}
	lines = append(lines, "content:")
	lines = append(lines, trimReceiveRunes(root.Content, receiveThreadRootContentMaxRunes))
	if len(root.Attachments) > 0 {
		var attachments []string
		visible := root.Attachments
		omitted := 0
		if len(visible) > receiveAttachmentMaxItems {
			omitted = len(visible) - receiveAttachmentMaxItems
			visible = visible[:receiveAttachmentMaxItems]
		}
		for _, attachment := range visible {
			switch {
			case strings.TrimSpace(attachment.Filename) != "" && strings.TrimSpace(attachment.ID) != "":
				attachments = append(attachments, fmt.Sprintf("- %s (id:%s)", trimReceiveRunes(attachment.Filename, 160), strings.TrimSpace(attachment.ID)))
			case strings.TrimSpace(attachment.Filename) != "":
				attachments = append(attachments, "- "+trimReceiveRunes(attachment.Filename, 160))
			case strings.TrimSpace(attachment.ID) != "":
				attachments = append(attachments, "- id:"+strings.TrimSpace(attachment.ID))
			}
		}
		if omitted > 0 {
			attachments = append(attachments, fmt.Sprintf("- ... (%d more)", omitted))
		}
		if len(attachments) > 0 {
			lines = append(lines, "attachments:")
			lines = append(lines, attachments...)
		}
	}
	return strings.Join(lines, "\n")
}

type reactionReceiveDetails struct {
	ReactionID           string
	TargetMessageID      string
	TargetMessagePreview string
	Emoji                string
}

func formatReactionReceiveMessage(msg types.InboxMessage) (string, bool) {
	if strings.TrimSpace(msg.MessageType) != "reaction" || len(msg.Metadata) == 0 {
		return "", false
	}
	rawReaction, ok := msg.Metadata["reaction"].(map[string]any)
	if !ok {
		return "", false
	}

	details := reactionReceiveDetails{
		ReactionID:           stringMapValue(rawReaction, "reactionId"),
		TargetMessageID:      stringMapValue(rawReaction, "targetMessageId"),
		TargetMessagePreview: stringMapValue(rawReaction, "targetMessagePreview"),
		Emoji:                stringMapValue(rawReaction, "emoji"),
	}
	if details.TargetMessageID == "" && details.ReactionID == "" && details.Emoji == "" {
		return "", false
	}

	timeText := FormatTimestamp(msg.Timestamp)
	messageID := "-"
	if trimmed := strings.TrimSpace(msg.MessageID); trimmed != "" {
		messageID = trimmed
	}

	var sb strings.Builder
	sb.WriteString("[Reaction]\n")
	if target := strings.TrimSpace(msg.Target); target != "" {
		fmt.Fprintf(&sb, "target: %s\n", target)
	}
	fmt.Fprintf(&sb, "channel: %s\n", msg.ChannelName)
	if displayName := trimmedDisplayName(msg.ChannelDisplayName, msg.ChannelName); displayName != "" {
		fmt.Fprintf(&sb, "channelDisplayName: %s\n", displayName)
	}
	if parentDisplayName := trimmedDisplayName(msg.ParentChannelDisplayName, msg.ParentChannelName); parentDisplayName != "" {
		fmt.Fprintf(&sb, "parentChannelDisplayName: %s\n", parentDisplayName)
	}
	fmt.Fprintf(&sb, "messageId: %s\n", messageID)
	writePromptAuthorLines(&sb, msg.AuthorIdentity())
	fmt.Fprintf(&sb, "time: %s\n", timeText)
	if details.Emoji != "" {
		fmt.Fprintf(&sb, "reaction: %s\n", details.Emoji)
	}
	if details.TargetMessageID != "" {
		fmt.Fprintf(&sb, "targetMessageId: %s\n", details.TargetMessageID)
	}
	if details.ReactionID != "" {
		fmt.Fprintf(&sb, "reactionId: %s\n", details.ReactionID)
	}
	if details.TargetMessagePreview != "" {
		sb.WriteString("targetMessagePreview:\n")
		sb.WriteString(trimReceiveRunes(details.TargetMessagePreview, receiveReactionPreviewMaxRunes))
		sb.WriteString("\n")
	}
	sb.WriteString("content:\n")
	sb.WriteString(trimReceiveRunes(msg.Content, receiveMessageContentMaxRunes))
	return strings.TrimSpace(sb.String()), true
}

func PromptAuthorMentionName(author types.AuthorIdentity) string {
	if author.ActorType == "agent" {
		return strings.TrimSpace(firstNonEmptyString(author.TargetID, author.PrincipalID, author.MemberID, author.DisplayName))
	}
	return strings.TrimSpace(author.DisplayName)
}

func promptAuthorDisplayName(author types.AuthorIdentity) string {
	if author.ActorType != "agent" {
		return ""
	}
	displayName := strings.TrimSpace(author.DisplayName)
	if displayName == "" || displayName == PromptAuthorMentionName(author) {
		return ""
	}
	return displayName
}

func promptAuthorLines(author types.AuthorIdentity) []string {
	mentionName := PromptAuthorMentionName(author)
	if mentionName == "" {
		return nil
	}
	lines := []string{"from: @" + mentionName}
	if displayName := promptAuthorDisplayName(author); displayName != "" {
		lines = append(lines, "fromDisplayName: "+displayName)
	}
	return lines
}

func writePromptAuthorLines(sb *strings.Builder, author types.AuthorIdentity) {
	for _, line := range promptAuthorLines(author) {
		fmt.Fprintln(sb, line)
	}
}

func trimmedDisplayName(displayName string, routingName string) string {
	display := strings.TrimSpace(displayName)
	if display == "" || display == strings.TrimSpace(routingName) {
		return ""
	}
	return display
}

func formatTaskDispatchReceiveDetails(msg types.InboxMessage) string {
	if strings.TrimSpace(msg.MessageType) != "task_dispatch" || len(msg.Metadata) == 0 {
		return ""
	}

	rawTask, _ := msg.Metadata["task"].(map[string]any)
	rawDiscussion, _ := msg.Metadata["taskDiscussion"].(map[string]any)
	if len(rawTask) == 0 {
		return ""
	}

	var lines []string
	if taskLabel := receiveTaskLabel(rawTask); taskLabel != "" {
		prefix := "task dispatch: "
		if rawDiscussion != nil {
			prefix = "task discussion: "
		}
		lines = append(lines, prefix+taskLabel)
	}

	channel := stringMapValue(rawTask, "channelRef")
	if channel != "" {
		lines = append(lines, "channel: "+channel)
	}
	if taskAttributes := formatTaskAttributeReceiveDetails(rawTask); taskAttributes != "" {
		lines = append(lines, taskAttributes)
	}
	if rawDiscussion == nil {
		lines = append(lines, "active task target: task discussion replies are scope-native; post a reply with the exact task scope target when present, task_checkpoint for high-signal checkpoints, and an ordinary reply only when deliberately addressing a channel/DM/thread audience")
		return strings.Join(lines, "\n")
	}

	commentBody := trimReceiveRunes(stringMapValue(rawDiscussion, "body"), receiveTaskDiscussionBodyMaxRunes)
	commentAuthor := receiveActorLabel(rawDiscussion["createdBy"])
	switch {
	case commentAuthor != "" && commentBody != "":
		lines = append(lines, "comment by "+commentAuthor+":\n"+commentBody)
	case commentAuthor != "":
		lines = append(lines, "comment by "+commentAuthor)
	case commentBody != "":
		lines = append(lines, "comment:\n"+commentBody)
	}

	lines = append(lines, `reply: post a reply (target="#tsk_...", content="...") with the task scope target to continue the task discussion`)
	lines = append(lines, "active task target: task discussion replies are scope-native; post a reply with the exact task scope target when present, task_checkpoint for high-signal checkpoints, and an ordinary reply only when deliberately addressing a channel/DM/thread audience")

	return strings.Join(lines, "\n")
}

func formatTaskScopeDiscussionReceiveDetails(msg types.InboxMessage) string {
	if strings.TrimSpace(msg.MessageType) != "task_discussion" {
		return ""
	}
	target := strings.TrimSpace(msg.Target)
	if target == "" {
		target = FormatTarget(msg.ChannelType, msg.ChannelName, msg.ParentChannelType, msg.ParentChannelName)
	}
	if strings.TrimSpace(target) == "" {
		return ""
	}

	return strings.Join([]string{
		"task discussion: scope-native message",
		"scope: " + target,
		fmt.Sprintf(`reply: post a reply (target=%q, content="...") to continue the task discussion`, target),
	}, "\n")
}

const (
	receiveMessageContentMaxRunes        = 4096
	receiveThreadRootContentMaxRunes     = 2000
	receiveReactionPreviewMaxRunes       = 1200
	receiveAttachmentLineMaxRunes        = 1200
	receiveAttachmentMaxItems            = 10
	receiveTaskDescriptionMaxRunes       = 1200
	receiveTaskDiscussionBodyMaxRunes    = 2000
	receiveTaskChecklistMaxItems         = 8
	receiveTaskChecklistItemRunes        = 300
	receiveTeamDescriptionMaxRunes       = 6000
	receiveTeamRoleOverlayPromptMaxRunes = 4000
	receiveTeamRosterMaxItems            = 20
	receiveTeamRosterDutiesMaxRunes      = 1200
)

func formatTeamContextReceiveDetails(contexts []types.TeamContext) string {
	if len(contexts) == 0 {
		return ""
	}
	lines := []string{"team contexts:"}
	for _, context := range contexts {
		teamLabel := receiveTeamContextLabel(context.TeamHandle, context.TeamID)
		if teamLabel == "" {
			continue
		}
		header := "- team: " + teamLabel
		lines = append(lines, header)
		if teamID := strings.TrimSpace(context.TeamID); teamID != "" {
			lines = append(lines, "  team id: "+teamID)
		}
		lines = append(lines, formatTeamContextInvocationReceiveDetails(context.TeamID))
		if description := trimReceiveRunes(context.TeamDescription, receiveTeamDescriptionMaxRunes); description != "" {
			lines = append(lines, "  team duty:\n"+indentReceiveBlock(description, "    "))
		}
		if role := strings.TrimSpace(context.Role); role != "" {
			lines = append(lines, "  your Team role: "+role)
		}
		if role := strings.TrimSpace(context.RoleLabel); role != "" {
			lines = append(lines, "  your role label: "+role)
		}
		if overlay := trimReceiveRunes(context.RoleOverlayPrompt, receiveTeamRoleOverlayPromptMaxRunes); overlay != "" {
			lines = append(lines, "  your duty:\n"+indentReceiveBlock(overlay, "    "))
		}
		if roster := formatTeamRosterReceiveDetails(context.MemberRoster); roster != "" {
			lines = append(lines, "  team member roster:\n"+roster)
		}
		if memberIndex := formatTeamRoleProposalReceiveMemberIndex(context.Role, context.MemberRoster); memberIndex != "" {
			lines = append(lines, memberIndex)
		}
		if strings.EqualFold(strings.TrimSpace(context.Role), "leader") {
			lines = append(lines, formatTeamResponsibilityUpdateReceiveDetails())
		}
	}
	if len(lines) == 1 {
		return ""
	}
	return strings.Join(lines, "\n")
}

func formatTeamContextInvocationReceiveDetails(teamID string) string {
	return "  team invocation: you were awakened as this Team member. Other Team members may not have been awakened automatically."
}

func formatTeamResponsibilityUpdateReceiveDetails() string {
	return "  Team responsibility updates: when the current request asks you to create or revise Team responsibilities, submit only the changed description and existing member fields with `agentteam exec team update`; it will request Human approval. When replacing a rejected proposal, use a new requestId."
}

func formatTeamRosterReceiveDetails(roster []types.TeamRosterMember) string {
	if len(roster) == 0 {
		return ""
	}
	limit := len(roster)
	if limit > receiveTeamRosterMaxItems {
		limit = receiveTeamRosterMaxItems
	}
	lines := make([]string, 0, limit+1)
	for _, member := range roster[:limit] {
		label := receiveTeamLabel(member.Handle, member.DisplayName, member.ActorID)
		if label == "" {
			continue
		}
		details := make([]string, 0, 4)
		if kind := strings.TrimSpace(member.ActorKind); kind != "" {
			details = append(details, kind)
		}
		if role := strings.TrimSpace(member.RoleLabel); role != "" {
			details = append(details, role)
		}
		if member.WakeOnTeamMention {
			details = append(details, "active participant")
		} else {
			details = append(details, "as-needed participant")
		}
		if member.Self {
			details = append(details, "self")
		}
		suffix := ""
		if len(details) > 0 {
			suffix = " (" + strings.Join(details, ", ") + ")"
		}
		lines = append(lines, "    - "+label+suffix)
		if teamMemberID := strings.TrimSpace(member.TeamMemberID); teamMemberID != "" {
			lines = append(lines, "      teamMemberId: "+teamMemberID)
		}
		if role := strings.TrimSpace(member.Role); role != "" {
			lines = append(lines, "      Team role: "+role)
		}
		if duties := trimReceiveRunes(member.RoleOverlayPrompt, receiveTeamRosterDutiesMaxRunes); duties != "" {
			lines = append(lines, "      duty:\n"+indentReceiveBlock(duties, "        "))
		} else {
			lines = append(lines, "      duty: not configured")
		}
	}
	if remaining := len(roster) - limit; remaining > 0 {
		lines = append(lines, fmt.Sprintf("    - ... (%d more)", remaining))
	}
	return strings.Join(lines, "\n")
}

func formatTeamRoleProposalReceiveMemberIndex(role string, roster []types.TeamRosterMember) string {
	if !strings.EqualFold(strings.TrimSpace(role), "leader") ||
		len(roster) <= receiveTeamRosterMaxItems {
		return ""
	}
	hasMissingResponsibility := false
	for _, member := range roster {
		if strings.TrimSpace(member.RoleOverlayPrompt) == "" {
			hasMissingResponsibility = true
			break
		}
	}
	if !hasMissingResponsibility {
		return ""
	}
	lines := make([]string, 0, len(roster)+1)
	lines = append(lines, "  complete responsibility proposal member index (use every teamMemberId exactly once):")
	for _, member := range roster {
		teamMemberID := strings.TrimSpace(member.TeamMemberID)
		if teamMemberID == "" {
			return ""
		}
		line := "    - " + teamMemberID
		if label := receiveTeamLabel(member.Handle, member.DisplayName, member.ActorID); label != "" {
			line += ": " + label
		}
		if teamRole := strings.TrimSpace(member.Role); teamRole != "" {
			line += "; Team role: " + teamRole
		}
		if roleLabel := strings.TrimSpace(member.RoleLabel); roleLabel != "" {
			line += "; role label: " + roleLabel
		}
		lines = append(lines, line)
	}
	return strings.Join(lines, "\n")
}

func receiveTeamContextLabel(handle string, fallback string) string {
	if trimmedHandle := strings.TrimSpace(handle); trimmedHandle != "" {
		return "@" + strings.TrimPrefix(trimmedHandle, "@")
	}
	return strings.TrimSpace(fallback)
}

func receiveTeamLabel(handle string, displayName string, fallback string) string {
	if trimmedHandle := strings.TrimSpace(handle); trimmedHandle != "" {
		label := "@" + strings.TrimPrefix(trimmedHandle, "@")
		if trimmedDisplayName := strings.TrimSpace(displayName); trimmedDisplayName != "" && trimmedDisplayName != trimmedHandle {
			label += " (" + trimmedDisplayName + ")"
		}
		return label
	}
	return strings.TrimSpace(firstNonEmptyString(displayName, fallback))
}

func indentReceiveBlock(text string, prefix string) string {
	text = strings.TrimSpace(text)
	if text == "" {
		return ""
	}
	lines := strings.Split(text, "\n")
	for i := range lines {
		lines[i] = prefix + strings.TrimRight(lines[i], " \t")
	}
	return strings.Join(lines, "\n")
}

func formatTaskAttributeReceiveDetails(task map[string]any) string {
	if len(task) == 0 {
		return ""
	}

	var lines []string
	if status := stringMapValue(task, "status"); status != "" {
		lines = append(lines, "status: "+status)
	}
	if kind := stringMapValue(task, "kind"); kind != "" {
		lines = append(lines, "kind: "+kind)
	}
	if priority := stringMapValue(task, "priority"); priority != "" {
		lines = append(lines, "priority: "+priority)
	}
	if blockedReason := trimReceiveRunes(stringMapValue(task, "blockedReason"), receiveTaskChecklistItemRunes); blockedReason != "" {
		lines = append(lines, "blockedReason: "+blockedReason)
	}
	if assignee := receiveActorLabel(task["assigneeProfile"]); assignee != "" {
		lines = append(lines, "assignee: "+assignee)
	}
	if description := trimReceiveRunes(stringMapValue(task, "description"), receiveTaskDescriptionMaxRunes); description != "" {
		lines = append(lines, "description:\n"+description)
	}
	if criteria := stringSliceMapValue(task, "acceptanceCriteria"); len(criteria) > 0 {
		lines = append(lines, "acceptanceCriteria:")
		lines = append(lines, receiveChecklistLines(criteria)...)
	}
	if checklist := stringSliceMapValue(task, "reviewChecklist"); len(checklist) > 0 {
		lines = append(lines, "reviewChecklist:")
		lines = append(lines, receiveChecklistLines(checklist)...)
	}
	if refs := stringSliceMapValue(task, "pinnedMessageRefs"); len(refs) > 0 {
		lines = append(lines, "pinnedMessageRefs:")
		lines = append(lines, receiveChecklistLines(refs)...)
	}
	if urls := stringSliceMapValue(task, "relevantURLs"); len(urls) > 0 {
		lines = append(lines, "relevantURLs:")
		lines = append(lines, receiveChecklistLines(urls)...)
	}
	if refs := stringSliceMapValue(task, "workspaceRefs"); len(refs) > 0 {
		lines = append(lines, "workspaceRefs:")
		lines = append(lines, receiveChecklistLines(refs)...)
	}
	if len(lines) == 0 {
		return ""
	}
	lines = append(lines, "instruction: Treat these structured task attributes as requirements. Use them before acting, and update task fields when the task asks you to fill or revise them; do not fetch full task discussion history just to inspect these attributes.")
	return "task attributes:\n" + strings.Join(lines, "\n")
}

func stringMapValue(values map[string]any, key string) string {
	raw, ok := values[key]
	if !ok {
		return ""
	}
	text, ok := raw.(string)
	if !ok {
		return ""
	}
	return strings.TrimSpace(text)
}

func stringSliceMapValue(values map[string]any, key string) []string {
	raw, ok := values[key]
	if !ok || raw == nil {
		return nil
	}
	switch items := raw.(type) {
	case []string:
		out := make([]string, 0, len(items))
		for _, item := range items {
			if text := strings.TrimSpace(item); text != "" {
				out = append(out, text)
			}
		}
		return out
	case []any:
		out := make([]string, 0, len(items))
		for _, item := range items {
			text, ok := item.(string)
			if !ok {
				continue
			}
			if text = strings.TrimSpace(text); text != "" {
				out = append(out, text)
			}
		}
		return out
	default:
		return nil
	}
}

func intMapValue(values map[string]any, key string) (int, bool) {
	raw, ok := values[key]
	if !ok {
		return 0, false
	}
	switch value := raw.(type) {
	case int:
		return value, true
	case int8:
		return int(value), true
	case int16:
		return int(value), true
	case int32:
		return int(value), true
	case int64:
		return int(value), true
	case float32:
		return int(value), true
	case float64:
		return int(value), true
	case json.Number:
		number, err := value.Int64()
		if err != nil {
			return 0, false
		}
		return int(number), true
	default:
		return 0, false
	}
}

func receiveActorLabel(raw any) string {
	values, ok := raw.(map[string]any)
	if !ok {
		return ""
	}
	name := stringMapValue(values, "name")
	if name == "" {
		name = stringMapValue(values, "targetId")
	}
	if name == "" {
		name = stringMapValue(values, "serviceSlug")
	}
	if name == "" {
		name = stringMapValue(values, "displayName")
	}
	if name == "" {
		name = stringMapValue(values, "principalId")
	}
	if name == "" {
		name = stringMapValue(values, "memberId")
	}
	if name == "" {
		name = stringMapValue(values, "id")
	}
	if name == "" {
		return ""
	}
	if !strings.HasPrefix(name, "@") {
		name = "@" + name
	}
	return name
}

func receiveTaskLabel(rawTask map[string]any) string {
	taskNumber, ok := intMapValue(rawTask, "taskNumber")
	if !ok {
		return ""
	}
	title := stringMapValue(rawTask, "title")
	if title == "" {
		return fmt.Sprintf("#t%d", taskNumber)
	}
	return fmt.Sprintf(`#t%d "%s"`, taskNumber, title)
}

func receiveChecklistLines(items []string) []string {
	limit := len(items)
	if limit > receiveTaskChecklistMaxItems {
		limit = receiveTaskChecklistMaxItems
	}
	lines := make([]string, 0, limit+1)
	for _, item := range items[:limit] {
		if text := trimReceiveRunes(item, receiveTaskChecklistItemRunes); text != "" {
			lines = append(lines, "- "+text)
		}
	}
	if remaining := len(items) - limit; remaining > 0 {
		lines = append(lines, fmt.Sprintf("- ... (%d more)", remaining))
	}
	return lines
}

func trimReceiveRunes(text string, maxRunes int) string {
	text = strings.TrimSpace(text)
	if text == "" || maxRunes <= 0 {
		return text
	}
	runes := []rune(text)
	if len(runes) <= maxRunes {
		return text
	}
	return strings.TrimSpace(string(runes[:maxRunes])) + "..."
}

func TruncateAgentTeamToolRunes(text string, maxRunes int) string {
	return trimReceiveRunes(text, maxRunes)
}

func FormatTimestamp(ts string) string {
	if strings.TrimSpace(ts) == "" {
		return "-"
	}
	for _, layout := range []string{
		time.RFC3339,
		time.RFC3339Nano,
		"2006-01-02T15:04:05.999Z07:00",
		"2006-01-02T15:04:05Z07:00",
	} {
		if t, err := time.Parse(layout, ts); err == nil {
			return t.Local().Format("15:04:05")
		}
	}
	return ts
}

// FormatTarget renders a AgentTeam target string (e.g. "#general",
// "dm:@alice:shortid") from its structured channel fields. Moved here from
// the wire package because the formatting logic is tightly coupled to
// inbound message rendering.
func FormatTarget(channelType, channelName, parentChannelType, parentChannelName string) string {
	if strings.TrimSpace(channelType) == "workflow" {
		return "workflow:" + strings.TrimSpace(channelName)
	}
	if channelType == "thread" && parentChannelName != "" {
		shortID := NormalizeReplyThreadShortID(channelName)
		if parentChannelType == "dm" {
			return fmt.Sprintf("dm:@%s:%s", parentChannelName, shortID)
		}
		return fmt.Sprintf("#%s:%s", parentChannelName, shortID)
	}
	if channelType == "dm" {
		return "dm:@" + channelName
	}
	return "#" + channelName
}

// NormalizeReplyThreadShortID strips the legacy "thread-" prefix from a
// reply-thread short id. Moved here from the wire package alongside
// FormatTarget.
func NormalizeReplyThreadShortID(value string) string {
	trimmed := strings.TrimSpace(value)
	if strings.HasPrefix(strings.ToLower(trimmed), "thread-") {
		return trimmed[len("thread-"):]
	}
	return trimmed
}

// firstNonEmptyString returns the first trimmed non-empty value from
// values. Local copy of the wire package helper to keep mcpformat
// self-contained.
func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
