package agent

import (
	"fmt"
	"unicode/utf8"

	"github.com/nextlevelbuilder/goclaw/internal/config"
	"github.com/nextlevelbuilder/goclaw/internal/providers"
)

// Context pruning defaults matching TS DEFAULT_CONTEXT_PRUNING_SETTINGS.
const (
	defaultKeepLastAssistants   = 3
	defaultSoftTrimRatio        = 0.3
	defaultHardClearRatio       = 0.5
	defaultMinPrunableToolChars = 50000
	defaultSoftTrimMaxChars     = 4000
	defaultSoftTrimHeadChars    = 1500
	defaultSoftTrimTailChars    = 1500
	defaultHardClearPlaceholder = "[Old tool result content cleared]"
	charsPerTokenEstimate       = 4
)

// effectivePruningSettings holds resolved pruning settings with defaults applied.
type effectivePruningSettings struct {
	keepLastAssistants   int
	softTrimRatio        float64
	hardClearRatio       float64
	minPrunableToolChars int
	softTrimMaxChars     int
	softTrimHeadChars    int
	softTrimTailChars    int
	hardClearEnabled     bool
	hardClearPlaceholder string
}

// resolvePruningSettings applies defaults to user config.
func resolvePruningSettings(cfg *config.ContextPruningConfig) *effectivePruningSettings {
	s := &effectivePruningSettings{
		keepLastAssistants:   defaultKeepLastAssistants,
		softTrimRatio:        defaultSoftTrimRatio,
		hardClearRatio:       defaultHardClearRatio,
		minPrunableToolChars: defaultMinPrunableToolChars,
		softTrimMaxChars:     defaultSoftTrimMaxChars,
		softTrimHeadChars:    defaultSoftTrimHeadChars,
		softTrimTailChars:    defaultSoftTrimTailChars,
		hardClearEnabled:     true,
		hardClearPlaceholder: defaultHardClearPlaceholder,
	}

	if cfg == nil {
		return s
	}

	if cfg.KeepLastAssistants > 0 {
		s.keepLastAssistants = cfg.KeepLastAssistants
	}
	if cfg.SoftTrimRatio > 0 && cfg.SoftTrimRatio <= 1 {
		s.softTrimRatio = cfg.SoftTrimRatio
	}
	if cfg.HardClearRatio > 0 && cfg.HardClearRatio <= 1 {
		s.hardClearRatio = cfg.HardClearRatio
	}
	if cfg.MinPrunableToolChars > 0 {
		s.minPrunableToolChars = cfg.MinPrunableToolChars
	}

	if cfg.SoftTrim != nil {
		if cfg.SoftTrim.MaxChars > 0 {
			s.softTrimMaxChars = cfg.SoftTrim.MaxChars
		}
		if cfg.SoftTrim.HeadChars > 0 {
			s.softTrimHeadChars = cfg.SoftTrim.HeadChars
		}
		if cfg.SoftTrim.TailChars > 0 {
			s.softTrimTailChars = cfg.SoftTrim.TailChars
		}
	}

	if cfg.HardClear != nil {
		if cfg.HardClear.Enabled != nil {
			s.hardClearEnabled = *cfg.HardClear.Enabled
		}
		if cfg.HardClear.Placeholder != "" {
			s.hardClearPlaceholder = cfg.HardClear.Placeholder
		}
	}

	return s
}

// pruneContextMessages trims old tool results to reduce context window usage.
// Matching TS src/agents/pi-extensions/context-pruning/pruner.ts.
//
// Two-pass approach:
//  1. Soft trim: keep head + tail of long tool results, drop middle.
//  2. Hard clear: replace entire tool result with placeholder.
//
// Only tool results older than keepLastAssistants are eligible for pruning.
// Returns a new slice if any changes were made, otherwise the original.
func pruneContextMessages(msgs []providers.Message, contextWindowTokens int, cfg *config.ContextPruningConfig) []providers.Message {
	if cfg == nil || cfg.Mode != "cache-ttl" {
		return msgs
	}
	if contextWindowTokens <= 0 || len(msgs) == 0 {
		return msgs
	}

	settings := resolvePruningSettings(cfg)
	charWindow := contextWindowTokens * charsPerTokenEstimate

	// Find cutoff: protect last N assistant messages.
	cutoffIndex := findAssistantCutoff(msgs, settings.keepLastAssistants)
	if cutoffIndex < 0 {
		return msgs
	}

	// Find first user message — never prune before it (protects bootstrap reads).
	pruneStart := len(msgs)
	for i, m := range msgs {
		if m.Role == "user" {
			pruneStart = i
			break
		}
	}

	// Estimate total chars.
	totalChars := 0
	for _, m := range msgs {
		totalChars += estimateMessageChars(m)
	}

	ratio := float64(totalChars) / float64(charWindow)
	if ratio < settings.softTrimRatio {
		return msgs // context is small enough
	}

	// Collect prunable tool result indexes.
	var prunableIndexes []int
	for i := pruneStart; i < cutoffIndex; i++ {
		if msgs[i].Role == "tool" && msgs[i].Content != "" {
			prunableIndexes = append(prunableIndexes, i)
		}
	}

	if len(prunableIndexes) == 0 {
		return msgs
	}

	// Pass 1: Soft trim long tool results.
	var result []providers.Message
	for i := range prunableIndexes {
		idx := prunableIndexes[i]
		msg := msgs[idx]
		msgChars := estimateMessageChars(msg)

		if msgChars <= settings.softTrimMaxChars {
			continue
		}

		// Lazy copy
		if result == nil {
			result = make([]providers.Message, len(msgs))
			copy(result, msgs)
		}

		head := takeHead(msg.Content, settings.softTrimHeadChars)
		tail := takeTail(msg.Content, settings.softTrimTailChars)
		trimmed := fmt.Sprintf("%s\n...\n%s\n\n[Tool result trimmed: kept first %d chars and last %d chars of %d chars.]",
			head, tail, settings.softTrimHeadChars, settings.softTrimTailChars, msgChars)

		result[idx] = providers.Message{
			Role:       msg.Role,
			Content:    trimmed,
			ToolCallID: msg.ToolCallID,
		}
		totalChars += len(trimmed) - msgChars
	}

	output := msgs
	if result != nil {
		output = result
	}

	// Re-check ratio after soft trim.
	ratio = float64(totalChars) / float64(charWindow)
	if ratio < settings.hardClearRatio || !settings.hardClearEnabled {
		return output
	}

	// Check min prunable chars threshold.
	prunableChars := 0
	for _, idx := range prunableIndexes {
		prunableChars += estimateMessageChars(output[idx])
	}
	if prunableChars < settings.minPrunableToolChars {
		return output
	}

	// Pass 2: Hard clear — replace entire tool results with placeholder.
	if result == nil {
		result = make([]providers.Message, len(msgs))
		copy(result, msgs)
		output = result
	}

	for _, idx := range prunableIndexes {
		if ratio < settings.hardClearRatio {
			break
		}
		msg := output[idx]
		beforeChars := estimateMessageChars(msg)

		output[idx] = providers.Message{
			Role:       msg.Role,
			Content:    settings.hardClearPlaceholder,
			ToolCallID: msg.ToolCallID,
		}
		afterChars := len(settings.hardClearPlaceholder)
		totalChars += afterChars - beforeChars
		ratio = float64(totalChars) / float64(charWindow)
	}

	return output
}

// findAssistantCutoff returns the index of the Nth-from-last assistant message.
// Messages at or after this index are protected from pruning.
// Returns -1 if not enough assistant messages exist.
func findAssistantCutoff(msgs []providers.Message, keepLast int) int {
	if keepLast <= 0 {
		return len(msgs)
	}

	remaining := keepLast
	for i := len(msgs) - 1; i >= 0; i-- {
		if msgs[i].Role == "assistant" {
			remaining--
			if remaining == 0 {
				return i
			}
		}
	}
	return -1
}

// estimateMessageChars returns the character count of a message's content.
func estimateMessageChars(m providers.Message) int {
	return utf8.RuneCountInString(m.Content)
}

// takeHead returns the first n runes of s.
func takeHead(s string, n int) string {
	if n <= 0 {
		return ""
	}
	runes := []rune(s)
	if len(runes) <= n {
		return s
	}
	return string(runes[:n])
}

// takeTail returns the last n runes of s.
func takeTail(s string, n int) string {
	if n <= 0 {
		return ""
	}
	runes := []rune(s)
	if len(runes) <= n {
		return s
	}
	return string(runes[len(runes)-n:])
}
