package claim

// Kayıt yönetimi (Faz 5): görüntüleme, düzenleme, silme ve değişiklik geçmişi.
//
// YETKİ (spec kural 2): bir kaydı YAZARI ya da PATRON değiştirebilir/silebilir; başka bir üye
// değiştiremez (ErrForbidden). Kayıtlar ekip ortağıdır: görüntülemek ve listelemek her
// doğrulanmış kullanıcıya açıktır (tür bazlı görünürlük Faz 8'dedir ve BURAYA filtre olarak eklenecek).
//
// DENETİM: her düzenleme ve silme `audit`'e (kim, ne zaman, önceki değer) yazılır. Denetim kaydı
// değişiklikten ÖNCE yazılır: silinen/değişen değer hiçbir koşulda kaybolmasın. Bedeli: değişikliğin
// kendisi (depo hatası) başarısız olursa denetimde "yapılmış" görünen ama yapılmamış bir kayıt kalır;
// kayıt hâlâ durduğundan bu fark görülebilir ve zararsızdır, tersi (denetimsiz silme) ise telafisizdir.
//
// EŞZAMANLILIK: Write uzun sürebildiği için (çakışma kararı LLM çağırır) bu işlemler yazma kilidini
// BEKLEMEZ. Aynı kayda aynı anda Write'ın durum değişikliği gelirse son yazan kazanır; pratikte
// pencere çok dardır ve yönetim işlemleri tek kayıtlıktır.

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"time"

	"fiko-ai/internal/claim/kinds"
)

var (
	// ErrForbidden kullanıcı kaydın yazarı ya da patron değil.
	ErrForbidden = errors.New("claim: bu kaydı değiştirme yetkiniz yok")
	// ErrKeyTaken listelik türde (firma, kisi, urun) aynı ada sahip başka bir geçerli kayıt var:
	// iki kaydın aynı anahtarı paylaşması çözülmemiş çelişki yaratırdı.
	ErrKeyTaken = errors.New("claim: aynı türde bu ada sahip başka bir kayıt var")
)

const (
	defaultManageLimit = 50
	maxManageLimit     = 200
)

// CanModify aktör kaydı düzenleyip silebilir mi? Yazar kendi kaydını, patron her kaydı yönetir.
func CanModify(actor Actor, c *Claim) bool {
	if strings.TrimSpace(actor.ID) == "" || c == nil {
		return false
	}
	return actor.Role == RolePatron || c.AuthorID == actor.ID
}

// EditRequest bir kaydın değiştirilebilir alanlarıdır. Tür (kind), yazar ve tarihçe değiştirilemez.
type EditRequest struct {
	// Text yeni metin; boş olamaz.
	Text string
	// Key nil ise anahtar olduğu gibi kalır. Listelik türlerde boş olamaz; değişirse aynı türde
	// başka bir geçerli kayıtla çakışmamalıdır.
	Key *string
	// Fields nil değilse tipli alanlar TÜMÜYLE bu değerlerle değişir (şemaya göre normalleştirilir,
	// zorunlu alan eksikse reddedilir). Metin ayrıca düzenlenir; ikisini uyumlu tutmak çağıranındır.
	Fields map[string]string
	// SubjectID nil değilse özel türlerde (personel, pdks) kaydın ilgili kişisini (hesap kimliği)
	// belirler; boş dizge bağı kaldırır (kaydı yalnızca patron görür). YALNIZCA PATRON (ErrPatronOnly).
	// Metin boş bırakılırsa mevcut metin korunur.
	SubjectID *string
}

// nextTick yönetim işlemi için zaman damgası üretir: milisaniyeye kırpılır ve bir öncekinden KESİN
// büyüktür. Neden? Saat çözünürlüğü kaba olabilir (Windows) ve MongoDB tarihi milisaniyeye yuvarlar;
// art arda iki işlem aynı damgayı alırsa geçmiş sırası (yeni önce) rastgele kimliğe kalırdı.
// Bu, yalnızca düzenle/sil içindir; toplu yazmanın damgaları gerçek saati izlemeye devam eder.
func (s *Service) nextTick() time.Time {
	s.tickMu.Lock()
	defer s.tickMu.Unlock()
	t := s.now().Truncate(time.Millisecond)
	if !t.After(s.lastTick) {
		t = s.lastTick.Add(time.Millisecond)
	}
	s.lastTick = t
	return t
}

func clampLimit(limit int) int {
	if limit <= 0 {
		return defaultManageLimit
	}
	if limit > maxManageLimit {
		return maxManageLimit
	}
	return limit
}

// Get tek bir kaydı (vektörsüz) döner. Yoksa ErrNotFound.
func (s *Service) Get(ctx context.Context, actor Actor, id string) (*Claim, error) {
	if strings.TrimSpace(actor.ID) == "" {
		return nil, ErrNoActor
	}
	c, err := s.repo.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	if !actor.CanSee(c) {
		return nil, ErrNotFound // görme yetkisi olmayana kaydın varlığı bile söylenmez
	}
	snap := c.Snapshot()
	if err := logViews(ctx, s.audit, s.newID, s.nextTick(), actor, []Claim{*snap}); err != nil {
		return nil, fmt.Errorf("claim: erişim kaydı yazılamadı: %w", err)
	}
	return snap, nil
}

// List süzgece uyan kayıtları ve süzgece uyan TOPLAM sayıyı döner (sayfalama için). Sıra Repository.List ile
// aynıdır (anahtar, oluşturma zamanı, kimlik). limit<=0 → 50, en fazla 200.
func (s *Service) List(ctx context.Context, actor Actor, f Filter, limit, offset int) ([]Claim, int, error) {
	if strings.TrimSpace(actor.ID) == "" {
		return nil, 0, ErrNoActor
	}
	if offset < 0 {
		offset = 0
	}
	f = actor.Scope(f) // yetki süzgeci sorgudan ÖNCE uygulanır; sonuçtan ayıklanmaz
	items, err := s.repo.List(ctx, f, clampLimit(limit), offset)
	if err != nil {
		return nil, 0, err
	}
	total, err := s.repo.Count(ctx, f)
	if err != nil {
		return nil, 0, err
	}
	if err := logViews(ctx, s.audit, s.newID, s.nextTick(), actor, items); err != nil {
		return nil, 0, fmt.Errorf("claim: erişim kaydı yazılamadı: %w", err)
	}
	return items, total, nil
}

// Edit kaydın metnini (ve istenirse anahtarını) değiştirir; denetime önceki ve sonraki değeri yazar.
// Değişiklik yoksa hiçbir şey yazılmaz. Metin değiştiyse embedding yeniden üretilir; üretilemezse ESKİ
// vektör ATILIR (eski metne ait vektörle aranmasın), kayıt vektörsüz kalır ve Reindex doldurur.
// Kaydın yazarı, durumu ve tarihçesi değişmez: düzenleyen yalnızca denetimde görünür.
func (s *Service) Edit(ctx context.Context, actor Actor, id string, req EditRequest) (*Claim, error) {
	if strings.TrimSpace(actor.ID) == "" {
		return nil, ErrNoActor
	}
	cur, err := s.repo.Get(ctx, id)
	if err != nil {
		return nil, err
	}
	if !actor.CanSee(cur) {
		return nil, ErrNotFound
	}
	if !CanModify(actor, cur) {
		return nil, ErrForbidden
	}

	next := cur.Clone()
	next.Text = NormalizeText(req.Text)
	if next.Text == "" && (req.SubjectID != nil || req.Fields != nil) {
		next.Text = cur.Text // yalnızca ilgili kişi/alanlar değişiyor: metin korunur
	}
	if req.Key != nil {
		next.Key = NormalizeKey(*req.Key)
	}
	if req.Fields != nil {
		sc, ok := kinds.Default().Get(cur.Kind)
		if !ok {
			return nil, &ValidationError{Errors: []FieldError{{Field: "fields", Message: "bu türün alan şeması yok"}}}
		}
		clean, missing := sc.NormalizeKeyed(req.Fields, "", s.now().In(time.Local))
		if len(missing) > 0 {
			errs := make([]FieldError, 0, len(missing))
			for _, m := range missing {
				errs = append(errs, FieldError{Field: "fields." + m, Message: "zorunlu ya da geçersiz"})
			}
			return nil, &ValidationError{Errors: errs}
		}
		next.Fields = clean
		if req.Key == nil && sc.KeyField != "" && clean[sc.KeyField] != "" {
			next.Key = NormalizeKey(clean[sc.KeyField]) // ad değişince birleştirme anahtarı ayrışmasın
		}
	}
	if req.SubjectID != nil {
		// Kaydın ilgili kişisini yalnızca patron belirler: kişiyi eşlemek, kişisel veriye erişim vermektir.
		if !actor.isPatron() {
			return nil, ErrPatronOnly
		}
		sid := strings.TrimSpace(*req.SubjectID)
		if sid != "" {
			if err := s.verifySubject(ctx, sid); err != nil {
				return nil, err
			}
		}
		next.SubjectID = sid
	}
	if next.Text == cur.Text && next.Key == cur.Key && next.SubjectID == cur.SubjectID &&
		reflect.DeepEqual(emptyToNil(next.Fields), emptyToNil(cur.Fields)) {
		return cur.Snapshot(), nil // değişiklik yok: denetim kirlenmesin
	}
	now := s.nextTick()
	next.UpdatedAt = now
	if err := next.Validate(); err != nil {
		return nil, err
	}
	if next.Key != "" && next.Key != cur.Key {
		peers, err := s.repo.ByKey(ctx, next.Kind, next.Key)
		if err != nil {
			return nil, err
		}
		for _, p := range peers {
			if p.ID != next.ID && (p.Status == StatusActive || p.Status == StatusConflict) {
				return nil, ErrKeyTaken
			}
		}
	}
	if next.Text != cur.Text {
		next.Embedding, next.EmbedModel = nil, ""
		if s.emb != nil {
			if v, eerr := s.emb.Embed(ctx, next.Text); eerr == nil && len(v) > 0 {
				next.Embedding, next.EmbedModel = v, s.cfg.EmbedModel
			} else {
				s.log.Warn("claim düzenlendi ama embedding üretilemedi: kayıt vektörsüz kaldı", "claim", id, "err", eerr)
			}
		}
	}

	if err := s.appendAudit(ctx, actor, id, ActionUpdate, cur, &next, now); err != nil {
		return nil, err
	}
	if err := s.repo.Update(ctx, &next); err != nil {
		return nil, err
	}
	return next.Snapshot(), nil
}

// Delete kaydı siler. Silinen değer denetimde (Before) yaşamaya devam eder. Silinen kayıt başka bir
// kaydın yerini almışsa (Supersedes) eski kayıt OTOMATİK geri getirilmez: hangisinin geçerli olacağı
// patron kararıdır (Faz 6).
func (s *Service) Delete(ctx context.Context, actor Actor, id string) error {
	if strings.TrimSpace(actor.ID) == "" {
		return ErrNoActor
	}
	cur, err := s.repo.Get(ctx, id)
	if err != nil {
		return err
	}
	if !actor.CanSee(cur) {
		return ErrNotFound
	}
	if !CanModify(actor, cur) {
		return ErrForbidden
	}
	// Çelişkili bir kaydın silinmesi karşısındakini tek başına bırakabilir: küme silmeden ÖNCE çıkarılır.
	var group []Claim
	if cur.Status == StatusConflict {
		g, gerr := s.conflictGroupOf(ctx, cur)
		if gerr != nil {
			return gerr
		}
		group = g
	}
	now := s.nextTick()
	if err := s.appendAudit(ctx, actor, id, ActionDelete, cur, nil, now); err != nil {
		return err
	}
	if err := s.repo.Delete(ctx, id); err != nil {
		return err
	}
	if group != nil {
		s.releaseOrphans(ctx, actor, group, id, now)
	}
	return nil
}

// History bir kaydın değişiklik geçmişini (yeni önce) döner. Kayıt silinmiş olsa da çalışır.
// Yalnızca patron ya da kaydın yazarı görebilir; yazar, denetim kayıtlarındaki anlık görüntülerden
// bulunur (kayıt silindiyse depoya bakılamaz). Hiç kayıt yoksa ErrNotFound.
func (s *Service) History(ctx context.Context, actor Actor, id string, limit, offset int) ([]AuditEntry, error) {
	if strings.TrimSpace(actor.ID) == "" {
		return nil, ErrNoActor
	}
	all, err := s.audit.ListByClaim(ctx, id, 0, 0)
	if err != nil {
		return nil, err
	}
	if len(all) == 0 {
		return nil, ErrNotFound
	}
	// Görme yetkisi: kayıt silinmiş olsa da denetimdeki son anlık görüntüden türü/ilgili kişiyi bilebiliriz.
	snap := latestSnapshot(all)
	if snap != nil && !actor.CanSee(snap) {
		return nil, ErrNotFound
	}
	if actor.Role != RolePatron && !authoredBy(all, actor.ID) {
		return nil, ErrForbidden
	}
	if snap != nil {
		if err := logViews(ctx, s.audit, s.newID, s.nextTick(), actor, []Claim{*snap}); err != nil {
			return nil, fmt.Errorf("claim: erişim kaydı yazılamadı: %w", err)
		}
	}
	if offset < 0 {
		offset = 0
	}
	if offset >= len(all) {
		return []AuditEntry{}, nil
	}
	end := offset + clampLimit(limit)
	if end > len(all) {
		end = len(all)
	}
	return all[offset:end], nil
}

// latestSnapshot yeni önce sıralı denetim kayıtlarından kaydın en son anlık görüntüsünü bulur
// (silinmiş kayıtta Before). Hiç görüntü yoksa nil.
func latestSnapshot(entries []AuditEntry) *Claim {
	for _, e := range entries {
		if e.After != nil {
			return e.After
		}
		if e.Before != nil {
			return e.Before
		}
	}
	return nil
}

func emptyToNil(m map[string]string) map[string]string {
	if len(m) == 0 {
		return nil
	}
	return m
}

// verifySubject subjectId'nin gerçek, devre dışı olmayan bir hesap olduğunu doğrular.
func (s *Service) verifySubject(ctx context.Context, userID string) error {
	bad := func(msg string) error {
		return &ValidationError{Errors: []FieldError{{Field: "subjectId", Message: msg}}}
	}
	if s.dir == nil {
		return bad("hesap dizini yok: ilgili kişi atanamaz")
	}
	p, err := s.dir.Lookup(ctx, userID)
	if err != nil {
		return err
	}
	if p == nil || p.Disabled || strings.TrimSpace(p.ID) != userID {
		return bad("böyle bir hesap yok ya da devre dışı")
	}
	return nil
}

// authoredBy denetim anlık görüntülerinden kaydın yazarının userID olup olmadığına bakar.
func authoredBy(entries []AuditEntry, userID string) bool {
	for _, e := range entries {
		if e.Before != nil && e.Before.AuthorID == userID {
			return true
		}
		if e.After != nil && e.After.AuthorID == userID {
			return true
		}
	}
	return false
}
