package memory

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sort"
	"strings"
	"sync"
	"time"

	"fiko-ai/internal/event"
	"fiko-ai/internal/llm"
	"fiko-ai/pkg/id"
)

// Hibrit skor ağırlıkları skill/bilgi tabanı aramasıyla aynıdır.
const (
	semanticWeight = 0.8
	keywordWeight  = 0.2

	DefaultSearchLimit = 5
	MaxSearchLimit     = 50

	defaultEmbedTimeout = 15 * time.Second
	// defaultSupersedeThreshold: yeni anı, mevcut bir anıya bu kosinüs benzerliğinden yakınsa
	// AYNI konu sayılır ve yenisi eskisinin yerini alır ("Go'yu sevmiyor" → "Go'yu seviyor").
	defaultSupersedeThreshold = 0.93
)

// Options servis ayarlarıdır; sıfır değerler varsayılanla değiştirilir.
type Options struct {
	Logger             *slog.Logger
	EmbedTimeout       time.Duration
	MaxPerUser         int
	SupersedeThreshold float64
	Now                func() time.Time // testler için
}

// Outcome Add'in ne yaptığını bildirir.
type Outcome string

const (
	// Created yeni anı eklendi.
	Created Outcome = "created"
	// Updated mevcut anı değişti (yeni bilgi eskisinin yerini aldı ya da kaynak kullanıcıya yükseldi).
	Updated Outcome = "updated"
	// Unchanged bilgi zaten biliniyordu ya da kullanıcının elle yazdığı anı korundu.
	Unchanged Outcome = "unchanged"
)

// Service bellek iş kurallarıdır: doğrulama, embedding, tekrar/yerine geçme kararı, arama.
// Eşzamanlı kullanıma güvenlidir.
type Service struct {
	repo Repository
	bus  event.Bus // nil olabilir
	opt  Options
	log  *slog.Logger

	mu    sync.RWMutex
	emb   llm.Embedder // nil: yalnızca anahtar kelime
	model string

	bg       context.Context // Close ile iptal edilir; arka plan işleri bunu kullanır
	bgCancel context.CancelFunc
	wg       sync.WaitGroup
	closed   bool
}

// NewService servisi kurar. bus nil olabilir.
func NewService(repo Repository, bus event.Bus, opt Options) *Service {
	if opt.Logger == nil {
		opt.Logger = slog.Default()
	}
	if opt.EmbedTimeout <= 0 {
		opt.EmbedTimeout = defaultEmbedTimeout
	}
	if opt.MaxPerUser <= 0 {
		opt.MaxPerUser = DefaultMaxPerUser
	}
	if opt.SupersedeThreshold <= 0 || opt.SupersedeThreshold > 1 {
		opt.SupersedeThreshold = defaultSupersedeThreshold
	}
	if opt.Now == nil {
		opt.Now = time.Now
	}
	bg, cancel := context.WithCancel(context.Background())
	return &Service{repo: repo, bus: bus, opt: opt, log: opt.Logger, bg: bg, bgCancel: cancel}
}

// SetEmbedder embedding sağlayıcısını bağlar; nil vermek anlamsal aramayı kapatır.
// Model adı vektörle birlikte saklanır: model değişince eski vektörler karşılaştırılmaz.
func (s *Service) SetEmbedder(e llm.Embedder, model string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.emb, s.model = e, model
}

func (s *Service) embedder() (llm.Embedder, string) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.emb, s.model
}

// Close arka plan işlerini (kullanım sayacı, yeniden indeksleme) iptal eder ve bitmelerini bekler.
func (s *Service) Close(ctx context.Context) error {
	s.mu.Lock()
	s.closed = true
	s.mu.Unlock()
	s.bgCancel()
	done := make(chan struct{})
	go func() { s.wg.Wait(); close(done) }()
	select {
	case <-done:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

// goBG arka plan işi başlatır; servis kapanmışsa başlatmaz (Wait ile Add yarışı olmasın diye kilitli).
func (s *Service) goBG(fn func(ctx context.Context)) bool {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return false
	}
	s.wg.Add(1)
	s.mu.Unlock()
	go func() {
		defer s.wg.Done()
		defer func() {
			if r := recover(); r != nil {
				s.log.Error("bellek arka plan işi çöktü", "panic", r)
			}
		}()
		fn(s.bg)
	}()
	return true
}

// embed metni vektöre çevirir. Sağlayıcı yoksa ErrNoEmbedder.
func (s *Service) embed(ctx context.Context, text string) ([]float32, string, error) {
	emb, model := s.embedder()
	if emb == nil {
		return nil, "", ErrNoEmbedder
	}
	ectx, cancel := context.WithTimeout(ctx, s.opt.EmbedTimeout)
	defer cancel()
	v, err := emb.Embed(ectx, text)
	if err != nil {
		return nil, "", err
	}
	return v, model, nil
}

func (s *Service) publish(ctx context.Context, t event.Type, userID string, data map[string]any) {
	if s.bus == nil {
		return
	}
	s.bus.Publish(ctx, event.Event{Type: t, UserID: userID, Data: data})
}

// AddInput yeni anı isteğidir.
type AddInput struct {
	UserID         string
	Kind           Kind   // boş = long_term
	Content        string // zorunlu
	Tags           []string
	Origin         Origin  // boş = user
	ConversationID string  // otomatik çıkarımda kaynağın izi
	Importance     float64 // 0 = varsayılan (user 0.8, auto 0.6)
	// TargetID, bu bilginin GÜNCELLEMESİ gereken mevcut anıdır.
	//
	// Boşsa normal akış yürür (hash → benzerlik → yeni kayıt). Doluysa o kayıt
	// doğrudan güncellenir: kozinüs eşiğine BAKILMAZ.
	//
	// Neden? (Kullanıcı kararı: "bir yere düşmesin, LLM karar versin") Metin benzerliği
	// bir düzeltmeyi yakalayamaz: "2014 doğumlu" ile "2013 doğumlu" kosinüsçe yakın görünse de
	// eşiğin altında kalabiliyor ve güncelleme sessizce yapılmıyordu. Kararı LLM verir,
	// hedefi buradan bildirilir.
	TargetID string
}

// Add anıyı ekler; zaten biliniyorsa tekrar etmez, aynı konuda daha yeni bilgi varsa eskisinin
// yerini alır. Dönen Outcome neyin olduğunu söyler.
//
// Kural: kullanıcının ELLE yazdığı anı otomatik çıkarımla ezilmez; otomatik anının yerine ise
// yenisi geçer ("daha yeni bilgi kazanır").
func (s *Service) Add(ctx context.Context, in AddInput) (*Memory, Outcome, error) {
	var verrs []FieldError
	if strings.TrimSpace(in.UserID) == "" {
		verrs = append(verrs, FieldError{Field: "userId", Message: "kullanıcı gerekli"})
	}
	if in.Kind == "" {
		in.Kind = KindLongTerm
	}
	if !in.Kind.Valid() {
		verrs = append(verrs, FieldError{Field: "kind", Message: "kind long_term, semantic veya episodic olmalı"})
	}
	if in.Origin == "" {
		in.Origin = OriginUser
	}
	if !in.Origin.Valid() {
		verrs = append(verrs, FieldError{Field: "origin", Message: "origin user veya auto olmalı"})
	}
	content, ferr := validateContent(in.Content)
	if ferr != nil {
		verrs = append(verrs, *ferr)
	}
	tags, ferr := cleanTags(in.Tags)
	if ferr != nil {
		verrs = append(verrs, *ferr)
	}
	if in.Importance < 0 || in.Importance > 1 {
		verrs = append(verrs, FieldError{Field: "importance", Message: "importance 0..1 aralığında olmalı"})
	}
	if len(verrs) > 0 {
		return nil, "", &ValidationError{Errors: verrs}
	}
	if in.Importance == 0 {
		in.Importance = 0.6
		if in.Origin == OriginUser {
			in.Importance = 0.8
		}
	}
	hash := HashOf(content)

	// 0) HEDEFLİ GÜNCELLEME: LLM bu bilginin mevcut bir kaydın düzeltmesi olduğunu
	// bildirdiyse O KAYIT doğrudan güncellenir; benzerlik eşiğine bakılmaz.
	if in.TargetID != "" {
		old, err := s.repo.Get(ctx, in.UserID, in.TargetID)
		switch {
		case err == nil:
			return s.replace(ctx, old, in, content, tags, hash)
		case errors.Is(err, ErrNotFound):
			// Hedef silinmiş/yok: sessizce yeni kayda düşmek yerine normal akışa devam et.
			s.log.Warn("hedeflenen anı bulunamadı; yeni kayıt deneniyor", "id", in.TargetID, "user", in.UserID)
		default:
			return nil, "", err
		}
	}

	// 1) Birebir aynı bilgi.
	switch ex, err := s.repo.GetByHash(ctx, in.UserID, hash); {
	case err == nil:
		return s.reaffirm(ctx, ex, in)
	case !errors.Is(err, ErrNotFound):
		return nil, "", err
	}

	// 2) Vektör varsa: aynı konuda yakın bir anı var mı?
	vec, model, eerr := s.embed(ctx, content)
	if eerr != nil && !errors.Is(eerr, ErrNoEmbedder) {
		if ctx.Err() != nil {
			return nil, "", ctx.Err()
		}
		s.log.Warn("anı embedding'i üretilemedi; anahtar kelimeyle aranabilir olarak kaydediliyor", "err", eerr)
	}
	if vec != nil {
		if near, score := s.nearest(ctx, in.UserID, vec, model); near != nil && score >= s.opt.SupersedeThreshold {
			return s.supersede(ctx, near, in, content, tags, hash, vec, model)
		}
	}

	// 3) Yeni anı.
	n, err := s.repo.Count(ctx, in.UserID)
	if err != nil {
		return nil, "", err
	}
	if n >= s.opt.MaxPerUser {
		return nil, "", ErrLimit
	}
	now := s.opt.Now().UTC()
	m := &Memory{
		ID: id.New("mem"), UserID: in.UserID, Kind: in.Kind, Content: content, Tags: tags,
		Origin: in.Origin, ConversationID: in.ConversationID, Importance: clamp01(in.Importance),
		CreatedAt: now, UpdatedAt: now, Hash: hash, Embedding: vec, EmbedModel: model,
	}
	if err := s.repo.Create(ctx, m); err != nil {
		if errors.Is(err, ErrDuplicate) { // yarış: aynı anda eklenen aynı bilgi
			if ex, gerr := s.repo.GetByHash(ctx, in.UserID, hash); gerr == nil {
				return s.reaffirm(ctx, ex, in)
			}
		}
		return nil, "", err
	}
	s.publish(ctx, event.MemoryAdded, in.UserID, map[string]any{"id": m.ID, "kind": m.Kind, "origin": m.Origin})
	out := m.Clone()
	out.Embedding = nil
	return &out, Created, nil
}

// replace, hedeflenen (TargetID) kaydın İÇERİĞİNİ yeni bilgiyle değiştirir. Embedding
// mutlaka yeniden üretilir: eski vektör artık yanlış metni temsil eder ve arama bozulur.
//
// Kullanıcının ELLE girdiği (OriginUser) kayıt otomatik çıkarımla EZİLMEZ — bu, supersede
// ile aynı güvenlik kuralıdır: kullanıcı bilgiyi kendi eliyle yazdıysa, sohbetten çıkan bir
// varsayım onu sessizce değiştiremez. Bu yüzden "hayır, 2013'lü" gibi bir düzeltme, elle
// yazılmış bir kaydı güncellemez; yalnızca otomatik kayıtları (OriginAuto) günceller.
// Kullanıcı kendi kaydını arayüzden düzenleyebilir.
func (s *Service) replace(ctx context.Context, old *Memory, in AddInput, content string,
	tags []string, hash string) (*Memory, Outcome, error) {
	if old.Origin == OriginUser && in.Origin == OriginAuto {
		out := old.Clone()
		out.Embedding = nil
		return &out, Unchanged, nil
	}
	// Hedeflenen kaydın içeriği değiştiyse Updated, aynıysa Unchanged döner.
	// Neden? "Ben Angular kullanıyorum" ikinci kez söylendiğinde kayıt GÜNCELLENMEMİŞTİR;
	// aynı özne (kullanıcı) bulunduğu için hedeflenmiş olsa bile içerik birebir aynıdır.
	// Kullanıcıya/arayüze "güncellendi" sinyali göndermek yanlış olur.
	contentChanged := old.Content != content
	// İçerik değiştiğine göre yeni hash; eski hash ile çakışma olursa depo reddeder.
	old.Content, old.Hash = content, hash
	old.Kind = in.Kind
	if len(tags) > 0 {
		old.Tags = tags
	}
	// Embedding'i yeniden üret: metin değişti, vektör de değişmeli.
	if vec, model, err := s.embed(ctx, content); err == nil {
		old.Embedding, old.EmbedModel = vec, model
	} else {
		// Üretilemezse eski vektörü BIRAKMAK yanlış olur (eski metni temsil eder).
		// Vektörsüz kaydedilir; anahtar kelime araması hâlâ çalışır.
		old.Embedding, old.EmbedModel = nil, ""
		if ctx.Err() == nil && !errors.Is(err, ErrNoEmbedder) {
			s.log.Warn("güncellenen anının embedding'i üretilemedi", "err", err)
		}
	}
	if in.Origin == OriginUser {
		old.Origin = OriginUser
	}
	if in.ConversationID != "" {
		old.ConversationID = in.ConversationID
	}
	old.Importance = clamp01(maxF(old.Importance, in.Importance))
	old.UpdatedAt = s.opt.Now().UTC()
	if err := s.repo.Update(ctx, old); err != nil {
		// HASH ÇAKIŞMASI: yeni içerik, kullanıcının BAŞKA bir kaydının özetiyle aynı.
		// Bu bir hata DEĞİL, bir BİRLEŞTİRME fırsatıdır: bilgi zaten kayıtlı, hedef
		// kayıt gereksiz bir kopya.
		//
		// Canlı hata: bu yol yakalanmadığı için ws katmanı kullanıcıya "bellek
		// güncellenemedi" (500) gösteriyordu. Doğrusu: mevcut kaydı yeniden onaylamak.
		if errors.Is(err, ErrDuplicate) {
			if ex, gerr := s.repo.GetByHash(ctx, old.UserID, hash); gerr == nil {
				// Kopya hedefi silinir; bilgi mevcut kayıtta zaten var, kaybolmaz.
				if derr := s.repo.Delete(context.WithoutCancel(ctx), old.UserID, old.ID); derr != nil {
					s.log.Warn("çakışan kopya anı silinemedi", "id", old.ID, "err", derr)
				}
				return s.reaffirm(ctx, ex, in)
			}
			s.log.Warn("güncelleme çakıştı, mevcut kayıt da bulunamadı", "id", old.ID, "user", old.UserID)
		}
		return nil, "", err
	}
	out := old.Clone()
	out.Embedding = nil
	if !contentChanged {
		// İçerik aynıydı: bu bir TEKRARDIR, güncelleme değil. Olay YAYINLANMAZ; arayüz
		// "güncellendi" diye yanlış bildirim alır.
		return &out, Unchanged, nil
	}
	s.publish(ctx, event.MemoryUpdated, old.UserID, map[string]any{
		"id": old.ID, "kind": old.Kind, "origin": old.Origin,
	})
	return &out, Updated, nil
}

// reaffirm zaten bilinen bir bilgiyi yeniden görme durumudur: yenilik (UpdatedAt) tazelenir.
// Kaynak "auto" iken kullanıcı aynısını elle yazdıysa anı kullanıcıya ait olur (korunur).
func (s *Service) reaffirm(ctx context.Context, ex *Memory, in AddInput) (*Memory, Outcome, error) {
	outcome := Unchanged
	ex.UpdatedAt = s.opt.Now().UTC()
	if in.Importance > ex.Importance {
		ex.Importance = clamp01(in.Importance)
	}
	if in.Origin == OriginUser && ex.Origin == OriginAuto {
		ex.Origin = OriginUser
		outcome = Updated
	}
	if err := s.repo.Update(ctx, ex); err != nil {
		return nil, "", err
	}
	if outcome == Updated {
		s.publish(ctx, event.MemoryUpdated, ex.UserID, map[string]any{"id": ex.ID, "kind": ex.Kind, "origin": ex.Origin})
	}
	out := ex.Clone()
	out.Embedding = nil
	return &out, outcome, nil
}

// supersede aynı konudaki eski anının yerine yenisini koyar. Kullanıcının elle yazdığı anıyı
// otomatik çıkarım EZMEZ.
func (s *Service) supersede(ctx context.Context, old *Memory, in AddInput, content string, tags []string,
	hash string, vec []float32, model string) (*Memory, Outcome, error) {
	if old.Origin == OriginUser && in.Origin == OriginAuto {
		out := old.Clone()
		out.Embedding = nil
		return &out, Unchanged, nil
	}
	old.Content, old.Hash, old.Embedding, old.EmbedModel = content, hash, vec, model
	old.Kind = in.Kind
	if len(tags) > 0 {
		old.Tags = tags
	}
	if in.Origin == OriginUser {
		old.Origin = OriginUser
	}
	if in.ConversationID != "" {
		old.ConversationID = in.ConversationID
	}
	old.Importance = clamp01(maxF(old.Importance, in.Importance))
	old.UpdatedAt = s.opt.Now().UTC()
	if err := s.repo.Update(ctx, old); err != nil {
		return nil, "", err
	}
	s.publish(ctx, event.MemoryUpdated, old.UserID, map[string]any{"id": old.ID, "kind": old.Kind, "origin": old.Origin})
	out := old.Clone()
	out.Embedding = nil
	return &out, Updated, nil
}

// nearest kullanıcının, verilen modelin vektörünü taşıyan en benzer anısını döndürür.
func (s *Service) nearest(ctx context.Context, userID string, vec []float32, model string) (*Memory, float64) {
	cands, err := s.repo.Searchable(ctx, userID)
	if err != nil {
		return nil, 0
	}
	var best *Memory
	bestScore := 0.0
	for i := range cands {
		c := &cands[i]
		if !c.HasEmbedding(model) || len(c.Embedding) != len(vec) {
			continue
		}
		if sc := cosine(vec, c.Embedding); sc > bestScore {
			best, bestScore = c, sc
		}
	}
	return best, bestScore
}

// Get başkasının anısı için de ErrNotFound döner (varlığı sızdırılmaz).
func (s *Service) Get(ctx context.Context, userID, id string) (*Memory, error) {
	m, err := s.repo.Get(ctx, userID, id)
	if err != nil {
		return nil, err
	}
	m.Embedding = nil
	return m, nil
}

// List en son güncellenen önce olacak biçimde listeler.
func (s *Service) List(ctx context.Context, f Filter, limit, offset int) ([]Memory, error) {
	if strings.TrimSpace(f.UserID) == "" {
		return nil, &ValidationError{Errors: []FieldError{{Field: "userId", Message: "kullanıcı gerekli"}}}
	}
	if f.Kind != "" && !f.Kind.Valid() {
		return nil, &ValidationError{Errors: []FieldError{{Field: "kind", Message: "kind long_term, semantic veya episodic olmalı"}}}
	}
	return s.repo.List(ctx, f, limit, offset)
}

// Count kullanıcının anı sayısıdır.
func (s *Service) Count(ctx context.Context, userID string) (int, error) {
	return s.repo.Count(ctx, userID)
}

// UpdateInput kısmi güncellemedir: nil alanlar değişmez.
type UpdateInput struct {
	UserID     string
	ID         string
	Content    *string
	Kind       *Kind
	Tags       *[]string
	Importance *float64
}

// Update anıyı değiştirir. Elle düzenlenen anı kullanıcıya ait olur (otomatik çıkarım ezmez).
// İçerik değişirse vektör yeniden üretilir; üretilemezse ESKİ vektör atılır (bayat vektör yanıltır).
func (s *Service) Update(ctx context.Context, in UpdateInput) (*Memory, error) {
	m, err := s.repo.Get(ctx, in.UserID, in.ID)
	if err != nil {
		return nil, err
	}
	var verrs []FieldError
	if in.Content != nil {
		c, ferr := validateContent(*in.Content)
		if ferr != nil {
			verrs = append(verrs, *ferr)
		} else if HashOf(c) != m.Hash {
			m.Content, m.Hash = c, HashOf(c)
			m.Embedding, m.EmbedModel = nil, ""
			if vec, model, eerr := s.embed(ctx, c); eerr == nil {
				m.Embedding, m.EmbedModel = vec, model
			} else if !errors.Is(eerr, ErrNoEmbedder) {
				if ctx.Err() != nil {
					return nil, ctx.Err()
				}
				s.log.Warn("düzenlenen anının embedding'i üretilemedi", "err", eerr)
			}
		} else {
			m.Content = c // yalnızca boşluk/biçim farkı
		}
	}
	if in.Kind != nil {
		if !in.Kind.Valid() {
			verrs = append(verrs, FieldError{Field: "kind", Message: "kind long_term, semantic veya episodic olmalı"})
		} else {
			m.Kind = *in.Kind
		}
	}
	if in.Tags != nil {
		t, ferr := cleanTags(*in.Tags)
		if ferr != nil {
			verrs = append(verrs, *ferr)
		} else {
			m.Tags = t
		}
	}
	if in.Importance != nil {
		if *in.Importance < 0 || *in.Importance > 1 {
			verrs = append(verrs, FieldError{Field: "importance", Message: "importance 0..1 aralığında olmalı"})
		} else {
			m.Importance = *in.Importance
		}
	}
	if len(verrs) > 0 {
		return nil, &ValidationError{Errors: verrs}
	}
	m.Origin = OriginUser
	m.UpdatedAt = s.opt.Now().UTC()
	if err := s.repo.Update(ctx, m); err != nil {
		return nil, err
	}
	s.publish(ctx, event.MemoryUpdated, m.UserID, map[string]any{"id": m.ID, "kind": m.Kind, "origin": m.Origin})
	m.Embedding = nil
	return m, nil
}

// Delete anıyı siler.
func (s *Service) Delete(ctx context.Context, userID, id string) error {
	if err := s.repo.Delete(ctx, userID, id); err != nil {
		return err
	}
	s.publish(ctx, event.MemoryDeleted, userID, map[string]any{"id": id})
	return nil
}

// DeleteAll kullanıcının tüm anılarını siler ("beni unut").
func (s *Service) DeleteAll(ctx context.Context, userID string) (int, error) {
	if strings.TrimSpace(userID) == "" {
		return 0, &ValidationError{Errors: []FieldError{{Field: "userId", Message: "kullanıcı gerekli"}}}
	}
	n, err := s.repo.DeleteAll(ctx, userID)
	if err != nil {
		return 0, err
	}
	if n > 0 {
		s.publish(ctx, event.MemoryDeleted, userID, map[string]any{"count": n})
	}
	return n, nil
}

// SearchOptions arama parametreleridir. UserID zorunludur.
type SearchOptions struct {
	UserID   string
	Query    string
	Limit    int     // <=0: DefaultSearchLimit; MaxSearchLimit ile sınırlı
	MinScore float64 // altındakiler elenir (0..1)
}

// Hit bir arama sonucudur. Skor 0..1; yüksek = daha ilgili.
type Hit struct {
	Memory
	Score float64 `json:"score"`
}

// Search kullanıcının anılarında sorguya en uygunları döndürür: vektör varsa %80 kosinüs +
// %20 anahtar kelime, yoksa yalnızca anahtar kelime (sağlayıcı çökse de arama çalışır).
func (s *Service) Search(ctx context.Context, opt SearchOptions) ([]Hit, error) {
	query := strings.TrimSpace(opt.Query)
	var verrs []FieldError
	if strings.TrimSpace(opt.UserID) == "" {
		verrs = append(verrs, FieldError{Field: "userId", Message: "kullanıcı gerekli"})
	}
	if query == "" {
		verrs = append(verrs, FieldError{Field: "query", Message: "arama metni gerekli"})
	}
	if opt.MinScore < 0 || opt.MinScore > 1 {
		verrs = append(verrs, FieldError{Field: "minScore", Message: "minScore 0..1 aralığında olmalı"})
	}
	if len(verrs) > 0 {
		return nil, &ValidationError{Errors: verrs}
	}
	limit := opt.Limit
	if limit <= 0 {
		limit = DefaultSearchLimit
	}
	if limit > MaxSearchLimit {
		limit = MaxSearchLimit
	}

	qvec, model, eerr := s.embed(ctx, query)
	if eerr != nil && !errors.Is(eerr, ErrNoEmbedder) {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		s.log.Warn("sorgu embedding'i üretilemedi; arama anahtar kelimeye düştü", "err", eerr)
	}

	cands, err := s.repo.Searchable(ctx, opt.UserID)
	if err != nil {
		return nil, err
	}
	qTokens := tokenize(query)
	hits := make([]Hit, 0, len(cands))
	for i := range cands {
		c := &cands[i]
		kw := keywordScore(qTokens, fold(c.Content+" "+strings.Join(c.Tags, " ")))
		score := kw
		if qvec != nil && c.HasEmbedding(model) && len(c.Embedding) == len(qvec) {
			score = semanticWeight*cosine(qvec, c.Embedding) + keywordWeight*kw
		}
		if score <= 0 || score < opt.MinScore {
			continue
		}
		h := Hit{Memory: c.Clone(), Score: score}
		h.Embedding = nil
		hits = append(hits, h)
	}
	sort.SliceStable(hits, func(i, j int) bool {
		a, b := hits[i], hits[j]
		switch {
		case a.Score != b.Score:
			return a.Score > b.Score
		case a.Importance != b.Importance:
			return a.Importance > b.Importance
		case !a.UpdatedAt.Equal(b.UpdatedAt):
			return a.UpdatedAt.After(b.UpdatedAt)
		}
		return a.ID < b.ID
	})
	if len(hits) > limit {
		hits = hits[:limit]
	}
	return hits, nil
}

// Recall ajanın her turda çağırdığı yoldur: Search + kullanım sayacı. HATA DÖNMEZ (getirme bir
// yakınlık önerisidir, üretimi engellemez); anı yoksa sorgu embedding'i hiç üretilmez.
func (s *Service) Recall(ctx context.Context, userID, query string, limit int, minScore float64) []Hit {
	if n, err := s.repo.Count(ctx, userID); err != nil || n == 0 {
		return nil
	}
	hits, err := s.Search(ctx, SearchOptions{UserID: userID, Query: query, Limit: limit, MinScore: minScore})
	if err != nil || len(hits) == 0 {
		return nil
	}
	ids := make([]string, 0, len(hits))
	for _, h := range hits {
		ids = append(ids, h.ID)
	}
	now := s.opt.Now().UTC()
	s.goBG(func(bg context.Context) {
		tctx, cancel := context.WithTimeout(bg, 5*time.Second)
		defer cancel()
		if err := s.repo.Touch(tctx, userID, ids, now); err != nil && bg.Err() == nil {
			s.log.Warn("anı kullanım sayacı güncellenemedi", "err", err)
		}
	})
	return hits
}

// ReindexReport yeniden indekslemenin özeti.
type ReindexReport struct {
	Total  int `json:"total"`  // kullanıcının anı sayısı
	Queued int `json:"queued"` // vektörü (yeniden) üretilecek anı sayısı
}

// Reindex vektörü olmayan ya da başka modelle üretilmiş anıların vektörünü ARKA PLANDA üretir
// (force: hepsi). Hemen döner; embedding modeli değişince çağrılır.
func (s *Service) Reindex(ctx context.Context, userID string, force bool) (ReindexReport, error) {
	emb, model := s.embedder()
	if emb == nil {
		return ReindexReport{}, ErrNoEmbedder
	}
	cands, err := s.repo.Searchable(ctx, userID)
	if err != nil {
		return ReindexReport{}, err
	}
	type job struct{ id, content string }
	var jobs []job
	for i := range cands {
		if force || !cands[i].HasEmbedding(model) {
			jobs = append(jobs, job{cands[i].ID, cands[i].Content})
		}
	}
	rep := ReindexReport{Total: len(cands), Queued: len(jobs)}
	if len(jobs) == 0 {
		return rep, nil
	}
	if !s.goBG(func(bg context.Context) {
		done := 0
		for _, j := range jobs {
			if bg.Err() != nil {
				return
			}
			vec, m, err := s.embed(bg, j.content)
			if err != nil {
				s.log.Warn("anı yeniden indekslenemedi", "id", j.id, "err", err)
				continue
			}
			sctx, cancel := context.WithTimeout(bg, 5*time.Second)
			if err := s.repo.SetEmbedding(sctx, userID, j.id, vec, m); err == nil {
				done++
			}
			cancel()
		}
		s.log.Info("anı yeniden indeksleme bitti", "kuyruk", len(jobs), "yazilan", done)
	}) {
		return ReindexReport{}, fmt.Errorf("memory: servis kapanıyor")
	}
	return rep, nil
}

func maxF(a, b float64) float64 {
	if a > b {
		return a
	}
	return b
}
