package memory

import (
	"context"
	"errors"
	"log/slog"
	"time"

	"fiko-ai/internal/llm"
)

// LearnInput bir tamamlanmış turdan öğrenme isteğidir.
type LearnInput struct {
	UserID         string
	ConversationID string
	Text           string      // kullanıcının mesajı (asistan cevabı DEĞİL)
	Model          string      // çıkarım modeli; boşsa sağlayıcı varsayılanı
	Options        llm.Options // üretim parametreleri (sıcaklık çıkarımda sıfırlanır)
}

// Saved öğrenilen (eklenen ya da güncellenen) bir anıdır.
type Saved struct {
	Memory  *Memory
	Outcome Outcome // Created | Updated
}

// Learner çıkarımı ve kaydı birleştirir: kapı → ilgili anıları getir → LLM ile çıkar → Service.Add.
//
// Learner YALNIZCA otomatik (arka plan) öğrenmedir. Kullanıcıya soru sormak ve cevabı
// uygulamak Learner'dan BAĞIMSIZDIR (bkz. question.go): MEMORY_AUTO_LEARN=false iken de
// memory.ask ve onay kartları çalışmalıdır.
type Learner struct {
	svc *Service
	ex  *Extractor
	log *slog.Logger
}

// NewLearner öğreniciyi kurar.
func NewLearner(svc *Service, ex *Extractor, log *slog.Logger) *Learner {
	if log == nil {
		log = slog.Default()
	}
	return &Learner{svc: svc, ex: ex, log: log}
}

// Learn mesajdan kalıcı bilgileri çıkarıp kaydeder; yalnızca yeni/değişen anıları döndürür.
//
// HATA DÖNMEZ: öğrenme bir iyileştirmedir, sohbeti hiçbir koşulda bozmamalıdır. Her başarısızlık
// loglanır ve boş sonuç olur. Mesaj kalıcı bilgi taşımıyorsa (LooksMemorable) LLM hiç çağrılmaz.
//
// "Emin değilim" kararı burada YOKTUR: belirsiz bilgi çıktıya hiç alınmaz (bkz. extractSystem).
// Kullanıcıya sormak, sohbet sırasında modelin `memory.ask` tool'unu çağırmasıyla olur.
func (l *Learner) Learn(ctx context.Context, in LearnInput) []Saved {
	if l == nil || l.svc == nil || l.ex == nil {
		return nil
	}
	started := time.Now()

	// 1) KURAL TABANLI KAYIT (LLM'siz, her zaman çalışır).
	//
	// Neden önce? Otomatik öğrenme iki katmanlıdır ve İKİNCİ katman (LLM çıkarımı) ortama
	// bağımlıdır: model bellekte yüklenemezse `done_reason: load` ile boş döner. Bu katman
	// çalışmazken kullanıcının "bunu hatırla" isteği sessizce kaybolurdu.
	//
	// Bu katman YALNIZCA ekleme yapar; güncelleme kararı vermez (kullanıcı kararı).
	var saved []Saved
	rules := ExtractRules(in.Text)
	for _, c := range rules {
		m, outcome, err := l.svc.Add(ctx, AddInput{
			UserID: in.UserID, Kind: c.Kind, Content: c.Content, Origin: OriginAuto,
			ConversationID: in.ConversationID, Importance: c.Importance,
		})
		if err != nil {
			if errors.Is(err, ErrLimit) {
				l.log.Warn("anı sınırına ulaşıldı; otomatik öğrenme duruyor", "user", in.UserID)
				break
			}
			if ctx.Err() == nil {
				l.log.Warn("kural tabanlı anı kaydedilemedi", "err", err)
			}
			continue
		}
		if outcome == Created || outcome == Updated {
			saved = append(saved, Saved{Memory: m, Outcome: outcome})
		}
	}
	if len(saved) > 0 {
		l.log.Info("kural tabanlı anı kaydedildi", "kaydedilen", len(saved), "conversation", in.ConversationID)
	}

	// 2) LLM KARARI: yeni bilgi / güncelleme.
	//
	// MALİYET KAPISI: kural katmanı bir şey ürettiyse bu mesaj zaten işlendi ve LLM'in
	// işi kalmadı — çağrı boşa GPU yakardı. Kural katmanı boş dönüyorsa kalıcı bilgi
	// taşıyıp taşımadığına bakılır; taşımıyorsa mesaj burada biter (geçici iş emirleri).
	//
	// (Canlı hata: "Hafta sonları dağ yürüyüşlerine çıkıyorum" kural katmanına takılmıyordu
	// ama LooksMemorable da false dönüyordu; kalıbı olmayan kalıcı cümleler böylece
	// LLM'e hiç ulaşamıyordu.)
	if len(rules) > 0 || !LooksMemorable(in.Text) {
		return finish(l, saved, 0, started, in)
	}

	seen := map[string]bool{}
	for _, s := range saved {
		seen[HashOf(s.Memory.Content)] = true
	}
	known := l.knownAll(ctx, in.UserID, saved)
	decisions, err := l.ex.Decide(ctx, in.Model, in.Options, in.Text, known)
	if err != nil {
		if ctx.Err() == nil {
			l.log.Warn("anı çıkarımı başarısız", "err", err, "conversation", in.ConversationID)
		}
		return finish(l, saved, 0, started, in)
	}

	// Etiketli döngü: aşağıdaki switch içindeki `break` YALNIZCA switch'ten çıkar, döngüden
	// değil. Önceki sürümde anı sınırına ulaşınca ya da ctx iptal edilince kalan kararlar
	// yine tek tek denenip aynı hatayı veriyordu.
decide:
	for _, d := range decisions {
		if seen[HashOf(d.Content)] {
			continue // kural katmanı zaten kaydetti
		}
		switch d.Action {
		case ActionUpdate, ActionNew:
			// Model kararı olduğu gibi uygulanır: güncelleme için hedef verilmiştir.
			// (ActionUpdate + TargetID == "" ise Service normal akışa düşer.)
			m, outcome, err := l.svc.Add(ctx, AddInput{
				UserID: in.UserID, Kind: d.Kind, Content: d.Content, Origin: OriginAuto,
				ConversationID: in.ConversationID, Importance: d.Importance,
				TargetID: d.TargetID,
			})
			if err != nil {
				if errors.Is(err, ErrLimit) {
					l.log.Warn("anı sınırına ulaşıldı; otomatik öğrenme duruyor", "user", in.UserID)
					break decide
				}
				if ctx.Err() != nil {
					break decide
				}
				l.log.Warn("çıkarılan anı kaydedilemedi", "err", err)
				continue
			}
			if outcome == Created || outcome == Updated {
				saved = append(saved, Saved{Memory: m, Outcome: outcome})
			}
		default:
			// Eski bir istem/model hâlâ "ask" üretebilir. Sessizce yok saymak "soruldu"
			// izlenimini bırakmasın diye loglanır; soru yolu yalnızca memory.ask'tır.
			l.log.Warn("desteklenmeyen anı kararı yok sayıldı",
				"action", string(d.Action), "conversation", in.ConversationID)
		}
	}
	return finish(l, saved, len(decisions), started, in)
}

// knownOf, bu turda kaydedilen/güncellenen anıları modele "bunları zaten bildik" olarak
// verir; aynı bilgi ikinci kez üretilmez. Kimlikleri de geçer: model bir düzeltmede
// hedef seçebilmelidir.
func knownOf(saved []Saved) []Known {
	if len(saved) == 0 {
		return nil
	}
	out := make([]Known, 0, len(saved))
	for _, s := range saved {
		out = append(out, Known{ID: s.Memory.ID, Content: s.Memory.Content})
	}
	return out
}

// knownLimit, LLM'e "zaten bildiklerim" olarak verilen anı sayısı sınırıdır.
// Sınırsız liste prompt'u şişirir (ve VRAM'i zaten yormaktayız); en yeni/güçlü
// anılar yeterlidir çünkü LLM yalnızca TEKRARLARI elemek için bunları kullanır.
const knownLimit = 20

// knownAll, LLM'e verilecek "bilinen anılar" listesini kurar: hem kullanıcının ONAYDA
// KALAN anıları hem de bu turda yeni kaydedilenler.
//
// Neden ONAYDAKİLER de dahil? Daha önce yalnızca bu turun kayıtları geçiliyordu
// (knownOf); kural katmanı bir şey kaydetmediyse listeye HİÇBİR ŞEY girmiyordu ve
// model, kullanıcının zaten kayıtlı olan aynı bilgisini yeniden üretiyordu. Bu,
// her turda mükerrer kayıt demektir. Mükerrer kayıt ayrıca anı sınırını (MaxPerUser)
// hak etmeden tüketir.
func (l *Learner) knownAll(ctx context.Context, userID string, saved []Saved) []Known {
	out := knownOf(saved)
	if userID == "" {
		return out
	}
	// Mükerrer kayıtları önlemek için yalnızca uzun süreli anılar: kısa vadeli notlar
	// kalıcı kimlik değildir ve LLM'e hatırlatma gerektirmez.
	existing, err := l.svc.List(ctx, Filter{UserID: userID, Kind: KindLongTerm}, knownLimit, 0)
	if err != nil {
		// Depo okunamazsa yalnızca bu turun kayıtlarıyla devam et: LLM çağrısı ATLANMAZ.
		if ctx.Err() == nil {
			l.log.Warn("bilinen anılar okunamadı", "err", err)
		}
		return out
	}
	has := make(map[string]bool, len(out))
	for _, k := range out {
		has[fold(k.Content)] = true
	}
	for i := range existing {
		c := existing[i].Content
		if c == "" || has[fold(c)] {
			continue
		}
		has[fold(c)] = true
		// Kimlik ZORUNLUDUR: model güncelleme kararını hedef kimliğe göre verir.
		out = append(out, Known{ID: existing[i].ID, Content: c})
		if len(out) >= knownLimit {
			break
		}
	}
	return out
}

// finish öğrenme sonucunu loglar. İçerik (kişisel veri) log'a yazılmaz: yalnızca sayılar.
func finish(l *Learner, saved []Saved, decisions int, started time.Time, in LearnInput) []Saved {
	l.log.Info("anı öğrenme tamamlandı", "karar", decisions, "kaydedilen", len(saved),
		"sureMs", time.Since(started).Milliseconds(), "conversation", in.ConversationID)
	return saved
}
