package notify

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

// Limits düzeltmeleri (gönderim sınırları). Varsayılanlar güvenlidir.
type Limits struct {
	// MaxPerHour gönderen başına SAATLİK bildirim sınırı.
	//
	// Neden var? Model istemci üzerinden başkalarına yazıyor. Sınırlama olmazsa
	// model bir döngüde onlarca bildirim üretebilir (canlı hata sınıfı: aynı aracı
	// tekrar tekrar çağırma) ve kullanıcı onlarla boğulur. Hata modele DÖNER ki
	// sınıra takıldığını ve neden gönderemediğini bilsin.
	MaxPerHour int
	// MaxPendingPerReceiver alıcı başına bekleyen soru sınırı.
	//
	// Neden var? notify.ask ASENKRON çalışır: soru depoya girer, model turu biter.
	// Alıcı hiç yanıtlamazsa sorular birikir; sınır aşılınca yeni soru reddedilir
	// (kullanıcının en yeni sorusu bozulmaz).
	MaxPendingPerReceiver int
	// MaxRecipients tek gönderideki en çok alıcı sayısı.
	MaxRecipients int
}

// DefaultLimits güvenli varsayılanlardır.
func DefaultLimits() Limits {
	return Limits{MaxPerHour: 20, MaxPendingPerReceiver: 10, MaxRecipients: MaxRecipients}
}

func (l Limits) normalized() Limits {
	d := DefaultLimits()
	if l.MaxPerHour <= 0 {
		l.MaxPerHour = d.MaxPerHour
	}
	if l.MaxPendingPerReceiver <= 0 {
		l.MaxPendingPerReceiver = d.MaxPendingPerReceiver
	}
	if l.MaxRecipients <= 0 {
		l.MaxRecipients = d.MaxRecipients
	}
	return l
}

// UserResolver, kullanıcı kimliğinden görünen adı çözer.
//
// Neden arayüz? notify paketinin `user` paketini BİLMESİ gerekmiyor; main.go
// bağlar (memory.Recaller deseninin aynısı).
//
// ÇÖZÜLEMEZSE AD BOŞ GELİR (hata değil): bildirim yine gönderilir, arayüzde
// "Bilinmeyen" görünür. Kullanıcı servisinin çökmesi mesaj göndermeyi
// engellememelidir — ama kimseye de yanlış ad gösterilmemelidir.
type UserResolver interface {
	DisplayName(ctx context.Context, userID string) (string, error)
}

// Recipients, alıcı listesini doğrular ve devre dışı kullanıcıları ayıklar.
type Recipients interface {
	// Search kullanıcıları ad ile arar (model `notify.users` ile arar).
	Search(ctx context.Context, q string, limit int) ([]UserRef, error)
	// Resolve kimliklerin etkin olup olmadığını çözer; devre dışı olanlar ÇIKARILIR.
	Resolve(ctx context.Context, userIDs []string) ([]UserRef, error)
}

// UserRef, modele verilen minimal kullanıcı bilgisidir.
//
// GÜVENLİK: parola/e-posta gibi alanlar YOKTUR. Model yalnızca kimliği ve
// görünen adı görür; e-posta modele sızarsa (prompt injection yoluyla) kimlikten
// çok daha fazla ifşa eder.
// UserRef, modele gösterilen minimal kullanıcı görünümüdür.
//
// GÜVENLİK: e-posta, rol, parola özeti ve devre dışı bilgisi TAŞINMAZ.
// Username YENİDEN eklendi (canlı hata 05.10.2026): arama yalnız DisplayName'e
// bakınca, görünen adı boş olan kullanıcılarda "admin" yazan model hiçbir sonuç
// almıyor ve kullanıcı adı UYDURUYORDU. Kullanıcının gerçekten bildiği metin
// kullanıcı adıdır (giriş onunla yapılır); arama onu da görmelidir.
// `json:"-"` DEĞİL: modele güvenli bir alandır (zaten giriş adı biliniyor).
type UserRef struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
	// Username, giriş için kullanılan addır; DisplayName boşsa aramada bu eşleşir.
	Username string `json:"username,omitempty"`
	Disabled bool   `json:"disabled,omitempty"`
}

// Sink, bildirimi alıcıya ANINDA ulaştırır (WebSocket).
//
// Tool katmanı WS'i BİLMEZ (memory.AskSink ile aynı ilke). Sink nil olsa BİLE
// bildirim depoya yazılır (kullanıcı çevrimdışıysa sonra görür) — yani anlık
// ulaştırma bir KOLAYLIK, kalıcılık değil.
type Sink interface {
	Deliver(userID string, m Message)
}

// Options, servis bağımlılıklarıdır.
type Options struct {
	Repo       Repository
	Users      UserResolver
	Recipients Recipients
	Sink       Sink
	Limits     Limits
	Logger     *slog.Logger
	// Now saat kaynağı; testte sabitlenir.
	Now func() time.Time
}

// Service, bildirim iş kurallarını uygular: sınırlar, katılım denetimi, yayın.
type Service struct {
	repo   Repository
	users  UserResolver
	rec    Recipients
	limits Limits
	log    *slog.Logger
	now    func() time.Time

	// mu yalnızca sink'i korur. Sink AÇILIS sırasında (SetSink) değişirken
	// gönderimler OKUNUR; RWMutex olmadan bu bir veri yarışıdır (go test -race
	// yakalar). Kalan alanlar açılışta bir kez yazılır, bu yüzden kilit gerekmez.
	mu   sync.RWMutex
	sink Sink
}

// NewService, Service'i kurar. repo zorunludur; diğerleri nil olabilir
// (özelliği kapatır — örn. Sink yoksa bildirim yine kalıcı yazılır).
//
// Ad neden `New` değil? Pakette zaten `New(NewInput)` mesaj kuruyor. Aynı pakette
// iki `New` olması çağıran tarafı ayırt edilemez hale getirirdi (memory paketi de
// aynı belirsizlikten kaçınmak için `NewService` der).
func NewService(o Options) *Service {
	log := o.Logger
	if log == nil {
		log = slog.Default()
	}
	now := o.Now
	if now == nil {
		now = time.Now
	}
	return &Service{
		repo: o.Repo, users: o.Users, rec: o.Recipients, sink: o.Sink,
		limits: o.Limits.normalized(), log: log, now: now,
	}
}

// activeSink, o an bağlı olan yayıncıyı döndürür (kilit altında okuma).
func (s *Service) activeSink() Sink {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.sink
}

// Repository depoyu döndürür (REST katmanı doğrudan okur).
func (s *Service) Repository() Repository { return s.repo }

// SetSink, anlık iletim yayıncısını bağlar (hub hazır olduğunda main.go çağırır).
//
// NEDEN AYRI? Servis, hub'ı bilmeden kurulur (testler, ileride REST senaryoları).
// Sonradan bağlanmak kurulum sırasını kolaylaştırır: önce depo+ servis, sonra hub.
//
// YARI BAĞLI OLMA UYARISI: Sink bağlanmadan önce gönderilen bildirimler yine
// KAYDEDİLİR, sadece anında iletilmez. Kullanıcı sonraki bağlantısında
// replayNotifications ile onları görür. Bu yüzden bu geçici durum veri kaybı
// DEĞİLDİR.
func (s *Service) SetSink(sink Sink) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.sink = sink
}

// Limits etkin sınırları döndürür (arayüz "saatte en fazla N" gösterebilir).
func (s *Service) Limits() Limits { return s.limits }

// Recipients alıcı arayüzünü döndürür; nil ise `notify.users` sunulmaz.
func (s *Service) Recipients() Recipients { return s.rec }

// SendInput, tek alıcıya bildirim gönderme girdisidir.
//
// ALICI ALANI BURADA YOK: alıcı kimliği doğrulanmış olmalıdır, model yazmaz.
// `notify.users` aramasından gelen kimlik kullanılır.
type SendInput struct {
	FromUserID     string
	ToUserID       string
	Kind           Kind
	Text           string
	Options        []Option
	ConversationID string
}

// Send tek bir bildirim gönderir ve kaydedilen kaydı döndürür.
//
// SIRA ÖNEMLİ: sınır kontrolü KAYDETMEDEN önce yapılır. Sınırı aşan bir gönderim
// depoya yazılmaz; yoksa "reddedildi" görünen bildirim aslında gitmiş olur.
func (s *Service) Send(ctx context.Context, in SendInput) (Message, error) {
	if s.repo == nil {
		return Message{}, errors.New("notify: depo yapılandırılmamış")
	}
	if err := s.checkSendQuota(ctx, in.FromUserID, 1); err != nil {
		return Message{}, err
	}
	m, err := New(NewInput{
		FromUserID:     in.FromUserID,
		FromName:       s.displayName(ctx, in.FromUserID),
		ToUserID:       in.ToUserID,
		Kind:           in.Kind,
		Text:           in.Text,
		Options:        in.Options,
		ConversationID: in.ConversationID,
		Now:            s.now(),
	})
	if err != nil {
		return Message{}, err
	}
	return s.deliver(ctx, m)
}

// SendResult, toplu gönderimin sonucunu AYRI ayrı bildirir: gidenler ve atlananlar.
type SendResult struct {
	Sent   []Message
	Failed map[string]string // alıcı kimliği → hata nedeni
}

// SendToMany, bir metni N alıcıya gönderir.
//
// NEDEN KISMEN GÖNDERİM? Toplu gönderimde tek bir alıcı geçersizse (devre dışı,
// silinmiş) tümü reddetmek kullanıcıya zarar verir; atlananı raporlamak ise
// modele "kimin gitmediğini" gösterir. Bu yüzden hata DEĞİL, `Failed` haritası.
func (s *Service) SendToMany(ctx context.Context, in SendInput, toUserIDs []string) (SendResult, error) {
	res := SendResult{Failed: map[string]string{}}
	if s.repo == nil {
		return res, errors.New("notify: depo yapılandırılmamış")
	}
	if len(toUserIDs) == 0 {
		return res, &ValidationError{Errors: []FieldError{{Field: "to", Message: "alıcı listesi boş"}}}
	}
	if len(toUserIDs) > s.limits.MaxRecipients {
		return res, &ValidationError{Errors: []FieldError{{
			Field:   "to",
			Message: fmt.Sprintf("en fazla %d alıcı seçilebilir", s.limits.MaxRecipients),
		}}}
	}
	// Tek seferlik kota kontrolü: N alıcı N bildirim demektir; saatlik sınır N kez düşülür.
	if err := s.checkSendQuota(ctx, in.FromUserID, len(toUserIDs)); err != nil {
		return res, err
	}

	from := s.displayName(ctx, in.FromUserID)
	now := s.now()
	for _, to := range toUserIDs {
		m, err := New(NewInput{
			FromUserID: in.FromUserID, FromName: from, ToUserID: to,
			Kind: in.Kind, Text: in.Text, Options: in.Options,
			ConversationID: in.ConversationID, Now: now,
		})
		if err != nil {
			res.Failed[to] = err.Error()
			continue
		}
		saved, err := s.deliver(ctx, m)
		if err != nil {
			res.Failed[to] = err.Error()
			continue
		}
		res.Sent = append(res.Sent, saved)
	}
	return res, nil
}

// Ask, alıcıya yanıt bekleyen bir soru sorar (notify.ask).
//
// İKİ KONTROL: gönderen kotası VE alıcının bekleyen soru sınırı. Alıcı sınırı
// doluysa soru GÖNDERİLMEZ ve modele anlaşılır bir hata döner.
func (s *Service) Ask(ctx context.Context, in SendInput) (Message, error) {
	if s.repo == nil {
		return Message{}, errors.New("notify: depo yapılandırılmamış")
	}
	if err := s.checkSendQuota(ctx, in.FromUserID, 1); err != nil {
		return Message{}, err
	}
	pending, err := s.repo.CountPending(ctx, in.ToUserID)
	if err != nil {
		return Message{}, err
	}
	if pending >= s.limits.MaxPendingPerReceiver {
		return Message{}, &ValidationError{Errors: []FieldError{{
			Field: "to",
			Message: fmt.Sprintf("bu kullanıcının yanıt bekleyen %d sorusu var (en fazla %d); önce yanıtlanmalı",
				pending, s.limits.MaxPendingPerReceiver),
		}}}
	}
	m, err := New(NewInput{
		FromUserID:     in.FromUserID,
		FromName:       s.displayName(ctx, in.FromUserID),
		ToUserID:       in.ToUserID,
		Kind:           KindQuestion, // tür burada SABİTLENİR: model "bilgi" diyemez
		Text:           in.Text,
		Options:        in.Options,
		ConversationID: in.ConversationID,
		Now:            s.now(),
	})
	if err != nil {
		return Message{}, err
	}
	return s.deliver(ctx, m)
}

// Answer, alıcının sorusuna verdiği yanıtı uygular ve sorana ANINDA bildirir.
//
// KATILIM DENETİMİ depoda (Answer): yalnızca sorunun alıcısı yanıtlar. İkinci
// katman gerekmez — tek yazma yolu budur.
func (s *Service) Answer(ctx context.Context, userID, questionID, answerText string) (Message, Message, error) {
	if s.repo == nil {
		return Message{}, Message{}, errors.New("notify: depo yapılandırılmamış")
	}
	// Yanıtlayanın adı BURADA çözülür ve depoya yazılır; yoksa kayıt adsız kalır
	// ve gönderen kutusunda "Bilinmeyen" görünür.
	from := s.displayName(ctx, userID)
	q, reply, err := s.repo.Answer(ctx, userID, questionID, answerText, from, s.now())
	if err != nil {
		return Message{}, Message{}, err
	}
	if sink := s.activeSink(); sink != nil {
		sink.Deliver(reply.ToUserID, reply)
	}
	s.log.Info("bildirim sorusu yanıtlandı",
		"soru", questionID, "soran", q.FromUserID, "yanıtlayan", userID)
	return q, reply, nil
}

// List kullanıcının kutusundaki bildirimleri okur (REST katmanı çağırır).
func (s *Service) List(ctx context.Context, userID string, f Filter) ([]Message, int, error) {
	if s.repo == nil {
		return nil, 0, errors.New("notify: depo yapılandırılmamış")
	}
	return s.repo.List(ctx, userID, f)
}

// MarkRead, alıcının bildirimini okundu işaretler.
func (s *Service) MarkRead(ctx context.Context, userID, id string) (Message, error) {
	if s.repo == nil {
		return Message{}, errors.New("notify: depo yapılandırılmamış")
	}
	return s.repo.MarkRead(ctx, userID, id, s.now())
}

// Delete kullanıcının bildirimini siler.
func (s *Service) Delete(ctx context.Context, userID, id string) error {
	if s.repo == nil {
		return errors.New("notify: depo yapılandırılmamış")
	}
	return s.repo.Delete(ctx, userID, id)
}

// Unread, alıcının okunmamış bildirim sayısıdır (zil rozeti).
//
// SAYIM NEREDE YAPILIR? Depo türüne özgüdür (bellekte alan taraması, Mongo'da
// sayım sorgusu); bu yüzden Repository arayüzüne değil, o TÜRE özel isteğe bakılır.
func (s *Service) Unread(ctx context.Context, userID string) int {
	if s.repo == nil {
		return 0
	}
	type counter interface{ UnreadCount(string) int }
	if c, ok := s.repo.(counter); ok {
		return c.UnreadCount(userID)
	}
	// Depo sayım desteklemiyorsa: liste üzerinden hesapla. Küçük veri için doğru,
	// büyük veri için verimsiz — bu yüzden Mongo deposu UnreadCount UYGULAMALIDIR.
	items, _, err := s.repo.List(ctx, userID, Filter{Box: BoxInbox, Limit: 200})
	if err != nil {
		return 0
	}
	n := 0
	for _, m := range items {
		if m.ReadAt == nil && m.Kind != KindAnswer {
			n++
		}
	}
	return n
}

// PendingQuestions, kullanıcının yanıt bekleyen sorularını döndürür (WS geri yükleme).
func (s *Service) PendingQuestions(userID string) []Message {
	type locker interface{ PendingQuestions(string) []Message }
	if l, ok := s.repo.(locker); ok {
		return l.PendingQuestions(userID)
	}
	items, _, err := s.repo.List(context.Background(), userID,
		Filter{Box: BoxInbox, Kind: KindQuestion, PendingOnly: true})
	if err != nil {
		return nil
	}
	return items
}

// deliver kaydı depoya yazar ve çevrimiçi alıcıya anında iter.
func (s *Service) deliver(ctx context.Context, m Message) (Message, error) {
	saved, err := s.repo.Create(ctx, m)
	if err != nil {
		return Message{}, err
	}
	if sink := s.activeSink(); sink != nil {
		// Alıcı çevrimdışıysa sink hiçbir şey yapmaz; kayıt yine de vardır.
		sink.Deliver(saved.ToUserID, saved)
	}
	s.log.Info("bildirim gönderildi",
		"tür", string(saved.Kind), "gönderen", saved.FromUserID,
		"alıcı", saved.ToUserID, "kimlik", saved.ID)
	return saved, nil
}

// checkSendQuota gönderen başına saatlik sınırı uygular. count kaç bildirim
// gönderileceğini belirtir (toplu gönderimde N).
func (s *Service) checkSendQuota(ctx context.Context, fromUserID string, count int) error {
	if s.repo == nil || strings.TrimSpace(fromUserID) == "" {
		return &ValidationError{Errors: []FieldError{{Field: "from", Message: "gönderen belirtilmeli"}}}
	}
	if count <= 0 {
		count = 1
	}
	sent, err := s.repo.CountSentWindow(ctx, fromUserID, s.now().Add(-time.Hour))
	if err != nil {
		return err
	}
	if sent+count > s.limits.MaxPerHour {
		return &ValidationError{Errors: []FieldError{{
			Field: "text",
			Message: fmt.Sprintf("saatte en fazla %d bildirim gönderebilirsin (bu saatte %d kullanıldı); birazdan tekrar dene",
				s.limits.MaxPerHour, sent),
		}}}
	}
	return nil
}

// displayName kullanıcının görünen adını çözer; çözülemezse boş döner
// (bildirim yine gider, arayüzde "Bilinmeyen" görünür).
func (s *Service) displayName(ctx context.Context, userID string) string {
	if s.users == nil || strings.TrimSpace(userID) == "" {
		return ""
	}
	name, err := s.users.DisplayName(ctx, userID)
	if err != nil {
		s.log.Warn("gönderen adı çözülemedi (bildirim yine gönderiliyor)",
			"kullanıcı", userID, "err", err)
		return ""
	}
	return Normalize(name)
}
