package memory

// question.go: kullanıcıya sorulan "bunu kaydedeyim mi?" soruları.
//
// Soru üretmenin TEK yolu `memory.ask` tool'udur (bkz. tools.go). Önceden bir de
// çıkarım JSON'unda "action":"ask" yolu vardı; iki ayrı yol iki ayrı davranış demekti
// (biri tool olayı üretiyor, diğeri üretmiyordu) ve model hangisini kullanacağını
// karıştırıyordu. JSON yolu silindi; bu dosya yalnızca tool yolunun deposudur.

import (
	"context"
	"errors"
	"sort"
	"sync"
	"time"
)

// QuestionTTL, kullanıcıya sorulan bir bellek sorusunun bekleme süresidir. Bu sürede
// yanıt gelmezse soru sessizce düşer: hafıza değişmez. (Kullanıcı "kalıcı olsun" derse
// güncelleriz; şimdilik güvenli varsayılan.)
const QuestionTTL = 10 * time.Minute

// MaxPendingPerUser, bir kullanıcının aynı anda bekleyebilecek en çok soru sayısıdır.
//
// Neden sınır? memory.ask ASENKRON çalışır: soru depoya girer ve model turu bitirir.
// Sınırsız olsaydı kullanıcı kartları yanıtlamayıp sohbet ettiğinde (ya da model
// döngüye girip aynı soruyu tekrar tekrar sorduğunda) süreç belleği TTL dolana kadar
// şişerdi. Sınır aşılırsa EN ESKİ soru düşer: en yeni soru kullanıcının baktığı sorudur.
const MaxPendingPerUser = 20

// DefaultAutoImportance, kullanıcı onayıyla kaydedilen bilginin önemidir. Onaylı
// olduğu için otomatik çıkarımdan biraz yüksek (kullanıcı bilinçli karar verdi).
const DefaultAutoImportance = 0.7

var (
	// ErrQuestionNotFound, soru bilinmiyor ya da süresi dolmuş demektir.
	ErrQuestionNotFound = errors.New("bellek sorusu bulunamadı veya süresi doldu")
	// ErrQuestionForbidden, sorunun başka bir kullanıcıya ait olduğunu bildirir.
	// "Bulunamadı"dan AYRI tutulur: ws katmanı bunu güvenlik olayı olarak loglar.
	ErrQuestionForbidden = errors.New("bellek sorusu başka bir kullanıcıya ait")
)

// Question, modelin emin olmadığı bir bilgi için KULLANICIYA sorulan sorudur.
//
// Kullanıcı isteği: "bilgi varsa güncellicek, yoksa eklicek; hafızaya hiçbir işlemden
// geçmeden soru sorma". Yani belirsizlik bu noktada durur ve insana gider — LLM de koda
// da kararı kendi başına vermez.
type Question struct {
	// ID sorunun kimliğidir; cevap bu kimlikle döner.
	ID     string
	UserID string
	// ConversationID sorunun doğduğu konuşmadır. Sekme yenilenince bekleyen sorular
	// konuşmaya göre geri yüklenir (bkz. QuestionStore.PendingFor).
	ConversationID string
	Kind           Kind
	Content        string // kaydedilecek/güncellenecek metin
	TargetID       string // güncellenecek kayıt (yoksa yeni kayıt)
	Text           string // soru metni (modelin yazdığı doğal Türkçe cümle)
	// Options: her seçenek kararını KENDİSİ taşır.
	//
	// Neden yapı? Önceden yalnızca metin taşınıyordu ve kararı arayüz buton
	// metninden tahmin ediyordu ("hayır" geçiyorsa ret). Model "Hayır, farklı bir yıl"
	// derse bu seçenek yanlışlıkla ret sayılıyordu; oysa kullanıcı onaylamak
	// istiyordu. Artık karar AÇIKÇA bildirilir.
	Options   []QuestionOption
	Previous  string    // önceki kaydın metni: arayüzde "şundan → şuna" göstermek için
	ExpiresAt time.Time // bu süreden sonra soru düşer
}

// QuestionOption, sorunun tek bir seçeneğidir: metin + uygulanacak karar.
type QuestionOption struct {
	Text      string // butonda görünen metin (modelden gelir)
	Confirmed bool   // true ise seçilince kayıt/güncelleme UYGULANIR
	SaveAsNew bool   // true ise uygulamak yerine ayrı kayıt açılır
}

// Answer, kullanıcının soruya verdiği yanıttır.
type Answer struct {
	// Confirmed true ise karar UYGULANIR (kayıt/güncelleme). false ise HİÇBİR ŞEY yapılmaz.
	Confirmed bool
	// SaveAsNew true ise güncelleme yerine YENİ kayıt açılır.
	SaveAsNew bool
}

// AnswerResult, bir yanıtın uygulanma sonucudur.
type AnswerResult struct {
	// Question yanıtlanan sorudur (konuşma kimliği olay yayını için gerekir).
	Question Question
	// Declined true ise kullanıcı reddetti ve hiçbir şey yazılmadı.
	Declined bool
	// Memory yazılan/güncellenen kayıttır (reddedilmişse nil).
	Memory  *Memory
	Outcome Outcome // Created | Updated | Unchanged
}

// QuestionStore, kullanıcıya sorulan soruları süreyle tutar.
//
// Neden bellekte, Mongo'da değil? Süre dolarsa düşecek zaten kalıcı olması gereksizdir ve
// kalıcı depoya yazmak, kullanıcının ONAYLAMADIĞI bir bilgiyi kalıcılaştırırdı.
type QuestionStore struct {
	mu   sync.Mutex
	ttl  time.Duration
	now  func() time.Time
	data map[string]Question
}

// NewQuestionStore boş bir soru deposu kurar.
func NewQuestionStore() *QuestionStore {
	return &QuestionStore{ttl: QuestionTTL, now: time.Now, data: make(map[string]Question)}
}

// Put yeni bir soru ekler (süre otomatik atanır). Kullanıcı sınırı aşılırsa o
// kullanıcının en eski sorusu düşer (bkz. MaxPendingPerUser).
func (s *QuestionStore) Put(q Question) {
	if s == nil || q.ID == "" {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if q.ExpiresAt.IsZero() {
		q.ExpiresAt = s.now().Add(s.ttl)
	}
	if _, exists := s.data[q.ID]; !exists {
		s.evictOldestLocked(q.UserID)
	}
	s.data[q.ID] = q
}

// evictOldestLocked, kullanıcı sınırdaysa en eski (bitişi en yakın) sorusunu siler.
func (s *QuestionStore) evictOldestLocked(userID string) {
	n := 0
	var oldestID string
	var oldest time.Time
	for id, q := range s.data {
		if q.UserID != userID {
			continue
		}
		n++
		if oldestID == "" || q.ExpiresAt.Before(oldest) {
			oldestID, oldest = id, q.ExpiresAt
		}
	}
	if n >= MaxPendingPerUser && oldestID != "" {
		delete(s.data, oldestID)
	}
}

// Get soruyu döndürür; süresi dolmuşsa YOK sayılır (bulunamaz).
func (s *QuestionStore) Get(id string) (Question, bool) {
	if s == nil || id == "" {
		return Question{}, false
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	q, ok := s.data[id]
	if !ok {
		return Question{}, false
	}
	if s.now().After(q.ExpiresAt) {
		delete(s.data, id) // süresi doldu
		return Question{}, false
	}
	return q, true
}

// Take soruyu alır ve DEPOdan siler: bir soru yalnızca bir kez yanıtlanabilir.
func (s *QuestionStore) Take(id string) (Question, bool) {
	if s == nil || id == "" {
		return Question{}, false
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	q, ok := s.data[id]
	if !ok {
		return Question{}, false
	}
	delete(s.data, id)
	if s.now().After(q.ExpiresAt) {
		return Question{}, false
	}
	return q, true
}

// TakeFor, Take'in SAHİPLİK KONTROLLÜ halidir: soru yalnızca sahibi için alınır.
//
// Neden ayrı? ws katmanı önce Get ile sahipliği kontrol edip sonra Take çağırıyordu;
// iki adım arasında soru başkası tarafından alınabilirdi (ve kontrol iki yerde yazılıydı).
// Burada kontrol ve silme TEK kilit altında yapılır. Başkasının sorusu SİLİNMEZ: yanlış
// kullanıcının denemesi gerçek sahibin kartını yok etmemeli.
func (s *QuestionStore) TakeFor(id, userID string) (Question, error) {
	if s == nil || id == "" {
		return Question{}, ErrQuestionNotFound
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	q, ok := s.data[id]
	if !ok {
		return Question{}, ErrQuestionNotFound
	}
	if s.now().After(q.ExpiresAt) {
		delete(s.data, id)
		return Question{}, ErrQuestionNotFound
	}
	if q.UserID != userID {
		return Question{}, ErrQuestionForbidden
	}
	delete(s.data, id)
	return q, nil
}

// PendingFor, kullanıcının hâlâ yanıt bekleyen sorularını döndürür (en eski önce).
// conversationID doluysa yalnızca o konuşmanınkiler gelir.
//
// Sekme yenilenince ya da WS yeniden bağlanınca kartların geri yüklenmesi içindir:
// soru yalnızca anlık bir WS olayıyla gidiyordu ve o sırada bağlantı yoksa sonsuza
// dek kayboluyordu (depoda 10 dakika duruyordu ama kimse sormuyordu).
func (s *QuestionStore) PendingFor(userID, conversationID string) []Question {
	if s == nil || userID == "" {
		return nil
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	now := s.now()
	var out []Question
	for _, q := range s.data {
		if q.UserID != userID || now.After(q.ExpiresAt) {
			continue
		}
		if conversationID != "" && q.ConversationID != conversationID {
			continue
		}
		out = append(out, q)
	}
	sort.Slice(out, func(i, j int) bool {
		if !out[i].ExpiresAt.Equal(out[j].ExpiresAt) {
			return out[i].ExpiresAt.Before(out[j].ExpiresAt)
		}
		return out[i].ID < out[j].ID
	})
	return out
}

// Purge süresi dolmuş soruları temizler.
func (s *QuestionStore) Purge() int {
	if s == nil {
		return 0
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	now := s.now()
	n := 0
	for id, q := range s.data {
		if now.After(q.ExpiresAt) {
			delete(s.data, id)
			n++
		}
	}
	return n
}

// Run, ctx iptal edilene kadar süresi dolan soruları düzenli temizler.
//
// Purge yazılmıştı ama HİÇ çağrılmıyordu: yanıtlanmayan sorular yalnızca aynı kimlikle
// Get/Take yapılırsa siliniyordu, yani süreç ömrünce birikiyordu. Çağıran `go store.Run(ctx)`
// ile başlatır.
func (s *QuestionStore) Run(ctx context.Context, every time.Duration) {
	if s == nil {
		return
	}
	if every <= 0 {
		every = time.Minute
	}
	tk := time.NewTicker(every)
	defer tk.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-tk.C:
			s.Purge()
		}
	}
}

// Size kaç soru bekliyor?
func (s *QuestionStore) Size() int {
	if s == nil {
		return 0
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return len(s.data)
}

// ApplyAnswer kullanıcının soruya verdiği yanıtı uygular: onaylarsa kaydeder/günceller,
// reddederse HİÇBİR ŞEY yapmaz. userID sorunun sahibi olmalıdır (ErrQuestionForbidden).
//
// ÖNEMLİ — başarısız uygulama soruyu YAKMAZ: soru uygulanamazsa (veritabanı hatası, anı
// sınırı, doğrulama) depoya GERİ KONUR. Eskiden Take soruyu önce siliyor, sonra yazmayı
// deniyordu; yazma hata verirse arayüz kartı "tekrar dene" diye açıyor ama sunucu artık
// "soru bulunamadı" diyordu: kullanıcı bilgiyi kaybediyor ve neden kaybettiğini
// göremiyordu.
//
// ORIGIN = USER: onay kullanıcının KENDİ kararıdır. Servis, otomatik çıkarımın elle
// yazılmış kaydı ezmesini engeller (OriginUser kaydı OriginAuto güncelleyemez); önceki
// uygulama OriginAuto kullandığı için kullanıcı "evet, güncelle" dediği halde elle
// yazdığı kayıt SESSİZCE DEĞİŞMİYORDU (sonuç Unchanged). Koruma LLM'in tahmini içindir,
// insanın açık kararı için değil.
func ApplyAnswer(ctx context.Context, svc *Service, store *QuestionStore, userID, questionID string, a Answer) (AnswerResult, error) {
	if svc == nil || store == nil {
		return AnswerResult{}, ErrQuestionNotFound
	}
	q, err := store.TakeFor(questionID, userID)
	if err != nil {
		return AnswerResult{}, err
	}
	res := AnswerResult{Question: q}
	if !a.Confirmed {
		// Kullanıcı reddetti: karar uygulanmaz. Bu da bir karardır — kullanıcının.
		res.Declined, res.Outcome = true, Unchanged
		return res, nil
	}
	target := q.TargetID
	if a.SaveAsNew {
		target = "" // güncelleme yerine ayrı kayıt istenmiş
	}
	m, outcome, err := svc.Add(ctx, AddInput{
		UserID: q.UserID, Kind: q.Kind, Content: q.Content, Origin: OriginUser,
		ConversationID: q.ConversationID, Importance: DefaultAutoImportance, TargetID: target,
	})
	if err != nil {
		store.Put(q) // süresi korunur (ExpiresAt dolu): kullanıcı yeniden deneyebilir
		return res, err
	}
	res.Memory, res.Outcome = m, outcome
	return res, nil
}
