package claim

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

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

const (
	// SimilarThreshold iki kaydın "benzer" sayıldığı kosinüs eşiğidir (spec: ~0.9).
	SimilarThreshold = 0.9
	// maxCompare karar için modele gösterilen en çok mevcut kayıt sayısıdır.
	maxCompare = 5
	// WriteTimeout arka plan yazmasının üst sınırıdır (140 firmalık döküm: parça başına bir LLM çağrısı).
	WriteTimeout = 10 * time.Minute
	reindexBatch = 64
)

var (
	// ErrNoActor yazan kullanıcı belli değil. Kimlik gerçek oturumdan (JWT) gelmelidir; boş kimlikle
	// yazılan kayıt izlenemez (spec: kural 3).
	ErrNoActor = errors.New("claim: işlemi yapan kullanıcı belli değil")
	// ErrClosed servis kapatıldı.
	ErrClosed = errors.New("claim: servis kapandı")
)

// Actor işlemi yapan kullanıcıdır. Kimlik gerçek oturumdan gelmelidir: sohbet akışında Actor'ı
// Identity.Actor üretir (bkz. identity.go). Role boşsa uye sayılır; yetki artırmaz.
type Actor struct {
	ID, Name string
	Role     Role
}

// Config servis ayarlarıdır.
type Config struct {
	Model      string      // çıkarım ve çakışma kararı için sohbet modeli
	Opts       llm.Options // model parametreleri (sıcaklık servis tarafından sıfırlanır)
	EmbedModel string      // vektörlerin modeli; model değişince eski vektörler karşılaştırılamaz
}

// Conflict çelişkili işaretlenen iki kayıttır. Suçlama içermez: hangisinin geçerli olduğunu patron belirler.
type Conflict struct {
	NewID      string `json:"newId"`
	ExistingID string `json:"existingId"`
	Kind       string `json:"kind"`
	Key        string `json:"key,omitempty"`
}

// Similar ayrı tutulan ama benzer bulunan iki kayıttır ("benzer kayıt var").
type Similar struct {
	NewID      string  `json:"newId"`
	ExistingID string  `json:"existingId"`
	Score      float64 `json:"score"`
}

// WriteResult bir yazma işleminin özetidir.
type WriteResult struct {
	Added     int        `json:"added"`     // oluşturulan kayıt sayısı (güncelleme ve çelişkili olanlar dahil)
	Updated   int        `json:"updated"`   // Added'in kaç tanesi eski bir kaydın yerini aldı
	Unchanged int        `json:"unchanged"` // zaten kayıtlı olduğu için eklenmeyenler
	Conflicts []Conflict `json:"conflicts"`
	Similar   []Similar  `json:"similar"`
	// Incomplete listelik türde adı çıkarılamayan, bu yüzden yazılamayan aday sayısı ("eksik bilgi").
	Incomplete int `json:"incomplete"`
	Duplicates int `json:"duplicates"` // aynı mesajda tekrar edip elenenler
	// Undecided çakışma kararı alınamayan (LLM hata verdi / geçersiz yanıt) kayıt sayısı: kayıt
	// bilgi kaybolmasın diye aktif eklendi, ama çelişki denetlenemedi.
	Undecided int `json:"undecided"`
	// Unembedded vektörsüz yazılan kayıt sayısı. Bunlar için benzerlik denetimi yapılamadı;
	// Service.Reindex ile doldurulur.
	Unembedded int `json:"unembedded"`
	// Rejected doğrulamadan geçmeyip yazılamayan kayıt sayısı.
	Rejected int `json:"rejected"`
	// Denied kişisel veri türlerinde (personel, pdks) yazma yetkisi olmadığı için yazılmayan kayıt
	// sayısı: bu türleri yalnızca patron girer (Faz 8; kişi eşlemesi ve içe aktarma Faz 10).
	Denied int `json:"denied"`
	// FailedChunks > 0 ise sonuç EKSİKTİR: o parçalardaki bilgiler çıkarılamadı.
	FailedChunks int `json:"failedChunks"`
	Chunks       int `json:"chunks"`
}

// Summary spec'teki toplu giriş özetidir: "X kayıt eklendi, Y çelişki var, Z kayıtta eksik bilgi var".
// Dil nötrdür; ek durumlar yalnızca varsa eklenir.
func (r *WriteResult) Summary() string {
	parts := []string{
		fmt.Sprintf("%d kayıt eklendi", r.Added),
		fmt.Sprintf("%d çelişki var", len(r.Conflicts)),
		fmt.Sprintf("%d kayıtta eksik bilgi var", r.Incomplete),
	}
	if r.Updated > 0 {
		parts = append(parts, fmt.Sprintf("%d kayıt eskisinin güncel hâli", r.Updated))
	}
	if r.Unchanged > 0 {
		parts = append(parts, fmt.Sprintf("%d bilgi zaten kayıtlıydı", r.Unchanged))
	}
	if len(r.Similar) > 0 {
		parts = append(parts, fmt.Sprintf("%d kayıt mevcut bir kayda benziyor", len(r.Similar)))
	}
	if r.Undecided > 0 {
		parts = append(parts, fmt.Sprintf("%d kayıt için çelişki denetlenemedi", r.Undecided))
	}
	if r.Denied > 0 {
		parts = append(parts, fmt.Sprintf("%d kayıt yazılamadı: kişisel veri türlerini (personel, pdks) yalnızca patron girebilir", r.Denied))
	}
	if r.FailedChunks > 0 {
		parts = append(parts, fmt.Sprintf("%d/%d bölüm işlenemedi, bazı bilgiler eksik olabilir", r.FailedChunks, r.Chunks))
	}
	return strings.Join(parts, ", ")
}

// ConflictNote çelişki varsa suçlamasız tek cümledir (spec: "İki farklı bilgi var, hangisi geçerli?
// Karar patrondadır."). Kimin neyi yazdığı söylenmez: yazar yalnızca kayıtta izlenebilirlik içindir.
// Çelişki yoksa boştur.
func (r *WriteResult) ConflictNote() string {
	switch len(r.Conflicts) {
	case 0:
		return ""
	case 1:
		return "Aynı konuda iki farklı bilgi var; hangisinin geçerli olduğuna patron karar verecek."
	}
	return "Bazı konularda iki farklı bilgi var; hangisinin geçerli olduğuna patron karar verecek."
}

// Message kullanıcıya gösterilen metindir: Summary + (varsa) çelişki notu. Summary spec biçimini
// aynen korur; not ayrı cümle olarak eklenir.
func (r *WriteResult) Message() string {
	s := r.Summary()
	if n := r.ConflictNote(); n != "" {
		s += ". " + n
	}
	return s
}

// Service claim yazma servisidir: niyet çıkarma → toplu embedding → çakışma kararı → kayıt + denetim.
type Service struct {
	repo  Repository
	audit AuditRepository
	ext   *Extractor
	dec   Decider
	emb   llm.Embedder // nil: embedding kapalı
	cfg   Config
	log   *slog.Logger
	dir   Directory // hesap dizini: subjectId atanırken hesabın varlığını doğrular (SetDirectory)

	now   func() time.Time
	newID func(prefix string) string

	mu sync.Mutex // yazmaları sırayla uygular: "var mı bak, sonra ekle" yarışmasın

	tickMu   sync.Mutex // yönetim işlemlerinin (düzenle/sil) zaman damgası sırası; bkz. manage.go nextTick
	lastTick time.Time

	life   sync.Mutex
	closed bool
	bg     context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup
}

// NewService servisi kurar. emb nil olabilir (embedding kapalı: yalnızca listelik türlerde anahtar
// eşleşmesiyle çakışma denetlenir). log nil olabilir.
func NewService(repo Repository, audit AuditRepository, p llm.Provider, emb llm.Embedder, cfg Config, log *slog.Logger) *Service {
	if log == nil {
		log = slog.Default()
	}
	bg, cancel := context.WithCancel(context.Background())
	return &Service{
		repo: repo, audit: audit, emb: emb, cfg: cfg, log: log,
		ext:    NewExtractor(p, log),
		dec:    NewResolver(p, cfg.Model, cfg.Opts),
		now:    func() time.Time { return time.Now().UTC() },
		newID:  id.New,
		bg:     bg,
		cancel: cancel,
	}
}

// SetDirectory hesap dizinini bağlar. subjectId (kaydın ilgili kişisi) yalnızca dizinde var olan,
// devre dışı olmayan bir hesaba atanabilir; dizin yoksa subjectId atanamaz (güvenli varsayılan).
func (s *Service) SetDirectory(d Directory) { s.dir = d }

// Write mesajdaki bilgileri ayıklayıp kaydeder ve özetini döndürür (eşzamanlı).
// Hata durumunda bile o ana kadar yapılanları anlatan sonuç döner.
func (s *Service) Write(ctx context.Context, actor Actor, message string) (*WriteResult, error) {
	if strings.TrimSpace(actor.ID) == "" {
		return nil, ErrNoActor
	}
	ex, err := s.ext.Extract(ctx, s.cfg.Model, s.cfg.Opts, message)
	res := &WriteResult{Conflicts: []Conflict{}, Similar: []Similar{}}
	if ex != nil {
		res.Chunks, res.FailedChunks = ex.Chunks, ex.FailedChunks
		res.Duplicates, res.Incomplete = ex.Duplicates, len(ex.Incomplete)
	}
	if err != nil {
		return res, err
	}
	// Kişisel veri türlerini yalnızca patron yazar: yetkisiz adaylar embedding ve çakışma denetimine
	// HİÇ girmez (mevcut özel kayıtların varlığı "zaten kayıtlı"/çelişki özetiyle sızmasın).
	if !actor.isPatron() {
		allowed := ex.Candidates[:0:0]
		for _, c := range ex.Candidates {
			if IsPrivateKind(c.Kind) {
				res.Denied++
				continue
			}
			allowed = append(allowed, c)
		}
		ex.Candidates = allowed
	}
	if len(ex.Candidates) == 0 {
		return res, nil
	}

	vecs := s.embed(ctx, ex.Candidates)

	s.mu.Lock()
	defer s.mu.Unlock()
	cache := make(map[string][]Claim)
	for i, cand := range ex.Candidates {
		if err := ctx.Err(); err != nil {
			return res, err
		}
		var vec []float32
		if vecs != nil {
			vec = vecs[i]
		}
		if err := s.apply(ctx, actor, cand, vec, cache, res); err != nil {
			return res, err
		}
	}
	return res, nil
}

// WriteAsync yazmayı arka planda yapar: çağıran yanıtı beklemez (spec: yazma akışı, madde 4).
// done iş bitince (başarıyla ya da hatayla) çağrılır; nil olabilir.
func (s *Service) WriteAsync(actor Actor, message string, done func(*WriteResult, error)) error {
	if strings.TrimSpace(actor.ID) == "" {
		return ErrNoActor
	}
	s.life.Lock()
	if s.closed {
		s.life.Unlock()
		return ErrClosed
	}
	s.wg.Add(1)
	s.life.Unlock()

	go func() {
		defer s.wg.Done()
		ctx, cancel := context.WithTimeout(s.bg, WriteTimeout)
		defer cancel()
		res, err := s.Write(ctx, actor, message)
		if err != nil {
			s.log.Warn("claim yazma başarısız", "author", actor.ID, "err", err)
		}
		if done != nil {
			done(res, err)
		}
	}()
	return nil
}

// Close yeni işleri reddeder, çalışanları iptal eder ve bitmelerini bekler (Mongo kapanmadan önce çağrılmalı).
func (s *Service) Close() {
	s.life.Lock()
	s.closed = true
	s.cancel()
	s.life.Unlock()
	s.wg.Wait()
}

// embed adayları toplu vektörleştirir. Başarısızsa nil döner: kayıtlar vektörsüz yazılır (bilgi
// kaybolmasın), Reindex sonradan doldurur.
func (s *Service) embed(ctx context.Context, cands []Candidate) [][]float32 {
	if s.emb == nil {
		return nil
	}
	texts := make([]string, len(cands))
	for i, c := range cands {
		texts[i] = c.Text
	}
	vecs, err := llm.EmbedAll(ctx, s.emb, texts)
	if err != nil || len(vecs) != len(cands) {
		s.log.Warn("claim embedding üretilemedi: kayıtlar vektörsüz yazılacak", "err", err)
		return nil
	}
	return vecs
}

// peers aynı türdeki geçerli (aktif/çelişkili) kayıtlardır; bir Write boyunca bir kez yüklenir.
func (s *Service) peers(ctx context.Context, kind string, cache map[string][]Claim) ([]Claim, error) {
	if c, ok := cache[kind]; ok {
		return c, nil
	}
	cs, err := s.repo.Searchable(ctx, Filter{Kinds: []string{kind}, Statuses: []Status{StatusActive, StatusConflict}})
	if err != nil {
		return nil, fmt.Errorf("claim: mevcut kayıtlar okunamadı: %w", err)
	}
	cache[kind] = cs
	return cs, nil
}

func (s *Service) apply(ctx context.Context, actor Actor, cand Candidate, vec []float32, cache map[string][]Claim, res *WriteResult) error {
	now := s.now()
	nc := Claim{
		ID: s.newID("clm"), Text: cand.Text, Kind: cand.Kind, Key: cand.Key, Fields: map[string]string(cand.Fields),
		AuthorID: actor.ID, AuthorName: actor.Name,
		CreatedAt: now, UpdatedAt: now, Status: StatusActive,
	}
	if len(vec) > 0 {
		nc.Embedding, nc.EmbedModel = vec, s.cfg.EmbedModel
	}
	if err := nc.Validate(); err != nil {
		s.log.Warn("claim adayı doğrulanamadı, atlandı", "err", err)
		res.Rejected++
		return nil
	}

	peers, err := s.peers(ctx, cand.Kind, cache)
	if err != nil {
		return err
	}
	// Aynı bilgi zaten kayıtlıysa karar bile gerekmez (yeniden yapıştırılan döküm 140 LLM çağrısı yapmasın).
	norm := NormalizeKey(cand.Text)
	for _, p := range peers {
		if (!IsListKind(cand.Kind) || p.Key == cand.Key) && NormalizeKey(p.Text) == norm {
			res.Unchanged++
			return nil
		}
	}

	related := s.related(cand, vec, peers)
	if IsListKind(cand.Kind) && len(vec) > 0 {
		if best, score, ok := s.mostSimilarOtherKey(cand, vec, peers); ok && score >= SimilarThreshold {
			res.Similar = append(res.Similar, Similar{NewID: nc.ID, ExistingID: best.ID, Score: score})
		}
	}

	verdict := VerdictUnrelated
	var target *Claim
	if len(related) > 0 {
		v, tid, derr := s.dec.Decide(ctx, cand, related)
		if derr == nil && (v == VerdictUpdate || v == VerdictConflict) {
			for i := range related {
				if related[i].ID == tid {
					target = &related[i]
				}
			}
			if target == nil {
				derr = fmt.Errorf("karar geçersiz hedef gösterdi: %q", tid)
			}
		}
		if derr != nil {
			if ctx.Err() != nil {
				return ctx.Err()
			}
			s.log.Warn("claim çakışma kararı alınamadı: kayıt aktif eklenecek", "kind", cand.Kind, "err", derr)
			res.Undecided++
			target = nil
		} else {
			verdict = v
		}
	}

	switch verdict {
	case VerdictSame:
		res.Unchanged++
		return nil
	case VerdictUpdate:
		nc.Supersedes = target.ID
	case VerdictConflict:
		nc.Status = StatusConflict
		nc.ConflictsWith = target.ID
	}

	if err := s.repo.Create(ctx, &nc); err != nil {
		return fmt.Errorf("claim: kayıt yazılamadı: %w", err)
	}
	if err := s.appendAudit(ctx, actor, nc.ID, ActionCreate, nil, &nc, now); err != nil {
		return err
	}
	res.Added++
	if len(vec) == 0 {
		res.Unembedded++
	}

	switch verdict {
	case VerdictUpdate:
		if err := s.setStatus(ctx, actor, target, StatusUpdated, now); err != nil {
			return err
		}
		res.Updated++
		dropFromCache(cache, cand.Kind, target.ID)
	case VerdictConflict:
		if target.Status != StatusConflict {
			if err := s.setStatus(ctx, actor, target, StatusConflict, now); err != nil {
				return err
			}
			markInCache(cache, cand.Kind, target.ID, StatusConflict)
		}
		res.Conflicts = append(res.Conflicts, Conflict{NewID: nc.ID, ExistingID: target.ID, Kind: nc.Kind, Key: nc.Key})
	}
	cache[cand.Kind] = append(cache[cand.Kind], nc)
	return nil
}

// related karar için modele gösterilecek mevcut kayıtlardır. Listelik türde YALNIZCA aynı anahtarlı
// kayıtlar (birebir eşleşme; spec madde 3); diğerlerinde benzerliği eşiği aşanlar.
func (s *Service) related(cand Candidate, vec []float32, peers []Claim) []Claim {
	var out []Claim
	if IsListKind(cand.Kind) {
		for _, p := range peers {
			if p.Key == cand.Key {
				out = append(out, p)
			}
		}
		if len(out) > maxCompare {
			out = out[len(out)-maxCompare:] // en yeniler
		}
		return out
	}
	if len(vec) == 0 {
		return nil
	}
	type scored struct {
		c Claim
		s float64
	}
	var sc []scored
	for _, p := range peers {
		if !p.HasEmbedding(s.cfg.EmbedModel) {
			continue
		}
		if v := cosine(vec, p.Embedding); v >= SimilarThreshold {
			sc = append(sc, scored{p, v})
		}
	}
	sort.SliceStable(sc, func(i, j int) bool { return sc[i].s > sc[j].s })
	for i := 0; i < len(sc) && i < maxCompare; i++ {
		out = append(out, sc[i].c)
	}
	return out
}

// mostSimilarOtherKey listelik türde FARKLI anahtarlı en benzer kaydı bulur. Birleştirilmez, yalnızca işaretlenir.
func (s *Service) mostSimilarOtherKey(cand Candidate, vec []float32, peers []Claim) (Claim, float64, bool) {
	var best Claim
	bestScore, found := 0.0, false
	for _, p := range peers {
		if p.Key == cand.Key || !p.HasEmbedding(s.cfg.EmbedModel) {
			continue
		}
		if v := cosine(vec, p.Embedding); !found || v > bestScore {
			best, bestScore, found = p, v, true
		}
	}
	return best, bestScore, found
}

func (s *Service) setStatus(ctx context.Context, actor Actor, c *Claim, st Status, at time.Time) error {
	if err := s.repo.SetStatus(ctx, c.ID, st, at); err != nil {
		return fmt.Errorf("claim: durum yazılamadı: %w", err)
	}
	after := *c
	after.Status, after.UpdatedAt = st, at
	return s.appendAudit(ctx, actor, c.ID, ActionStatus, c, &after, at)
}

func (s *Service) appendAudit(ctx context.Context, actor Actor, claimID string, act Action, before, after *Claim, at time.Time) error {
	e := AuditEntry{ID: s.newID("aud"), ClaimID: claimID, Action: act, By: actor.ID, ByName: actor.Name, At: at}
	if before != nil {
		e.Before = before.Snapshot()
	}
	if after != nil {
		e.After = after.Snapshot()
	}
	if err := s.audit.Append(ctx, &e); err != nil {
		return fmt.Errorf("claim: denetim kaydı yazılamadı: %w", err)
	}
	return nil
}

func dropFromCache(cache map[string][]Claim, kind, claimID string) {
	cs := cache[kind]
	out := cs[:0:0]
	for _, c := range cs {
		if c.ID != claimID {
			out = append(out, c)
		}
	}
	cache[kind] = out
}

func markInCache(cache map[string][]Claim, kind, claimID string, st Status) {
	cs := cache[kind]
	for i := range cs {
		if cs[i].ID == claimID {
			cs[i].Status = st
		}
	}
}

// ReindexReport Reindex sonucudur.
type ReindexReport struct{ Total, Embedded int }

// Reindex embedding'i eksik ya da başka modelle üretilmiş kayıtların vektörünü yeniden üretir
// (embedding kapalıyken/başarısızken yazılanlar için). Yalnızca vektörü yazar; kaydın diğer alanlarına dokunmaz.
func (s *Service) Reindex(ctx context.Context) (ReindexReport, error) {
	var rep ReindexReport
	if s.emb == nil {
		return rep, llm.ErrNoEmbedModel
	}
	all, err := s.repo.Searchable(ctx, Filter{})
	if err != nil {
		return rep, err
	}
	rep.Total = len(all)
	var todo []Claim
	for _, c := range all {
		if !c.HasEmbedding(s.cfg.EmbedModel) {
			todo = append(todo, c)
		}
	}
	for start := 0; start < len(todo); start += reindexBatch {
		end := min(start+reindexBatch, len(todo))
		texts := make([]string, 0, end-start)
		for _, c := range todo[start:end] {
			texts = append(texts, c.Text)
		}
		vecs, err := llm.EmbedAll(ctx, s.emb, texts)
		if err != nil {
			return rep, err
		}
		for i, c := range todo[start:end] {
			if err := s.repo.SetEmbedding(ctx, c.ID, vecs[i], s.cfg.EmbedModel); err != nil {
				return rep, err
			}
			rep.Embedded++
		}
	}
	return rep, nil
}

// cosine iki vektörün kosinüs benzerliğidir; boyut uyuşmazsa ya da sıfır vektörse 0.
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 {
		dot += float64(a[i]) * float64(b[i])
		na += float64(a[i]) * float64(a[i])
		nb += float64(b[i]) * float64(b[i])
	}
	if na == 0 || nb == 0 {
		return 0
	}
	return dot / (math.Sqrt(na) * math.Sqrt(nb))
}
