package agent

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"sync"

	"go-micro.dev/v6/ai"
	"go-micro.dev/v6/store"
)

// Memory is an agent's conversation memory. Like the rest of the
// framework it is pluggable: the default is store-backed and durable
// across restarts, but any implementation can be supplied with
// WithMemory — in-process, a database, or a semantic/vector store.
type Memory interface {
	// Add appends a message to the conversation.
	Add(role, content string)
	// Messages returns the retained conversation, oldest first.
	Messages() []ai.Message
	// Clear resets the conversation.
	Clear()
}

// MemorySummaryFunc turns older conversation messages into a compact
// replacement message for active context. It is called while the default
// memory is locked, so implementations should be deterministic and avoid
// calling back into the same memory instance.
type MemorySummaryFunc func([]ai.Message) ai.Message

// MemoryCompaction configures deterministic, store-backed context compaction
// for the default memory implementation. When the retained conversation grows
// past MaxMessages, older turns are collapsed into a summary message while the
// newest KeepRecent turns stay verbatim for provider-neutral continuity.
type MemoryCompaction struct {
	MaxMessages int
	KeepRecent  int
	Summarize   MemorySummaryFunc
}

// MemoryRecall is implemented by memory backends that can retrieve durable
// prior context relevant to a new turn without replaying every stored message.
type MemoryRecall interface {
	Recall(query string, limit int) []ai.Message
}

// MemorySummary is implemented by memory backends that expose their current
// compacted summary for inspection. It lets long-running agents make memory
// compaction observable without coupling callers to a concrete store.
type MemorySummary interface {
	Summary() string
}

// Summary returns the current compacted-memory summary for m, when supported.
// It returns an empty string for memory backends that have not compacted or do
// not expose an inspectable summary.
func Summary(m Memory) string {
	if m == nil {
		return ""
	}
	summarizer, ok := m.(MemorySummary)
	if !ok {
		return ""
	}
	return summarizer.Summary()
}

// NewMemory returns the default store-backed memory: an in-process
// conversation buffer (truncated to limit) that persists to the store
// under key, so an agent picks up where it left off after a restart.
// A nil store or empty key yields non-persistent memory.
func NewMemory(s store.Store, key string, limit int) Memory {
	m := &storeMemory{store: s, key: key, hist: ai.NewHistory(limit)}
	m.load()
	return m
}

// NewRetrievalMemory returns store-backed memory that keeps a bounded active
// conversation and archives every turn for retrieval. It is useful when callers
// want relevant durable recall without summary compaction in the active context.
// A nil store or empty key keeps only the active in-process buffer.
func NewRetrievalMemory(s store.Store, key string, activeLimit int) Memory {
	m := &storeMemory{store: s, key: key, hist: ai.NewHistory(activeLimit), retrieveAll: true}
	m.load()
	return m
}

// NewCompactingMemory returns store-backed memory with explicit compaction and
// retrieval controls. It keeps all messages in the backing store, compacts older
// turns into a deterministic summary when the conversation exceeds maxMessages,
// and lets callers recall relevant prior turns with Recall.
func NewCompactingMemory(s store.Store, key string, maxMessages, keepRecent int) Memory {
	return NewCompactingMemoryWithOptions(s, key, MemoryCompaction{MaxMessages: maxMessages, KeepRecent: keepRecent})
}

// NewCompactingMemoryWithOptions returns store-backed memory configured with
// explicit compaction options, including an optional summarization hook.
func NewCompactingMemoryWithOptions(s store.Store, key string, compaction MemoryCompaction) Memory {
	maxMessages := compaction.MaxMessages
	keepRecent := compaction.KeepRecent
	if keepRecent <= 0 {
		keepRecent = maxMessages / 2
	}
	if keepRecent < 1 {
		keepRecent = 1
	}
	m := &storeMemory{
		store: s,
		key:   key,
		// Use an unlimited buffer here; compaction, not truncation, decides
		// what remains in active context so a summary can preserve older turns.
		hist: ai.NewHistory(0),
		compaction: MemoryCompaction{
			MaxMessages: maxMessages,
			KeepRecent:  keepRecent,
			Summarize:   compaction.Summarize,
		},
	}
	m.load()
	m.compact()
	return m
}

// NewInMemory returns conversation memory that is not persisted.
func NewInMemory(limit int) Memory {
	return &storeMemory{hist: ai.NewHistory(limit)}
}

// storeMemory is the default Memory: an ai.History buffer optionally
// persisted to a store.
type storeMemory struct {
	mu          sync.Mutex
	store       store.Store
	key         string
	hist        *ai.History
	compaction  MemoryCompaction
	archive     []ai.Message
	summary     string
	retrieveAll bool
}

func (m *storeMemory) Add(role, content string) {
	m.mu.Lock()
	if m.retrieveAll {
		m.archive = append(m.archive, ai.Message{Role: role, Content: content})
	}
	m.hist.Add(role, content)
	m.mu.Unlock()
	m.compact()
	m.save()
}

func (m *storeMemory) Messages() []ai.Message {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.hist.Messages()
}

func (m *storeMemory) Clear() {
	m.mu.Lock()
	m.hist.Reset()
	m.archive = nil
	m.summary = ""
	m.mu.Unlock()
	m.save()
}

// Summary returns the latest compacted summary text, if this memory has
// compacted older turns. The returned value is safe to show in debug UIs or
// checkpoints because it is exactly the summary retained in active context.
func (m *storeMemory) Summary() string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.summary
}

// Recall returns archived messages whose content contains words from query.
// It is deterministic and provider-neutral: no embeddings or model calls are
// required, but semantic/vector stores can replace Memory for richer retrieval.
// When created with NewRetrievalMemory the archive contains every persisted
// turn; when created with NewCompactingMemory it contains compacted older turns.
func (m *storeMemory) Recall(query string, limit int) []ai.Message {
	m.mu.Lock()
	defer m.mu.Unlock()
	if limit <= 0 {
		limit = 5
	}
	terms := recallTerms(query)
	type match struct {
		msg   ai.Message
		score int
		index int
	}
	matches := make([]match, 0, len(m.archive))
	for i := len(m.archive) - 1; i >= 0; i-- {
		msg := m.archive[i]
		if score := recallScore(msg, terms); score > 0 {
			matches = append(matches, match{msg: msg, score: score, index: i})
		}
	}
	sort.SliceStable(matches, func(i, j int) bool {
		if matches[i].score != matches[j].score {
			return matches[i].score > matches[j].score
		}
		return matches[i].index > matches[j].index
	})
	if len(matches) > limit {
		matches = matches[:limit]
	}
	out := make([]ai.Message, 0, len(matches))
	for _, match := range matches {
		out = append(out, match.msg)
	}
	return out
}

func (m *storeMemory) load() {
	if m.store == nil || m.key == "" {
		return
	}
	recs, err := m.store.Read(m.key)
	if err != nil || len(recs) == 0 {
		return
	}
	var state memoryState
	if err := json.Unmarshal(recs[0].Value, &state); err != nil {
		var msgs []ai.Message
		if err := json.Unmarshal(recs[0].Value, &msgs); err != nil {
			return
		}
		state.Messages = msgs
	}
	m.mu.Lock()
	m.archive = state.Archive
	m.summary = state.Summary
	if m.retrieveAll && len(m.archive) == 0 {
		m.archive = append(m.archive, state.Messages...)
	}
	for _, msg := range state.Messages {
		m.hist.Add(msg.Role, msg.Content)
	}
	if m.summary == "" {
		m.summary = currentMemorySummary(state.Messages)
	}
	m.mu.Unlock()
}

func (m *storeMemory) save() {
	if m.store == nil || m.key == "" {
		return
	}
	m.mu.Lock()
	data, err := json.Marshal(memoryState{
		Messages: m.hist.Messages(),
		Archive:  m.archive,
		Summary:  m.summary,
	})
	m.mu.Unlock()
	if err != nil {
		return
	}
	_ = m.store.Write(&store.Record{Key: m.key, Value: data})
}

func (m *storeMemory) compact() {
	if m.compaction.MaxMessages <= 0 {
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	msgs := m.hist.Messages()
	if len(msgs) <= m.compaction.MaxMessages {
		return
	}
	keep := m.compaction.KeepRecent
	if keep <= 0 || keep >= m.compaction.MaxMessages {
		keep = m.compaction.MaxMessages - 1
	}
	if keep < 1 {
		keep = 1
	}
	cut := len(msgs) - keep
	older := msgs[:cut]
	recent := msgs[cut:]
	m.archive = append(m.archive, older...)
	summarize := m.compaction.Summarize
	if summarize == nil {
		summarize = defaultMemorySummary
	}
	summary := summarize(older)
	if summary.Role == "" {
		summary.Role = "system"
	}
	m.summary = fmt.Sprint(summary.Content)
	m.hist.Reset()
	m.hist.Add(summary.Role, summary.Content)
	for _, msg := range recent {
		m.hist.Add(msg.Role, msg.Content)
	}
}

func currentMemorySummary(msgs []ai.Message) string {
	for _, msg := range msgs {
		if msg.Role != "system" {
			continue
		}
		text := fmt.Sprint(msg.Content)
		if strings.HasPrefix(text, "Conversation memory summary:") {
			return text
		}
	}
	return ""
}

func defaultMemorySummary(msgs []ai.Message) ai.Message {
	return ai.Message{
		Role:    "system",
		Content: fmt.Sprintf("Conversation memory summary: %s", summarizeMessages(msgs)),
	}
}

func summarizeMessages(msgs []ai.Message) string {
	var b strings.Builder
	for i, msg := range msgs {
		if i > 0 {
			b.WriteString(" | ")
		}
		fmt.Fprintf(&b, "%s: %s", msg.Role, compactText(fmt.Sprint(msg.Content), 120))
	}
	return b.String()
}

func compactText(s string, max int) string {
	s = strings.Join(strings.Fields(s), " ")
	if max > 0 && len(s) > max {
		return s[:max] + "…"
	}
	return s
}

func recallScore(msg ai.Message, terms []string) int {
	text := strings.ToLower(fmt.Sprint(msg.Content))
	score := 0
	for _, term := range terms {
		if strings.Contains(text, term) {
			score++
		}
	}
	return score
}

func recallTerms(query string) []string {
	seen := map[string]bool{}
	var terms []string
	for _, term := range strings.Fields(strings.ToLower(query)) {
		term = strings.Trim(term, ".,!?;:\"'()[]{}")
		if len(term) < 3 || seen[term] {
			continue
		}
		seen[term] = true
		terms = append(terms, term)
	}
	return terms
}

type memoryState struct {
	Messages []ai.Message `json:"messages"`
	Archive  []ai.Message `json:"archive,omitempty"`
	Summary  string       `json:"summary,omitempty"`
}
