package agent

import (
	"context"
	"fmt"
	"strings"
	"unicode"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
)

const claudeCompactCommand = "/compact"

// IsContextWindowExceeded implements drivercontract.ContextWindowExceededDriver.
// Claude Code reports input-context exhaustion through provider-specific
// errors. A max_tokens stop reason is output truncation, not evidence that the
// input context window is full.
func (d *ClaudeDriver) IsContextWindowExceeded(ev drivercontract.ParsedEvent) bool {
	if ev.Kind == consts.ParsedEventKindError && isClaudeContextError(ev.Message) {
		return true
	}
	return false
}

// QueueContextCompaction queues a Claude Code slash-command compaction for the
// next safe idle boundary after context-window exhaustion.
func (d *ClaudeDriver) QueueContextCompaction(ev drivercontract.ParsedEvent) bool {
	if !d.IsContextWindowExceeded(ev) {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.slashCommandsDisabled || d.compactionInProgress {
		return false
	}
	d.compactQueued = true
	return true
}

// StartIdleCompaction writes Claude Code's native /compact command to the
// stream-json stdin channel. Claude Code executes slash commands locally before
// model dispatch; completion is observed through the usual result/turn_end
// boundary.
func (d *ClaudeDriver) StartIdleCompaction(ctx context.Context, sessionID string) (bool, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	d.mu.Lock()
	if !d.compactQueued || d.compactionInProgress || d.slashCommandsDisabled {
		d.mu.Unlock()
		return false, nil
	}
	stdinPipe := d.stdinPipe
	d.compactQueued = false
	d.compactionInProgress = true
	d.compactBoundaryObserved = false
	d.mu.Unlock()
	if stdinPipe == nil {
		d.mu.Lock()
		d.compactQueued = true
		d.compactionInProgress = false
		d.mu.Unlock()
		return false, fmt.Errorf("claude stdin is unavailable")
	}
	data := buildClaudeStdinMsgWithOptions(claudeCompactCommand, sessionID, claudeStdinOptions{})
	select {
	case <-ctx.Done():
		d.mu.Lock()
		d.compactQueued = true
		d.compactionInProgress = false
		d.mu.Unlock()
		return false, ctx.Err()
	default:
	}
	if _, err := stdinPipe.Write(append(data, '\n')); err != nil {
		d.mu.Lock()
		d.compactQueued = true
		d.compactionInProgress = false
		d.mu.Unlock()
		return false, fmt.Errorf("claude compact stdin write: %w", err)
	}
	return true, nil
}

// IsOutputTruncated implements drivercontract.OutputTruncationDriver.
func (d *ClaudeDriver) IsOutputTruncated(ev drivercontract.ParsedEvent) bool {
	if ev.Kind == consts.ParsedEventKindTurnEnd &&
		ev.StopReason == consts.ParsedEventStopReasonMaxTokens &&
		!parsedBoolMetadata(ev.Metadata, "claude_result_abnormal") {
		return true
	}
	if ev.Kind != consts.ParsedEventKindError {
		return false
	}
	return isClaudeOutputTruncationError(ev.Message)
}

// isClaudeContextError checks whether an error message string indicates
// input-context exhaustion. Keep phrases explicit: generic combinations such
// as "context" + "limit" also describe configuration errors and must not
// discard a resumable session.
func isClaudeContextError(msg string) bool {
	normalized := normalizeClaudeErrorText(msg)
	if normalized == "" {
		return false
	}
	return strings.Contains(normalized, "prompt is too long") ||
		strings.Contains(normalized, "exceeds the context window") ||
		strings.Contains(normalized, "context window limit reached") ||
		strings.Contains(normalized, "reached its context window limit") ||
		strings.Contains(normalized, "context length exceeded") ||
		strings.Contains(normalized, "maximum context length") ||
		strings.Contains(normalized, "input too large")
}

func isClaudeOutputTruncationError(msg string) bool {
	normalized := normalizeClaudeErrorText(msg)
	if normalized == "" {
		return false
	}
	return strings.Contains(normalized, "response truncated") &&
		(strings.Contains(normalized, "finish reason length") ||
			strings.Contains(normalized, "max output tokens") ||
			strings.Contains(normalized, "hit max output"))
}

func normalizeClaudeErrorText(msg string) string {
	normalized := strings.Map(func(r rune) rune {
		if unicode.IsLetter(r) || unicode.IsNumber(r) {
			return unicode.ToLower(r)
		}
		return ' '
	}, strings.TrimSpace(msg))
	return strings.Join(strings.Fields(normalized), " ")
}
