package skill

import (
	"context"
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"sync"
	"time"
	"unicode"

	"fiko-ai/internal/llm"
)

// SearchMode aramanın nasıl yapılacağını belirler (PROJE.MD: semantic | keyword | hybrid).
type SearchMode string

const (
	// ModeHybrid varsayılan: embedding varsa semantik skor + anahtar kelime skoru, yoksa yalnızca anahtar kelime.
	// Embedding sağlayıcısı çökse bile arama çalışmaya devam eder.
	ModeHybrid SearchMode = "hybrid"
	// ModeSemantic yalnızca vektör benzerliği; embedding üretilemezse hata döner (sessizce düşmez).
	ModeSemantic SearchMode = "semantic"
	// ModeKeyword yalnızca anahtar kelime eşleşmesi; embedding gerektirmez.
	ModeKeyword SearchMode = "keyword"
)

const (
	DefaultSearchLimit = 5
	MaxSearchLimit     = 50

	// embedTimeout tek bir embedding çağrısının üst sınırı: kayıt/arama yavaş sağlayıcıya takılı kalmasın.
	embedTimeout = 20 * time.Second

	// Hibrit skor ağırlıkları (toplam 1). Semantik baskın; anahtar kelime eşit skorları ayırır.
	semanticWeight = 0.8
	keywordWeight  = 0.2
)

// ErrNoEmbedder embedding sağlayıcısı yapılandırılmamışken embedding gerektiren işlem istenirse döner.
var ErrNoEmbedder = errors.New("skill: embedding sağlayıcısı yapılandırılmamış (OLLAMA_EMBED_MODEL ayarla)")

// SearchOptions arama parametreleridir.
type SearchOptions struct {
	Query    string
	Mode     SearchMode // boş = hybrid
	Limit    int        // <=0: DefaultSearchLimit; MaxSearchLimit ile sınırlanır
	MinScore float64    // bu skorun altındakiler elenir (0..1)
}

// SearchResult bir eşleşmedir. Skor 0..1 aralığındadır; yüksek = daha ilgili.
type SearchResult struct {
	Skill Skill   `json:"skill"`
	Score float64 `json:"score"`
}

// ReindexReport Reindex sonucudur.
type ReindexReport struct {
	Total    int `json:"total"`    // taranan skill sayısı
	Embedded int `json:"embedded"` // embedding'i (yeniden) üretilen
	Skipped  int `json:"skipped"`  // zaten güncel olan ya da tarama sırasında değiştirilen
	Failed   int `json:"failed"`   // embedding üretilemeyen
}

// SetEmbedder embedding sağlayıcısını bağlar; nil verilirse embedding devre dışı kalır.
// Kurulum sırasında (eşzamanlı kullanım başlamadan) çağrılmalıdır.
//
// Sağlayıcı DEĞİŞTİĞİNDE önbellek temizlenir: farklı modellerin vektörleri
// karşılaştırılamaz (eski vektörle yeni modelin çıktısı aynı uzayda değildir).
func (s *Service) SetEmbedder(e llm.Embedder) {
	s.embedder = e
	if s.cache != nil {
		s.cache.clear()
	}
}

// ---- embedding önbelleği (performans) ----
//
// Neden? Her kullanıcı mesajı bir embedding İSTEĞİ doğurur. Ollama'da bu, embedding
// modelinin (bizde 4.7 GB) yüklenmesi/VRAM'e yerleşmesi anlamına gelir; arka arkaya
// gelen mesajlarda saniyeler sürebilir. Aynı/aynı-benzeri sorular tekrar tekrar
// vektörleştirilmesin diye sonuçlar kısa süre tutulur.
//
// Anahtar: normalleştirilmiş metin. Değer: vektör.
// Politika: boyut sınırlı + süreli (TTL). Embedding MODELİ değişirse vektörler
// karşılaştırılamaz; bu yüzden SetEmbedder çağrısı önbelleği temizler (bkz. yukarı).
const (
	cacheTTL       = 10 * time.Minute
	cacheMaxItems  = 512
	embedCacheKeyF = "\x00" // anahtarları ayırmak için ayrılmaz karakter
)

// embedCache, metin -> vektör eşlemesidir; TTL dolan veya taşan girdiler temizlenir.
type embedCache struct {
	mu    sync.Mutex
	ttl   time.Duration
	max   int
	items map[string]cacheItem
	now   func() time.Time
}

type cacheItem struct {
	vec []float32
	at  time.Time
}

// get, önbellekte vektörü döndürür; yoksa ok=false verir.
func (c *embedCache) get(key string) ([]float32, bool) {
	if c == nil {
		return nil, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	it, ok := c.items[key]
	if !ok {
		return nil, false
	}
	if c.now().Sub(it.at) > c.ttl {
		delete(c.items, key) // eskimiş: model çıktısı geçerliliğini yitirmiş olabilir
		return nil, false
	}
	return it.vec, true
}

// put vektörü ekler; kapasite aşılırsa en eskiyi atar.
func (c *embedCache) put(key string, vec []float32) {
	if c == nil || vec == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.items == nil {
		c.items = make(map[string]cacheItem, 64)
	}
	if len(c.items) >= c.max {
		// Basit yöntem: en eski girdiyi at (O(n) ama n=512 ve seyrek).
		var oldestKey string
		var oldestAt time.Time
		for k, v := range c.items {
			if oldestKey == "" || v.at.Before(oldestAt) {
				oldestKey, oldestAt = k, v.at
			}
		}
		delete(c.items, oldestKey)
	}
	c.items[key] = cacheItem{vec: vec, at: c.now()}
}

// clear tüm önbelleği siler (embedding sağlayıcısı değiştiğinde çağrılır).
func (c *embedCache) clear() {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.items = nil
}

// newEmbedCache, testlerde devre dışı bırakılabilsin diye now enjekte eder.
func newEmbedCache(now func() time.Time) *embedCache {
	if now == nil {
		now = func() time.Time { return time.Now() }
	}
	return &embedCache{ttl: cacheTTL, max: cacheMaxItems, now: now}
}

// cacheKey, sorgu vektörü önbelleğinin anahtarıdır. Arama MODU anahtara girer:
// aynı metin semantic ile aranınca farklı sonuç beklentisi vardır ve bu iki vektör
// ayrı hesaplanabilir. Ayraç, "ab"+"c" ile "a"+"bc" çakışmasını engeller.
func cacheKey(mode SearchMode, query string) string {
	return embedCacheKeyF + string(mode) + "\x00" + query
}

// EmbeddingText skill'in vektöre çevrilen metnidir: ad, açıklama, etiketler ve örnek cümleler.
// Örnekler arama kalitesini en çok artıran kısımdır ("30 mm geriye plane koy" ↔ create_plane).
func EmbeddingText(sk *Skill) string {
	var b strings.Builder
	b.WriteString(strings.ReplaceAll(sk.Name, "_", " "))
	if sk.Description != "" {
		b.WriteString(". ")
		b.WriteString(sk.Description)
	}
	if len(sk.Tags) > 0 {
		b.WriteString(". Etiketler: ")
		b.WriteString(strings.Join(sk.Tags, ", "))
	}
	for _, ex := range sk.Examples {
		if ex.Input != "" {
			b.WriteString(". Örnek: ")
			b.WriteString(ex.Input)
		}
	}
	return b.String()
}

// embedSkill skill için vektör üretir; sonuç önbelleğe alınır.
// Hata kayıt işlemini DÜŞÜRMEZ: nil döner ve uyarı loglanır
// (embedding sonradan Reindex ile doldurulabilir).
//
// force=true önbelleği ATLAR: Reindex(force) "embedding modeli değişti, hepsini
// yeniden üret" demektir; önbellekten dönmek bu işi sessizce yapmazdı.
func (s *Service) embedSkill(ctx context.Context, sk *Skill) []float32 {
	return s.embedSkillOpt(ctx, sk, false)
}

func (s *Service) embedSkillOpt(ctx context.Context, sk *Skill, force bool) []float32 {
	if s.embedder == nil {
		return nil
	}
	text := EmbeddingText(sk)
	if !force {
		if vec, ok := s.cache.get(text); ok {
			return vec
		}
	}
	ectx, cancel := context.WithTimeout(ctx, embedTimeout)
	defer cancel()
	vec, err := s.embedder.Embed(ectx, text)
	if err != nil {
		s.log.Warn("skill embedding üretilemedi; kayıt embedding'siz devam ediyor (Reindex ile doldurulabilir)",
			"skill", sk.Name, "err", err)
		return nil
	}
	s.cache.put(text, vec)
	return vec
}

// Search aktif skill'ler arasında sorguya en uygun olanları döndürür (skora göre azalan).
// Yalnızca status=active skill'ler aranır: taslak ve kapalı skill'ler LLM'e hiç gitmemelidir.
func (s *Service) Search(ctx context.Context, opt SearchOptions) ([]SearchResult, error) {
	query := strings.TrimSpace(opt.Query)
	var verrs []FieldError
	if query == "" {
		verrs = append(verrs, FieldError{Field: "query", Message: "arama metni gerekli"})
	}
	mode := opt.Mode
	switch mode {
	case "":
		mode = ModeHybrid
	case ModeHybrid, ModeSemantic, ModeKeyword:
	default:
		verrs = append(verrs, FieldError{Field: "mode", Message: "mode semantic, keyword veya hybrid olmalı"})
	}
	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
	}

	// Sorgu vektörü: keyword modunda gerekmez; hybrid'de başarısızlık anahtar kelimeye düşürür.
	var qvec []float32
	if mode != ModeKeyword {
		switch {
		case s.embedder == nil && mode == ModeSemantic:
			return nil, ErrNoEmbedder
		case s.embedder != nil:
			// Önce önbellek: aynı mesajın aranması (skill retrieval HER mesajda
			// çalışır) tekrar Ollama'ya gitmesin.
			if v, ok := s.cache.get(cacheKey(mode, query)); ok {
				qvec = v
				break
			}
			ectx, cancel := context.WithTimeout(ctx, embedTimeout)
			v, err := s.embedder.Embed(ectx, query)
			cancel()
			if err != nil {
				if mode == ModeSemantic {
					return nil, fmt.Errorf("skill: sorgu embedding'i üretilemedi: %w", err)
				}
				s.log.Warn("sorgu embedding'i üretilemedi; arama anahtar kelimeye düştü", "err", err)
			} else {
				qvec = v
				s.cache.put(cacheKey(mode, query), v)
			}
		}
	}

	skills, err := s.repo.List(ctx, ListFilter{Status: StatusActive}, 0, 0)
	if err != nil {
		return nil, err
	}

	qTokens := tokenize(query)
	results := make([]SearchResult, 0, len(skills))
	for i := range skills {
		sk := &skills[i]
		kw := keywordScore(qTokens, sk)
		var score float64
		switch {
		case mode == ModeKeyword:
			score = kw
		case qvec != nil && len(sk.Embedding) == len(qvec):
			sem := cosine(qvec, sk.Embedding)
			if mode == ModeSemantic {
				score = sem
			} else {
				score = semanticWeight*sem + keywordWeight*kw
			}
		case mode == ModeHybrid:
			// Embedding'i olmayan (ya da boyutu uyuşmayan) skill hibritte yalnızca anahtar kelimeyle bulunur.
			score = kw
		default:
			continue // semantic modda embedding'siz skill aranamaz
		}
		if score <= 0 || score < opt.MinScore {
			continue
		}
		results = append(results, SearchResult{Skill: sk.Clone(), Score: score})
	}

	sort.SliceStable(results, func(i, j int) bool {
		if results[i].Score != results[j].Score {
			return results[i].Score > results[j].Score
		}
		return results[i].Skill.Name < results[j].Skill.Name
	})
	if len(results) > limit {
		results = results[:limit]
	}
	return results, nil
}

// Reindex embedding'i eksik skill'leri doldurur; force=true ise hepsini yeniden üretir
// (embedding modeli değiştiğinde gerekir: eski ve yeni vektörler karşılaştırılamaz).
// Tarama sırasında kullanıcı skill'i düzenlerse o skill atlanır (kullanıcının değişikliği ezilmez).
func (s *Service) Reindex(ctx context.Context, force bool) (ReindexReport, error) {
	var rep ReindexReport
	if s.embedder == nil {
		return rep, ErrNoEmbedder
	}
	skills, err := s.repo.List(ctx, ListFilter{}, 0, 0)
	if err != nil {
		return rep, err
	}
	rep.Total = len(skills)
	for i := range skills {
		if err := ctx.Err(); err != nil {
			return rep, err
		}
		sk := skills[i]
		if !force && len(sk.Embedding) > 0 {
			rep.Skipped++
			continue
		}
		vec := s.embedSkillOpt(ctx, &sk, force)
		if vec == nil {
			rep.Failed++
			continue
		}
		cur, err := s.repo.Get(ctx, sk.ID)
		if err != nil || !cur.UpdatedAt.Equal(sk.UpdatedAt) {
			rep.Skipped++ // silindi ya da araya düzenleme girdi
			continue
		}
		cur.Embedding = vec // UpdatedAt değişmez: embedding yenilemek kullanıcı düzenlemesi değildir
		if err := s.repo.Update(ctx, cur); err != nil {
			s.log.Warn("embedding kaydedilemedi", "skill", sk.Name, "err", err)
			rep.Failed++
			continue
		}
		rep.Embedded++
	}
	return rep, nil
}

// cosine iki vektörün kosinüs benzerliğidir; negatif değerler 0'a çekilir (ilgisiz = 0).
// Boyutlar farklıysa ya da bir vektör sıfırsa 0 döner.
func cosine(a, b []float32) float64 {
	if len(a) == 0 || len(a) != len(b) {
		return 0
	}
	var dot, na, nb float64
	for i := range a {
		x, y := float64(a[i]), float64(b[i])
		dot += x * y
		na += x * x
		nb += y * y
	}
	if na == 0 || nb == 0 {
		return 0
	}
	c := dot / (math.Sqrt(na) * math.Sqrt(nb))
	switch {
	case c < 0:
		return 0
	case c > 1:
		return 1
	}
	return c
}

// tokenize metni küçük harfli, en az 2 karakterlik sözcüklere böler (harf/rakam dışı ayırıcıdır).
func tokenize(s string) []string {
	fields := strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
	})
	out := fields[:0]
	seen := make(map[string]struct{}, len(fields))
	for _, f := range fields {
		if len([]rune(f)) < 2 {
			continue
		}
		if _, dup := seen[f]; dup {
			continue
		}
		seen[f] = struct{}{}
		out = append(out, f)
	}
	return out
}

// keywordScore sorgu sözcüklerinin ne kadarının skill metninde geçtiğini döndürür (0..1).
// Alt dize eşleşmesi tek yönde ek toleransı sağlar: "geri" sorgusu metindeki "geriye"yi bulur (tersi bulunmaz).
func keywordScore(qTokens []string, sk *Skill) float64 {
	if len(qTokens) == 0 {
		return 0
	}
	text := strings.ToLower(EmbeddingText(sk))
	hit := 0
	for _, t := range qTokens {
		if strings.Contains(text, t) {
			hit++
		}
	}
	return float64(hit) / float64(len(qTokens))
}
