package mcp

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// Core tool implementations: try AgentTeam API first, then fall back to store.

const (
	envWorkDir                 = "AGENTTEAM_EXEC_WORKDIR"
	inboxMessagesMaxLimit      = 50
	inboxMessagesDefaultLimit  = inboxMessagesMaxLimit
	readHistoryDefaultLimit    = 20
	readHistoryMaxLimit        = 100
	searchMessagesDefaultLimit = 20
	searchMessagesMaxLimit     = 50
	historyContentMaxRunes     = 4096
	historyAttachmentMaxItems  = 10
)

func (h *mcpHandler) toolSendMessage(ctx context.Context, agentID string, args json.RawMessage) *types.MCPToolResult {
	var p struct {
		Target           string   `json:"target"`
		Content          *string  `json:"content"`
		FilePaths        []string `json:"file_paths"`
		AttachmentIDs    []string `json:"attachment_ids"`
		TeamInvokeTeamID string   `json:"team_invoke_team_id"`
		AllowCrossTarget bool     `json:"allow_cross_target"`
	}
	decoder := json.NewDecoder(bytes.NewReader(args))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&p); err != nil {
		return toolError("invalid arguments: " + err.Error())
	}

	if p.Content == nil {
		return toolError("content is required")
	}
	content := *p.Content
	target := strings.TrimSpace(p.Target)
	if target == "" {
		return toolError("target is required")
	}
	target, err := normalizeMessageTarget(target)
	if err != nil {
		return toolError(err.Error())
	}

	if denied := h.checkSendTarget(ctx, agentID, target, p.AllowCrossTarget); denied != nil {
		return denied
	}

	serverID := "agentteam"
	if h.c != nil {
		serverID = h.c.ID()
	}
	limitKey := sendLimiterKey(serverID, target, agentID)
	if h.service != nil && h.service.limits != nil && !h.service.limits.Get(limitKey).Allow() {
		return toolError("rate limit exceeded: too many messages sent in a short period, please wait a moment before sending again")
	}

	attachmentIDs := make([]string, 0, len(p.AttachmentIDs))
	for _, id := range p.AttachmentIDs {
		if trimmed := strings.TrimSpace(id); trimmed != "" {
			attachmentIDs = append(attachmentIDs, trimmed)
		}
	}

	if len(attachmentIDs) > 0 {
		api := h.apiClient(agentID)
		if api == nil {
			return toolError("attachment_ids require agentteam API access")
		}
		return h.sendMessageViaAPI(ctx, agentID, api, target, content, p.FilePaths, attachmentIDs, p.TeamInvokeTeamID)
	}

	if api := h.apiClient(agentID); api != nil {
		return h.sendMessageViaAPI(ctx, agentID, api, target, content, p.FilePaths, nil, p.TeamInvokeTeamID)
	}

	// Fallback: send through channel.
	if strings.TrimSpace(p.TeamInvokeTeamID) != "" {
		return toolError("team_invoke_team_id requires agentteam API access")
	}
	if h.c == nil || h.runtime == nil {
		return toolError("agentteam API not available for this session")
	}
	sess, ok := h.runtime.Session(agentID)
	if !ok {
		return toolError("session not found")
	}

	msg := types.OutboundMessage{Content: strings.TrimSpace(normalizeEscapedAgentSendMessageContent(content))}
	if len(p.FilePaths) > 0 {
		attachments, err := resolveAttachmentPaths(p.FilePaths, sess.WorkDir)
		if err != nil {
			return toolError(err.Error())
		}
		msg.Attachments = attachments
	}

	serverAgentID := strings.TrimSpace(sess.ServerAgentID)
	if serverAgentID == "" {
		serverAgentID = strings.TrimSpace(agentID)
	}
	meta := &types.AgentTeamMeta{
		Target:  target,
		AgentID: serverAgentID,
	}
	if err := h.c.Send(ctx, meta, msg); err != nil {
		return toolError(err.Error())
	}
	h.recordAgentSent(agentID)
	return toolText(formatSendMessageSuccess("Message sent."))
}

func normalizeEscapedAgentSendMessageContent(content string) string {
	if strings.Contains(content, "\n") || strings.Contains(content, "\r") {
		return content
	}
	if !strings.Contains(content, `\n`) && !strings.Contains(content, `\r`) {
		return content
	}
	if !hasAccidentalEscapedNewlineOutsideInlineCode(content) {
		return content
	}
	replaced, _ := replaceEscapedNewlinesOutsideInlineCode(content, 0)
	return replaced
}

func hasAccidentalEscapedNewlineOutsideInlineCode(content string) bool {
	escapedCount, formattingLikely, _ := scanEscapedNewlinesOutsideInlineCode(content, 0)
	return formattingLikely || escapedCount >= 2
}

func scanEscapedNewlinesOutsideInlineCode(content string, inlineTicks int) (int, bool, int) {
	escapedCount := 0
	formattingLikely := false
	for index := 0; index < len(content); {
		if content[index] == '`' {
			runLen := countByteRun(content, index, '`')
			if inlineTicks == 0 {
				inlineTicks = runLen
			} else if runLen == inlineTicks {
				inlineTicks = 0
			}
			index += runLen
			continue
		}
		if inlineTicks == 0 && content[index] == '\\' {
			runLen, replacementWidth := escapedNewlineRun(content[index:])
			if runLen > 0 {
				escapedCount++
				formattingLikely = formattingLikely || newlineEscapeLooksLikeFormatting(content[index+replacementWidth:])
				index += runLen
				continue
			}
		}
		index++
	}
	return escapedCount, formattingLikely, inlineTicks
}

func replaceEscapedNewlinesOutsideInlineCode(content string, inlineTicks int) (string, int) {
	var builder strings.Builder
	builder.Grow(len(content))
	for index := 0; index < len(content); {
		if content[index] == '`' {
			runLen := countByteRun(content, index, '`')
			if inlineTicks == 0 {
				inlineTicks = runLen
			} else if runLen == inlineTicks {
				inlineTicks = 0
			}
			builder.WriteString(content[index : index+runLen])
			index += runLen
			continue
		}
		if inlineTicks == 0 && content[index] == '\\' {
			runLen, _ := escapedNewlineRun(content[index:])
			if runLen > 0 {
				builder.WriteByte('\n')
				index += runLen
				continue
			}
		}
		builder.WriteByte(content[index])
		index++
	}
	return builder.String(), inlineTicks
}

func countByteRun(content string, start int, target byte) int {
	runLen := 0
	for index := start; index < len(content) && content[index] == target; index++ {
		runLen++
	}
	return runLen
}

func escapedNewlineRun(content string) (int, int) {
	switch {
	case strings.HasPrefix(content, `\r\n`):
		return len(`\r\n`), 2
	case strings.HasPrefix(content, `\n`):
		return len(`\n`), 1
	case strings.HasPrefix(content, `\r`):
		return len(`\r`), 1
	default:
		return 0, 0
	}
}

func newlineEscapeLooksLikeFormatting(remainder string) bool {
	if remainder == "" {
		return false
	}
	if strings.HasPrefix(remainder, `\n`) || strings.HasPrefix(remainder, `\r\n`) || strings.HasPrefix(remainder, `\r`) {
		return true
	}
	switch remainder[0] {
	case '-', '*', '+', '>', '#':
		return true
	}
	if remainder[0] >= '0' && remainder[0] <= '9' {
		index := 1
		for index < len(remainder) && remainder[index] >= '0' && remainder[index] <= '9' {
			index++
		}
		if index < len(remainder) && remainder[index] == '.' {
			return true
		}
	}
	return false
}

type messageTargetNotActiveStructuredContent struct {
	Code            string   `json:"code"`
	RequestedTarget string   `json:"requestedTarget"`
	ActiveTargets   []string `json:"activeTargets"`
}

// checkSendTarget enforces the turn-scoped delivery scope on message_send.
//
// Contract: a successful check is the linearization point of THIS
// message_send request's authorization. The request may then complete even
// if its turn or session is replaced afterwards; a replacement never
// retroactively revokes an already-passed send. Conversely, any replacement
// that lands before or during the scope read fails closed.
// The scope is the turn message-send scope: the exact sendable targets the
// runtime delivered in the current authoritative request, derived from daemon
// delivery state and never from model input. When the scope has targets, a
// send may only address those; anything else requires the explicit
// allow_cross_target opt-in, a deliberate product-supported choice rather
// than an anomaly. An empty scope on a live
// runtime means the turn has no delivered context, so any valid
// explicit target is allowed. A nil result means the send may proceed.
func (h *mcpHandler) checkSendTarget(
	ctx context.Context,
	agentID string,
	target string,
	allowCrossTarget bool,
) *types.MCPToolResult {
	if h.sendScope == nil {
		// The provider is a required dependency; a nil here means a broken
		// wiring, so fail closed rather than silently allowing any target.
		return toolError("send scope provider unavailable. No message was sent.")
	}
	scope, ok := h.sendScope.SendScope(ctx, agentID)
	if !ok {
		return toolError("runtime scope unavailable for this session. No message was sent.")
	}
	if len(scope.Targets) == 0 {
		return nil
	}
	if scope.ContainsTarget(target) {
		return nil
	}
	if allowCrossTarget {
		return nil
	}
	active := make([]string, 0, len(scope.Targets))
	for _, scopeTarget := range scope.Targets {
		active = append(active, scopeTarget.Target)
	}
	quoted := make([]string, 0, len(active))
	for _, activeTarget := range active {
		quoted = append(quoted, strconv.Quote(activeTarget))
	}
	result := toolError(fmt.Sprintf(
		"No message was sent. Target %q isn't active in this turn. "+
			"Retry with one of the active targets: [%s]. If you intentionally "+
			"need to send across targets, retry with allow_cross_target: true.",
		target, strings.Join(quoted, ", "),
	))
	result.StructuredContent = messageTargetNotActiveStructuredContent{
		Code:            "target_not_active",
		RequestedTarget: target,
		ActiveTargets:   active,
	}
	return result
}
