// Package notify kullanıcılar ARASI bildirim ve soru akışını yönetir.
//
// Kullanıcı kararı (2026-10-04): "AI diğer kullanıcılara soru sorabilsin +
// bildirim yollayabilsin; bunun için tool istiyorum."
//
// Neden ayrı paket? Kalıcı belleğin KENDİ soruları vardır (`memory.ask`: "bunu
// kaydedeyim mi?"). Buradaki soru farklıdır: "karar senin, soru sana". İkisi
// karışırsa kullanıcı "bunu hatırla" derken aslında görev dağıtıyor sanılır.
// Ayrı tutmak ikisinin de sınırlarını (TTL, sahiplik) netleştirir.
//
// KALICILIK: bildirim ALICIYA AİTTİR. Kullanıcı çevrimdışıysa soru/bildirim
// silinmez, açtığında görür — bu yüzden bellek içi değil Mongo deposu kullanılır.
// Yalnızca "yanıt bekleyen sorular" süreç içinde tutulur.
//
// GÜVENLİK (tasarımda kapatılan):
//   - Gönderen kimliği MODEL DEĞİŞTİREMEZ; executor'dan gelir (tools.go).
//   - Alıcının adı HER ZAMAN gösterilir ("X adına") — model "Fiko gönderdi"
//     diyemez, çünkü gönderen kullanıcıdır.
//   - Gönderen başına saatlik sınır, alıcı başına bekleyen soru sınırı.
//   - Yanıtı YALNIZCA sorunun sahibi verebilir (ErrForbidden).
//   - DEVRE DIŞI kullanıcıya gönderilmez.
//
// AÇIK RİSK (bilinçli): model dolaylı istemle (web içeriği okurken) başkalarına
// mesaj attırabilir. Şimdilik sınırlarla azaltıldı, onay formu YOK.
package notify

import (
	"errors"
	"fmt"
	"strings"
	"time"
	"unicode/utf8"

	"fiko-ai/pkg/id"
)

// NewID, yeni bir bildirim/soru için kimlik üretir (ön ek "nt").
//
// Neden DIŞA AÇIK? mongostore, bir YANIT kaydı yaratırken kimlik üretmek zorunda
// (yanıt kendi kaydıdır; sorunun kimliğini ALAN üzerinden taşır). İki ayrı
// üretici olsaydı biçimler ayrışabilirdi.
func NewID() string { return newID() }

// newID üretilen bildirim kimliğini döndürür. Önek "nt" (notify) okunabilirlik için.
func newID() string { return id.New("nt") }

// Kind, bildirimin türüdür.
//
// Neden üç tür? Arayüzün bunları FARKLI göstermesi gerekir: `info` okunur,
// `question` YANIT BEKLER (kullanıcı karar vermeli), `answer` ise o kararın
// sorana geri dönüşüdür. Tek tür olsaydı arayüz hangisinin aksiyon istediğini
// bilemezdi.
type Kind string

const (
	// KindInfo, karşılığı beklemeyen tek yönlü bildirimdir ("Toplantı 15:00'te").
	KindInfo Kind = "info"
	// KindQuestion, alıcıdan YANIT bekleyen sorudur; süresi dolmadan yanıtlanmalıdır.
	KindQuestion Kind = "question"
	// KindAnswer, alıcının verdiği yanıttır; soranın GELEN KUTUSUNA düşer.
	KindAnswer Kind = "answer"
)

// Valid bilinen bir tür mü?
func (k Kind) Valid() bool {
	switch k {
	case KindInfo, KindQuestion, KindAnswer:
		return true
	}
	return false
}

// NeedsAnswer, bu tür alıcıdan yanıt bekliyor mu?
func (k Kind) NeedsAnswer() bool { return k == KindQuestion }

// Sınırlar.
//
// Neden sınırlar? Model istemci üzerinden KİŞİLER ARASI yazma yapıyor; sınırsız
// olsaydı bir döngüde yüzlerce bildirim üretebilirdi (canlı hata sınıfı: model
// aynı aracı tekrar tekrar çağırır). Sınır hem modeli hem insanı korur.
const (
	// MinTextRunes/MaxTextRunes: bildirim metni. Üst sınır, alıcının okuyacağı
	// makul uzunluk; alt sınır, anlamsız tek kelime göndermeyi engeller.
	MinTextRunes = 3
	MaxTextRunes = 1000

	// MaxOptions bir sorudaki en çok seçenek sayısı (buton satırı taşmasın).
	MaxOptions = 4
	// MaxOptionRunes tek seçeneğin uzunluğu.
	MaxOptionRunes = 120

	// MaxRecipients tek gönderideki en çok alıcı sayısı. Modelin "herkese"
	// göndermesini (broadcast) engeller.
	MaxRecipients = 10

	// QuestionTTL, yanıt bekleyen sorunun süresi. Süre dolunca soru DÜŞER:
	// kimse yanıtlamadıysa gönderen de "yanıt yok" görür, soru sonsuza dek beklemez.
	QuestionTTL = 7 * 24 * time.Hour

	// AnswerTTL, yanıtın gelen kutusunda kalacağı süre.
	AnswerTTL = 30 * 24 * time.Hour
)

// Hata değerleri.
var (
	ErrNotFound   = errors.New("notify: bildirim bulunamadı")
	ErrForbidden  = errors.New("notify: bu bildirim başka bir kullanıcıya ait")
	ErrLimit      = errors.New("notify: sınır aşıldı")
	ErrValidation = errors.New("notify: geçersiz bildirim")
)

// FieldError tek alanın doğrulama hatasıdır (memory paketiyle aynı desen).
type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

// ValidationError bir ya da daha çok alanın geçersiz olduğunu bildirir.
type ValidationError struct{ Errors []FieldError }

func (e *ValidationError) Error() string {
	parts := make([]string, 0, len(e.Errors))
	for _, f := range e.Errors {
		parts = append(parts, f.Field+": "+f.Message)
	}
	return "notify: geçersiz bildirim: " + strings.Join(parts, "; ")
}

// Unwrap ValidationError'ı ErrValidation ile eşleştirir: çağıran `errors.Is` ile
// "geçersiz" ayrımını yapabilsin (REST katmanı 400 üretir).
func (e *ValidationError) Unwrap() error { return ErrValidation }

// Option, sorunun tek bir seçeneğidir: metin + uygulanacak karar.
//
// Neden kararı seçenek taşır? Bellek sorularındaki ders: arayüz buton METNİNDEN
// karar tahmin ederse ("hayır" geçiyorsa ret) model "Hayır, 2014 değil" yazınca
// yanlış karar uygulanır. Karar AÇIKÇA bildirilir.
type Option struct {
	Text string `json:"text" bson:"text"`
	// Value, seçilince yanıt olarak gönderilecek metindir (arayüzde görünmez).
	// Boşsa seçenek kendi metniyle yanıtlar.
	Value string `json:"value,omitempty" bson:"value,omitempty"`
}

// Message, kullanıcılar arası tek bir bildirim/soru/yanıttır.
//
// ALAN AİDİYETİ: bir kayıt üç ayrı listeye girebilir — alıcının GELEN KUTUSU
// (ToUserID), soranın GELEN KUTUSU (FromUserID) ve alıcının soru durumu. Bu
// yüzden tek bir "Inbox" sütunu YOK; sorgular alan üzerinden yapılır. Kalıcılık
// Mongo'da; bellek içi depo yalnızca test içindir.
type Message struct {
	ID string `json:"id" bson:"_id"`
	// QuestionID, bu kaydın hangi soruya ait olduğudur. Soru ve yanıt aynı
	// kimliği paylaşır: soran "şu sorunun yanıtı geldi" listesini bu alanla bulur.
	// Düz bildirimde (KindInfo) boştur.
	QuestionID string `json:"questionId,omitempty" bson:"questionId,omitempty"`

	// FromUserID gönderendir. Model DEĞİŞTİREMEZ; executor'dan gelir (tools.go).
	FromUserID string `json:"fromUserId" bson:"fromUserId"`
	FromName   string `json:"fromName" bson:"fromName"` // arayüzde "X adına" gösterilir
	// ToUserID alıcıdır. Her kayıt TEK bir alıcıya aittir; toplu gönderim N kayıt
	// yazar. Bu, "alıcı başına sınır" uygulamayı ve silmeyi basitleştirir.
	ToUserID string `json:"toUserId" bson:"toUserId"`

	Kind Kind   `json:"kind" bson:"kind"`
	Text string `json:"text" bson:"text"`
	// Options yalnızca KindQuestion'ta dolar.
	Options []Option `json:"options,omitempty" bson:"options,omitempty"`
	// AnswerText, alıcının verdiği yanıtın metni (yalnızca KindAnswer).
	AnswerText string `json:"answerText,omitempty" bson:"answerText,omitempty"`

	// ConversationID, bildirimin doğduğu sohbet (yoksa boş). Denetim izi için.
	ConversationID string `json:"conversationId,omitempty" bson:"conversationId,omitempty"`

	// ReadAt alıcının okuduğu an; nil = okunmadı. Zil sayacı bunu kullanır.
	ReadAt *time.Time `json:"readAt,omitempty" bson:"readAt,omitempty"`
	// AnsweredAt alıcının yanıtladığı an (yalnızca soru).
	AnsweredAt *time.Time `json:"answeredAt,omitempty" bson:"answeredAt,omitempty"`
	// ExpiresAt sonrası kayıt otomatik silinir (soru/yanıt).
	ExpiresAt *time.Time `json:"expiresAt,omitempty" bson:"expiresAt,omitempty"`

	CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
}

// Clone derin kopya döndürür (depolar iç durumu sızdırmasın).
func (m Message) Clone() Message {
	c := m
	c.Options = append([]Option(nil), m.Options...)
	if m.ReadAt != nil {
		t := *m.ReadAt
		c.ReadAt = &t
	}
	if m.AnsweredAt != nil {
		t := *m.AnsweredAt
		c.AnsweredAt = &t
	}
	if m.ExpiresAt != nil {
		t := *m.ExpiresAt
		c.ExpiresAt = &t
	}
	return c
}

// IsPending, soru hâlâ yanıt bekliyor mu?
func (m Message) IsPending() bool {
	return m.Kind == KindQuestion && m.AnsweredAt == nil
}

// Normalize metni tek satıra indirger (bellek paketiyle aynı davranış).
func Normalize(s string) string { return strings.Join(strings.Fields(s), " ") }

// NewInput, bir bildirim/soru oluşturma girdisidir (doğrulama burada yapılır).
type NewInput struct {
	FromUserID     string
	FromName       string
	ToUserID       string
	Kind           Kind
	Text           string
	Options        []Option
	ConversationID string
	Now            time.Time // test için sabitlenebilir
}

// New, girişi doğrulayıp kalıcı bir Message üretir.
//
// Doğrulama modele düz "hata" döner; model hatayı görüp kendi düzeltir. Bu yüzden
// alan alan hata toplanır: "alıcı eksik" ve "metin çok kısa" ikisini birden
// bildirmek modelin tek turda ikisini de düzeltmesini sağlar.
func New(in NewInput) (Message, error) {
	now := in.Now
	if now.IsZero() {
		now = time.Now()
	}
	var errs []FieldError

	kind := in.Kind
	if kind == "" {
		kind = KindInfo
	}
	if !kind.Valid() {
		errs = append(errs, FieldError{Field: "kind", Message: "bilinmeyen tür"})
	}
	if strings.TrimSpace(in.FromUserID) == "" {
		errs = append(errs, FieldError{Field: "fromUserId", Message: "gönderen zorunlu"})
	}
	if strings.TrimSpace(in.ToUserID) == "" {
		errs = append(errs, FieldError{Field: "toUserId", Message: "alıcı zorunlu"})
	}
	if in.FromUserID != "" && in.FromUserID == in.ToUserID {
		// Kendine mesaj: model bunu "bildirim gönderdim" diye sayabilirdi ama
		// karşılığında hiçbir şey olmaz; sessizce geçmek yanıltıcı.
		errs = append(errs, FieldError{Field: "toUserId", Message: "kendine gönderilemez"})
	}

	text := Normalize(in.Text)
	n := utf8.RuneCountInString(text)
	switch {
	case n < MinTextRunes:
		errs = append(errs, FieldError{Field: "text",
			Message: fmt.Sprintf("en az %d karakter olmalı", MinTextRunes)})
	case n > MaxTextRunes:
		errs = append(errs, FieldError{Field: "text",
			Message: fmt.Sprintf("en fazla %d karakter olabilir", MaxTextRunes)})
	}

	opts, optErr := cleanOptions(in.Options, kind)
	if optErr != nil {
		errs = append(errs, *optErr)
	}
	if len(errs) > 0 {
		return Message{}, &ValidationError{Errors: errs}
	}

	m := Message{
		ID:             newID(),
		FromUserID:     strings.TrimSpace(in.FromUserID),
		FromName:       Normalize(in.FromName),
		ToUserID:       strings.TrimSpace(in.ToUserID),
		Kind:           kind,
		Text:           text,
		Options:        opts,
		ConversationID: strings.TrimSpace(in.ConversationID),
		CreatedAt:      now,
	}
	if kind.NeedsAnswer() {
		exp := now.Add(QuestionTTL)
		m.ExpiresAt = &exp
	}
	return m, nil
}

// cleanOptions seçenekleri sınırlar; yalnızca SORU türünde geçerlidir.
func cleanOptions(in []Option, kind Kind) ([]Option, *FieldError) {
	if kind != KindQuestion {
		return nil, nil // düz bildirimde seçenek anlamsız
	}
	out := make([]Option, 0, len(in))
	seen := map[string]struct{}{}
	for _, o := range in {
		text := Normalize(o.Text)
		if text == "" {
			continue
		}
		if utf8.RuneCountInString(text) > MaxOptionRunes {
			return nil, &FieldError{Field: "options",
				Message: fmt.Sprintf("seçenek en fazla %d karakter olabilir", MaxOptionRunes)}
		}
		if _, dup := seen[text]; dup {
			continue
		}
		seen[text] = struct{}{}
		out = append(out, Option{Text: text, Value: Normalize(o.Value)})
		if len(out) >= MaxOptions {
			break
		}
	}
	return out, nil
}
