package knowledge

import (
	"context"
	"errors"
)

const (
	// vectorMinPool/vectorMaxPool depo tarafından istenen aday sayısının sınırlarıdır:
	// hibritte anahtar kelime skoru yeniden sıralamaya katıldığı için limit'ten fazla aday gerekir.
	vectorMinPool = 50
	vectorMaxPool = 200
	// unembeddedSupplement vektörsüz (işlenmemiş/bayat) parçalardan anahtar kelimeyle taranacak üst sınır.
	unembeddedSupplement = 200
)

// scoreByVector adayları deponun vektör aramasından (MongoDB $vectorSearch) alıp skorlar.
// ok=false: vektör yolu kullanılamadı (depo desteklemiyor, mongot yok, hata); çağıran Go tarafı
// taramaya (scoreByScan) düşer. Düşüş HATA DEĞİLDİR: arama her koşulda çalışmaya devam eder.
//
// Skor, tarama yoluyla AYNI formüldür (semantic: kosinüs; hybrid: %80 kosinüs + %20 anahtar kelime),
// bu yüzden MinScore eşiği ve ajanın knowledgeMinScore kalibrasyonu iki yolda aynı davranır.
//
// Bilinen fark (hibrit): anahtar kelime skoru yalnızca vektör havuzundaki adaylara + vektörsüz
// parçalara uygulanır. Vektör havuzunun DIŞINDA kalan, yalnızca sözcük olarak eşleşen parça
// bulunmaz; tam tarama yolu bunu bulurdu. Havuz limit×4 (en az 50) olduğundan pratikte nadirdir.
func (s *Service) scoreByVector(ctx context.Context, f ChunkFilter, mode SearchMode, qvec []float32,
	model string, qTokens []string, minScore float64, limit int) ([]Hit, bool) {
	vs, ok := s.repo.(VectorSearcher)
	if !ok || len(qvec) == 0 {
		return nil, false
	}

	pool := limit * rerankPool
	if pool < vectorMinPool {
		pool = vectorMinPool
	}
	if pool > vectorMaxPool {
		pool = vectorMaxPool
	}
	vhits, err := vs.VectorSearch(ctx, VectorQuery{Filter: f, Vector: qvec, Model: model, Limit: pool})
	if err != nil {
		if ctx.Err() != nil {
			return nil, false
		}
		if errors.Is(err, ErrVectorUnavailable) {
			s.log.Debug("vektör arama hazır değil; Go tarafı skorlamaya düşüldü", "err", err)
		} else {
			s.log.Warn("vektör arama başarısız; Go tarafı skorlamaya düşüldü", "err", err)
		}
		return nil, false
	}

	scored := make([]Hit, 0, len(vhits))
	seen := make(map[string]struct{}, len(vhits))
	for i := range vhits {
		c := &vhits[i].Chunk
		seen[c.ID] = struct{}{}
		sem := vhits[i].Similarity
		score := sem
		if mode == ModeHybrid {
			kw := keywordScore(qTokens, fold(c.Title+" "+c.Text))
			score = semanticWeight*sem + keywordWeight*kw
		}
		if score <= 0 || score < minScore {
			continue
		}
		scored = append(scored, hitOf(c, score))
	}

	// Hibritte henüz işlenmemiş parçalar da yalnızca anahtar kelimeyle bulunabilmeli
	// (belge "indexing" iken ya da sağlayıcı hatasıyla kısmi kaldığında).
	if mode == ModeHybrid {
		extra, err := vs.UnembeddedChunks(ctx, f, model, unembeddedSupplement)
		if err != nil {
			if ctx.Err() != nil {
				return nil, false
			}
			s.log.Warn("vektörsüz parçalar okunamadı; yalnızca vektör sonuçları kullanılıyor", "err", err)
		}
		for i := range extra {
			c := &extra[i]
			if _, dup := seen[c.ID]; dup {
				continue
			}
			kw := keywordScore(qTokens, fold(c.Title+" "+c.Text))
			if kw <= 0 || kw < minScore {
				continue
			}
			scored = append(scored, hitOf(c, kw))
		}
	}
	return scored, true
}

// hitOf parçadan arama sonucu üretir (etiketler kopyalanır; depo verisi paylaşılmaz).
func hitOf(c *Chunk, score float64) Hit {
	return Hit{
		ChunkID: c.ID, DocumentID: c.DocumentID, Index: c.Index, Title: c.Title,
		Source: c.Source, Tags: cloneSlice(c.Tags), Text: c.Text, Score: score,
	}
}
