package agent

import (
	"context"
	"strings"
	"time"
	"unicode/utf8"

	"fiko-ai/internal/llm"
	"fiko-ai/internal/prompt"
)

// PersonaProfiles, kullanıcıya atanmış profilleri isteme girecek biçime çevirir.
//
// Neden burada çeviriyoruz? persona paketi ajanı bilmez; ajan da persona'yı
// bilmemelidir. Ortak dil prompt.UserProfile'dır (metin taşıyıcı tiptir).
func (a *Agent) PersonaProfiles(ctx context.Context, userID string) []prompt.UserProfile {
	// Nil ALICI da korunur: ajan yalnız profile bağlanmayı unutsa (ya da
	// SetPersonas(nil) çağrılsa) sohbet PANİKLEMEZ, profil kuralları olmadan
	// sürer. Profil bir zenginliktir, zorunlu özellik değil.
	if a == nil || a.personas == nil {
		return nil
	}
	list := a.personas.ActiveProfiles(ctx, userID)
	if len(list) == 0 {
		return nil
	}
	out := make([]prompt.UserProfile, 0, len(list))
	for _, p := range list {
		out = append(out, prompt.UserProfile{
			Name:         p.Name,
			Role:         p.Role,
			Instructions: p.Instructions,
			Notes:        p.Notes,
		})
	}
	return out
}

// Kalıcı bellek (Faz 10) sınırları. Ses gecikmesini ve küçük bağlam penceresini korurlar.
const (
	// MaxMemoriesPerTurn bir turda isteme girebilecek en çok anı.
	MaxMemoriesPerTurn = 5
	// MaxMemoryRunes tek anının üst sınırı (anılar zaten kısa; bu bir emniyet).
	MaxMemoryRunes = 300
	// MaxMemoriesTotalRunes tüm anıların toplam üst sınırı.
	MaxMemoriesTotalRunes = 1200
	// MemoryRecallTimeout getirmenin üst süresi. Aşılırsa tur anılar OLMADAN sürer.
	MemoryRecallTimeout = 3 * time.Second
	// MemoryLearnTimeout bir turdan öğrenmenin (LLM çıkarımı + kayıt) üst süresi.
	MemoryLearnTimeout = 60 * time.Second
)

// Recalled bellekten gelen ince anı görünümüdür (alan modelinin tamamı taşınmaz).
type Recalled struct {
	Kind    string
	Content string
	Score   float64
}

// MemoryRecaller kullanıcı mesajına uygun kalıcı anıları bulur.
//
// Sözleşme: HATA DÖNMEZ. Getirme bir yakınlık önerisidir; sağlayıcı çökse, bellek boş olsa ya da süre
// dolsa boş dilim dönmelidir ve ajan normal cevap verir (KnowledgeRetriever ile aynı ilke).
type MemoryRecaller interface {
	Recall(ctx context.Context, userID, query string, limit int) []Recalled
}

// LearnRequest tamamlanmış bir turdan öğrenme isteğidir.
type LearnRequest struct {
	UserID         string
	ConversationID string
	// Text kullanıcının mesajıdır. Asistanın cevabı KASTEN verilmez: model kendi cevabındaki
	// uydurmayı "kullanıcı gerçeği" diye kaydetmesin.
	Text    string
	Model   string
	Options llm.Options
}

// MemoryLearner bir turdan kalıcı bilgileri çıkarıp kaydeder. Kaydedilen (eklenen/güncellenen)
// anı sayısını ve KULLANICIYA SORULACAK soruları döndürür. Hata DÖNMEZ: öğrenme sohbeti
// hiçbir koşulda bozmamalıdır.
type MemoryLearner interface {
	// Learn, metinden kalıcı anılar çıkarır ve kaydeder; kaydedilenlerin sayısını
	// döner. SORU ÜRETMEZ: belirsiz bilgi sohbet sırasında `memory.ask` tool'u ile
	// kullanıcıya sorulur.
	Learn(ctx context.Context, req LearnRequest) int
}

// SetMemory kalıcı bellek getirme ve öğrenme kaynaklarını bağlar; ikisi de nil olabilir
// (nil = ilgili yön kapalı). Kurulumda, sunucu istek almadan önce çağrılmalıdur.
//
// Not: BURADA soru arayüzü yok. Kullanıcıya sormak `memory.ask` tool'unun işidir;
// ajan yalnızca tool kataloğunu sunar. Bu yüzden PendingQuestion/QuestionOption
// gibi türler bu pakette TANIMLI DEĞİLDİR (ajan, memory paketini bilmez).
func (a *Agent) SetMemory(r MemoryRecaller, l MemoryLearner) { a.memory, a.learner = r, l }

// recallMemoriesAsync anıları arka planda getirir ve sonucu TAM BİR KEZ gönderilen kanaldan verir.
// Bilgi tabanı getirmesiyle paralel çalışır: ikisi de sorgu embedding'i ürettiği için sıralı
// çalışsalar ses gecikmesi iki katına çıkardı.
func (a *Agent) recallMemoriesAsync(ctx context.Context, userID, userMessage string) <-chan []prompt.Remembered {
	ch := make(chan []prompt.Remembered, 1)
	if a.memory == nil || strings.TrimSpace(userMessage) == "" {
		ch <- nil
		return ch
	}
	go func() {
		defer func() {
			// Getirme asla turu düşürmemeli.
			if r := recover(); r != nil {
				a.log.Error("anı getirme çöktü", "panic", r)
				ch <- nil
			}
		}()
		ch <- a.retrieveMemories(ctx, userID, userMessage)
	}()
	return ch
}

// retrieveMemories turun anılarını getirir, uzunluk bütçesine sığdırır.
func (a *Agent) retrieveMemories(ctx context.Context, userID, userMessage string) []prompt.Remembered {
	if a.memory == nil || strings.TrimSpace(userMessage) == "" {
		return nil
	}
	mctx, cancel := context.WithTimeout(ctx, MemoryRecallTimeout)
	defer cancel()
	started := time.Now()
	got := a.memory.Recall(mctx, userID, userMessage, MaxMemoriesPerTurn)
	if len(got) == 0 {
		return nil
	}

	out := make([]prompt.Remembered, 0, len(got))
	budget := MaxMemoriesTotalRunes
	for _, m := range got {
		if len(out) >= MaxMemoriesPerTurn || budget <= 0 {
			break
		}
		text := clipRunes(strings.TrimSpace(m.Content), min(MaxMemoryRunes, budget))
		if text == "" {
			continue
		}
		budget -= utf8.RuneCountInString(text)
		out = append(out, prompt.Remembered{Kind: m.Kind, Content: text})
	}
	if len(out) > 0 {
		// İçerik (kişisel veri) log'a yazılmaz: yalnızca sayı.
		a.log.Info("kalıcı bellekten anı eklendi", "anı", len(out), "sureMs", time.Since(started).Milliseconds())
	}
	return out
}

// learn tamamlanmış bir turdan ARKA PLANDA öğrenir. Çağıran turu BEKLETMEZ.
//
// Ajan kapanırken (Close) iptal edilir ve bitmesi beklenir. wg.Add burada güvenlidir: çağıran tur
// hâlâ etkindir (sayaç ≥ 1), dolayısıyla Close'un Wait'i ile yarışmaz.
//
// Bu yol YALNIZCA kaydeder, SORU SORMAZ. Belirsiz bilgi sohbet sırasında modelin
// `memory.ask` tool'unu çağırmasıyla kullanıcıya sorulur; böylece karar anında,
// konuşmanın bağlamıyla birlikte verilir. (Eskiden burada ayrı bir soru üretim
// yolu vardı; iki yol birbirini bölüyordu.)
func (a *Agent) learn(in Input, text, model string, opts llm.Options) {
	if a.learner == nil || a.bg == nil || strings.TrimSpace(text) == "" {
		return
	}
	req := LearnRequest{
		UserID: in.UserID, ConversationID: in.ConversationID, Text: text, Model: model, Options: opts,
	}
	a.wg.Add(1)
	go func() {
		defer a.wg.Done()
		defer func() {
			if r := recover(); r != nil {
				a.log.Error("anı öğrenme çöktü", "panic", r, "conversation", in.ConversationID)
			}
		}()
		lctx, cancel := context.WithTimeout(a.bg, MemoryLearnTimeout)
		defer cancel()
		if saved := a.learner.Learn(lctx, req); saved > 0 {
			a.log.Info("kalıcı belleğe anı yazıldı", "anı", saved, "conversation", in.ConversationID)
		}
	}()
}
