package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/identitynotice"
	"github.com/tidbcloud/AgentTeam/desktop/internal/runtimeattachments"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/mcpformat"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

func (h *mcpHandler) formatInboxMessagePage(ctx context.Context, agentID string, page types.InboxReadPage, api *apiclient.Client, limit int) *types.MCPToolResult {
	messages := page.Messages
	if len(messages) == 0 {
		return toolText("No new messages.")
	}

	var latestActiveTarget types.ActiveTaskTarget
	for i := range messages {
		messages[i].ActiveTaskTarget = messages[i].ResolvedActiveTaskTarget()
		if target := messages[i].ActiveTaskTarget.Normalize(); target.Valid() {
			latestActiveTarget = target
		}
		workflowContext := messages[i].ResolvedWorkflowContext()
		messages[i].WorkflowContext = workflowContext
		messages[i].Metadata = types.MetadataWithoutWorkflowContext(messages[i].Metadata)
	}
	if latestActiveTarget.Valid() {
		h.setSessionActiveTaskTarget(agentID, latestActiveTarget)
	}

	freshClaims := make(map[string]string, len(page.RuntimeDeliveryClaims))
	for _, claim := range page.RuntimeDeliveryClaims {
		messageID := strings.TrimSpace(claim.MessageID)
		receipt := strings.TrimSpace(claim.Receipt)
		if messageID != "" && types.IsRuntimeDeliveryReceipt(receipt) {
			freshClaims[messageID] = receipt
		}
	}
	historicalMessageIDs := make([]string, 0, len(messages))
	for _, msg := range messages {
		if trimmed := strings.TrimSpace(msg.MessageID); trimmed != "" {
			if _, freshlyClaimed := freshClaims[trimmed]; !freshlyClaimed {
				historicalMessageIDs = append(historicalMessageIDs, trimmed)
			}
		}
	}
	if len(historicalMessageIDs) > 0 {
		_ = h.runtime.ReconcileInboxMessages(agentID, historicalMessageIDs)
	}

	if h.c != nil {
		if len(historicalMessageIDs) > 0 {
			suppressedSet := make(map[string]struct{})
			if h.runtime != nil {
				suppressed, err := h.runtime.ReconcileRuntimeAckedDeliveries(agentID, historicalMessageIDs)
				if err == nil {
					for _, id := range suppressed {
						if trimmed := strings.TrimSpace(id); trimmed != "" {
							suppressedSet[trimmed] = struct{}{}
						}
					}
				}
			}
			if h.runtime != nil {
				suppressed, err := h.runtime.ReconcileDeferredDeliveriesForAgent(agentID, historicalMessageIDs)
				if err == nil {
					for _, id := range suppressed {
						if trimmed := strings.TrimSpace(id); trimmed != "" {
							suppressedSet[trimmed] = struct{}{}
						}
					}
				}
			}
			if len(suppressedSet) > 0 {
				filtered := messages[:0]
				for _, msg := range messages {
					if _, ok := suppressedSet[strings.TrimSpace(msg.MessageID)]; !ok {
						filtered = append(filtered, msg)
					}
				}
				messages = filtered
				if len(messages) == 0 {
					return toolText("No new messages.")
				}
			}
		}
	}

	formatted := mcpformat.FormatReceiveMessages(messages, api)

	// Prepend pending identity notice so the agent stays aware of name changes.
	if sess, ok := h.runtime.Session(agentID); ok && sess.WorkDir != "" {
		if pending, ok, err := identitynotice.Load(sess.WorkDir); err == nil && ok {
			if notice := strings.TrimSpace(identitynotice.Format(pending)); notice != "" {
				formatted = notice + "\n\n" + formatted
			}
		}
	}

	if prepared := h.prepareRuntimeMessageDeliveries(agentID, messages); prepared != len(messages) {
		// A callback body must never escape without its Exec-side pending barrier.
		// Return no body and explicitly transfer claimed receipts to bounded
		// reconciliation instead of waiting for the final-hop orphan timer.
		receipts := make([]string, 0, len(page.RuntimeDeliveryClaims))
		for _, claim := range page.RuntimeDeliveryClaims {
			if receipt := strings.TrimSpace(claim.Receipt); types.IsRuntimeDeliveryReceipt(receipt) {
				receipts = append(receipts, receipt)
			}
		}
		if len(receipts) > 0 && page.RuntimeDeliveryCommitOwner != nil {
			page.RuntimeDeliveryCommitOwner.AcknowledgeMCPToolResultDelivery(receipts, types.MCPToolDeliveryUncertain)
		}
		return toolError("workflow callback surface is temporarily unavailable; retry after the daemon reconciles the message")
	}
	// Freshly claimed FIFO owners advance freshness only after their final MCP
	// response hop commits the receipt. Historical tool bodies retain the legacy
	// tracker path because no daemon-owned live owner exists for them.
	historicalMessages := make([]types.InboxMessage, 0, len(messages))
	receipts := make([]string, 0, len(messages))
	for _, msg := range messages {
		messageID := strings.TrimSpace(msg.MessageID)
		if receipt, freshlyClaimed := freshClaims[messageID]; freshlyClaimed {
			receipts = append(receipts, receipt)
			continue
		}
		historicalMessages = append(historicalMessages, msg)
	}
	h.rememberObservedMessages(agentID, historicalMessages)
	if page.HasMore || page.RemainingCount > 0 {
		if limit <= 0 {
			limit = len(messages)
		}
		formatted = strings.TrimRight(formatted, "\n") + fmt.Sprintf("\n\n--- %d new message(s) shown (limit=%d); has_more=%t; remaining_count=%d. Read exact-target history before retrying if more context is needed. ---",
			len(messages), limit, page.HasMore, page.RemainingCount)
	}
	if len(receipts) > 0 {
		markers := make([]string, 0, len(receipts))
		for _, receipt := range receipts {
			if marker := types.RuntimeDeliveryReceiptMarker(receipt); marker != "" {
				markers = append(markers, marker)
			}
		}
		// Keep markers before the body so the final result budget cannot truncate
		// away the durable Codex thread-history reconciliation evidence.
		if len(markers) > 0 {
			formatted = strings.Join(markers, "\n") + "\n" + formatted
		}
	}
	return toolTextWithRuntimeDeliveryReceipts(formatted, receipts, page.RuntimeDeliveryCommitOwner)
}

func toolTextWithRuntimeDeliveryReceipts(text string, receipts []string, owner types.MCPToolResultDeliveryOwner) *types.MCPToolResult {
	result := toolText(text)
	validated := make([]string, 0, len(receipts))
	seen := make(map[string]struct{}, len(receipts))
	for _, receipt := range receipts {
		receipt = strings.TrimSpace(receipt)
		if !types.IsRuntimeDeliveryReceipt(receipt) {
			continue
		}
		if _, duplicate := seen[receipt]; duplicate {
			continue
		}
		seen[receipt] = struct{}{}
		validated = append(validated, receipt)
	}
	if len(validated) > 0 {
		result.Meta = map[string]any{types.MCPRuntimeDeliveryReceiptsMetaKey: validated}
		result.DeliveryOwner = owner
	}
	return result
}

func (h *mcpHandler) prepareRuntimeMessageDeliveries(agentID string, messages []types.InboxMessage) int {
	if h == nil || h.c == nil || h.runtime == nil || len(messages) == 0 {
		return 0
	}
	return h.runtime.PrepareRuntimeMessageToolDeliveries(agentID, messages)
}

func (h *mcpHandler) setSessionActiveTaskTarget(agentID string, target types.ActiveTaskTarget) {
	if h == nil || h.c == nil || h.runtime == nil {
		return
	}
	h.runtime.SetActiveTaskTarget(agentID, target)
}

func (h *mcpHandler) toolReadHistory(ctx context.Context, api *apiclient.Client, args json.RawMessage) *types.MCPToolResult {
	var p struct {
		Target string  `json:"target"`
		Limit  float64 `json:"limit"`
		Before float64 `json:"before"`
		After  float64 `json:"after"`
	}
	if err := json.Unmarshal(args, &p); err != nil {
		return toolError("invalid arguments: " + err.Error())
	}
	target, err := normalizeMessageTarget(p.Target)
	if err != nil {
		return toolError(err.Error())
	}
	if target == "" {
		return toolError("target is required")
	}

	limit := float64(readHistoryDefaultLimit)
	if p.Limit > 0 {
		limit = p.Limit
		if limit > readHistoryMaxLimit {
			limit = readHistoryMaxLimit
		}
	}

	q := url.Values{}
	q.Set("channel", target)
	q.Set("limit", fmt.Sprintf("%.0f", limit))
	if p.Before > 0 {
		q.Set("before_seq", fmt.Sprintf("%.0f", p.Before))
	}
	if p.After > 0 {
		return toolError("after is not supported; use before to page older messages")
	}
	path := api.AgentRuntimePath("/history?" + q.Encode())

	var result struct {
		Messages            []historyMessage `json:"messages"`
		TargetKey           string           `json:"targetKey"`
		CoversThroughSeq    int64            `json:"coversThroughSeq"`
		HasMore             bool             `json:"has_more"`
		NextCursor          int64            `json:"next_cursor"`
		HistoryLimited      bool             `json:"historyLimited"`
		HistoryLimitMessage string           `json:"historyLimitMessage"`
		LastReadSeq         int64            `json:"last_read_seq"`
		Error               string           `json:"error"`
	}
	if err := api.Get(ctx, path, &result); err != nil {
		return toolError(err.Error())
	}
	if result.Error != "" {
		return toolError(result.Error)
	}
	if len(result.Messages) == 0 {
		return toolText("No messages in this target.")
	}

	lines := make([]string, 0, len(result.Messages))
	for _, message := range result.Messages {
		lines = append(lines, formatReadHistoryMessageLine(result.TargetKey, message))
	}

	header := fmt.Sprintf("## Message History for %s (%d messages)", target, len(result.Messages))
	if result.LastReadSeq > 0 && p.Before == 0 {
		header += fmt.Sprintf("\nYour last read position: seq %d.", result.LastReadSeq)
	}
	body := strings.Join(lines, "\n")
	footer := ""
	if result.HistoryLimited {
		msg := result.HistoryLimitMessage
		if strings.TrimSpace(msg) == "" {
			msg = "Message history is limited on this plan."
		}
		footer = "\n\n--- " + msg + " ---"
	} else if result.HasMore && len(result.Messages) > 0 {
		beforeSeq := result.NextCursor
		if beforeSeq == 0 {
			beforeSeq = oldestHistoryTargetSeq(result.TargetKey, result.Messages)
		}
		if beforeSeq > 0 {
			footer = fmt.Sprintf("\n\n--- %d messages shown. Use before=%d to load older messages. ---", len(result.Messages), beforeSeq)
		}
	}

	if result.CoversThroughSeq > 0 {
		sourceMessageID := ""
		for _, message := range result.Messages {
			if historyMessageBelongsToTarget(result.TargetKey, message) && message.TargetSeq == result.CoversThroughSeq && strings.TrimSpace(message.ID) != "" {
				sourceMessageID = message.ID
			}
		}
		h.rememberContiguousHistoryCoverage(firstNonEmptyString(api.LocalAgentID, api.AgentID), result.TargetKey, result.CoversThroughSeq, sourceMessageID)
	}

	return toolText(header + "\n\n" + body + footer)
}

func formatReadHistoryMessageLine(targetKey string, message historyMessage) string {
	timeText := mcpformat.FormatTimestamp(message.CreatedAt)
	messageID := "-"
	if strings.TrimSpace(message.ID) != "" {
		messageID = message.ID
	}
	author := message.AuthorIdentity()
	authorName := mcpformat.PromptAuthorMentionName(author)
	senderType := ""
	if author.ActorType == "agent" {
		senderType = " type=agent"
	}
	attachmentSuffix := ""
	if len(message.Attachments) > 0 {
		attachmentSuffix = formatHistoryAttachmentSuffix(message.Attachments)
	}
	if historyMessageIsExternalContext(targetKey, message) {
		return fmt.Sprintf("thread root: [parent_seq=%d msg=%s time=%s%s] @%s: %s%s",
			message.Seq, messageID, timeText, senderType, authorName, mcpformat.TruncateAgentTeamToolRunes(message.Content, historyContentMaxRunes), attachmentSuffix)
	}
	return fmt.Sprintf("[seq=%d msg=%s time=%s%s] @%s: %s%s",
		message.Seq, messageID, timeText, senderType, authorName, mcpformat.TruncateAgentTeamToolRunes(message.Content, historyContentMaxRunes), attachmentSuffix)
}

func historyMessageIsExternalContext(targetKey string, message historyMessage) bool {
	targetKey = strings.TrimSpace(targetKey)
	messageTargetKey := strings.TrimSpace(message.TargetKey)
	return targetKey != "" && messageTargetKey != "" && messageTargetKey != targetKey
}

func historyMessageBelongsToTarget(targetKey string, message historyMessage) bool {
	return !historyMessageIsExternalContext(targetKey, message)
}

func oldestHistoryTargetSeq(targetKey string, messages []historyMessage) int64 {
	oldestSeq := int64(0)
	for _, message := range messages {
		if !historyMessageBelongsToTarget(targetKey, message) {
			continue
		}
		seq := message.TargetSeq
		if seq == 0 {
			seq = message.Seq
		}
		if seq <= 0 {
			continue
		}
		if oldestSeq == 0 || seq < oldestSeq {
			oldestSeq = seq
		}
	}
	return oldestSeq
}

type historyMessage struct {
	Seq           int64               `json:"seq"`
	TargetKey     string              `json:"targetKey"`
	TargetSeq     int64               `json:"targetSeq"`
	ID            string              `json:"id"`
	SenderType    string              `json:"sender_type"`
	SenderName    string              `json:"sender_name"`
	AuthorProfile types.AuthorProfile `json:"author_profile"`
	Content       string              `json:"content"`
	CreatedAt     string              `json:"created_at"`
	Attachments   []historyAttachment `json:"attachments"`
}

func (message *historyMessage) UnmarshalJSON(data []byte) error {
	var raw struct {
		Seq                 int64               `json:"seq"`
		TargetKey           string              `json:"targetKey"`
		LegacyTargetKey     string              `json:"target_key"`
		TargetSeq           int64               `json:"targetSeq"`
		LegacyTargetSeq     int64               `json:"target_seq"`
		ID                  string              `json:"id"`
		SenderType          string              `json:"senderType"`
		LegacySenderType    string              `json:"sender_type"`
		SenderName          string              `json:"senderName"`
		LegacySenderName    string              `json:"sender_name"`
		AuthorProfile       types.AuthorProfile `json:"authorProfile"`
		LegacyAuthorProfile types.AuthorProfile `json:"author_profile"`
		Content             string              `json:"content"`
		CreatedAt           string              `json:"createdAt"`
		LegacyCreatedAt     string              `json:"created_at"`
		Attachments         []historyAttachment `json:"attachments"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	message.Seq = raw.Seq
	message.TargetKey = firstNonEmptyString(raw.TargetKey, raw.LegacyTargetKey)
	message.TargetSeq = raw.TargetSeq
	if message.TargetSeq == 0 {
		message.TargetSeq = raw.LegacyTargetSeq
	}
	message.ID = raw.ID
	message.SenderType = firstNonEmptyString(raw.SenderType, raw.LegacySenderType)
	message.SenderName = firstNonEmptyString(raw.SenderName, raw.LegacySenderName)
	message.AuthorProfile = raw.AuthorProfile
	if message.AuthorProfile == (types.AuthorProfile{}) {
		message.AuthorProfile = raw.LegacyAuthorProfile
	}
	message.Content = raw.Content
	message.CreatedAt = firstNonEmptyString(raw.CreatedAt, raw.LegacyCreatedAt)
	message.Attachments = raw.Attachments
	return nil
}

func (message historyMessage) AuthorIdentity() types.AuthorIdentity {
	return types.ResolveAuthorIdentity(message.SenderType, message.SenderName, message.AuthorProfile)
}

type historyAttachment struct {
	ID          string `json:"id"`
	Filename    string `json:"filename"`
	ContentType string `json:"contentType,omitempty"`
	SizeBytes   int64  `json:"sizeBytes,omitempty"`
}

func formatHistoryAttachmentSuffix(attachments []historyAttachment) string {
	if len(attachments) == 0 {
		return ""
	}

	visible := attachments
	omitted := 0
	if len(visible) > historyAttachmentMaxItems {
		omitted = len(visible) - historyAttachmentMaxItems
		visible = visible[:historyAttachmentMaxItems]
	}
	parts := make([]string, 0, len(visible)+1)
	for index, attachment := range visible {
		label := "attachment"
		if len(attachments) > 1 {
			label = fmt.Sprintf("attachment %d", index+1)
		}

		filename := mcpformat.TruncateAgentTeamToolRunes(strings.TrimSpace(attachment.Filename), 160)
		if filename == "" {
			filename = "unnamed file"
		}
		id := strings.TrimSpace(attachment.ID)
		details := make([]string, 0, 2)
		size := attachment.SizeBytes
		if size <= 0 {
			size = -1
		}
		if metadata := runtimeattachments.MetadataSummary(filename, attachment.ContentType, size); metadata != "" {
			details = append(details, metadata)
		}
		if id != "" {
			details = append(details, "id:"+id)
		}
		if len(details) > 0 {
			parts = append(parts, fmt.Sprintf("%s: %s (%s)", label, filename, strings.Join(details, ", ")))
			continue
		}
		parts = append(parts, fmt.Sprintf("%s: %s", label, filename))
	}
	if omitted > 0 {
		parts = append(parts, fmt.Sprintf("... %d attachment(s) omitted by per-item budget", omitted))
	}

	return "\n" + strings.Join(parts, "\n") + "\n-- use the attachment-view tool to inspect attachments --"
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func normalizeMessageTarget(target string) (string, error) {
	trimmed := strings.TrimSpace(target)
	if trimmed == "" {
		return "", nil
	}
	reference, err := domain.ParseMessageTargetReference(trimmed)
	if err != nil {
		return "", err
	}
	return reference.String(), nil
}
