package notify

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

// MemoryRepository, bildirimleri bellekte tutan depodur.
//
// NEDEN VAR? Üretimde Mongo kullanılır (mongostore/notify.go); bu depo:
//   - testleri (service_test) hızlı ve izole çalıştırır,
//   - MONGO_URI yoksa kurulum çalışmaya devam eder (proje deseni: belleğe düş).
//
// DİKKAT: süreç kapanınca bildirimler KAYBOLUR. Kalıcılık gereken kurulumda
// Mongo bağlanır.
type MemoryRepository struct {
	mu sync.RWMutex
	// data, mesaj kimliği → mesaj. Kutu sorgusu alan taramasıyla yapılır: ölçek bu
	// özellik için küçük (kullanıcı başına yüzlerce kayıt).
	data map[string]Message
}

// NewMemoryRepository boş bir bellek içi depo kurar.
func NewMemoryRepository() *MemoryRepository {
	return &MemoryRepository{data: make(map[string]Message)}
}

var _ Repository = (*MemoryRepository)(nil)

func (r *MemoryRepository) Create(_ context.Context, m Message) (Message, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if m.ID == "" {
		m.ID = newID()
	}
	r.data[m.ID] = m.Clone()
	return m.Clone(), nil
}

func (r *MemoryRepository) Get(_ context.Context, id string) (Message, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	m, ok := r.data[id]
	if !ok {
		return Message{}, ErrNotFound
	}
	return m.Clone(), nil
}

// List, kullanıcının kutusundaki kayıtları döndürür; süresi dolanlar ÖNCE temizlenir
// (bellek içi depoda TTL indeksi yok, temizlik burada yapılır).
func (r *MemoryRepository) List(_ context.Context, userID string, f Filter) ([]Message, int, error) {
	r.purgeExpired()
	r.mu.RLock()
	defer r.mu.RUnlock()

	all := make([]Message, 0, len(r.data))
	for _, m := range r.data {
		if !matchesBox(m, userID, f.Box) {
			continue
		}
		if f.Kind != "" && m.Kind != f.Kind {
			continue
		}
		if f.PendingOnly && !m.IsPending() {
			continue
		}
		all = append(all, m.Clone())
	}
	// Yeniye önce: aynı anda üretilen kayıtlarda kararlılık için kimlik sırası.
	sort.Slice(all, func(i, j int) bool {
		if !all[i].CreatedAt.Equal(all[j].CreatedAt) {
			return all[i].CreatedAt.After(all[j].CreatedAt)
		}
		return all[i].ID > all[j].ID
	})
	total := len(all)
	if f.Offset > 0 {
		if f.Offset >= len(all) {
			return nil, total, nil
		}
		all = all[f.Offset:]
	}
	if f.Limit > 0 && len(all) > f.Limit {
		all = all[:f.Limit]
	}
	return all, total, nil
}

// matchesBox, kayıt bu kullanıcının belirtilen kutusunda mi?
//
// DİKKAT — YANITLAR NEDEN ÖZEL? Bir yanıt kaydında FromUserID, soruyu YANITLAYAN
// kişidir (kim cevapladı); ToUserID ise SORANDIR (kime cevap verildi). Bu yüzden
// yanıt, gönderenin kutusunda DEĞİL, SORANIN kutusunda görünmelidir. Sadece
// "FromUserID == ben" kuralı uygulansaydı "bana gelen yanıtlar" listesinde hiç
// yanıt görünmezdi — oysa kullanıcı tam olarak onu bekler.
//
// Kural özeti:
//
//	inbox → bana gelen bildirim/soru (yanıtlar sayılmaz; onlar gelen kutusunda gürültü)
//	sent  → benim gönderdiklerim + BANA GELEN YANITLAR
func matchesBox(m Message, userID, box string) bool {
	switch box {
	case BoxSent:
		if m.FromUserID == userID {
			return true
		}
		return m.Kind == KindAnswer && m.ToUserID == userID
	default: // BoxInbox
		return m.ToUserID == userID && m.Kind != KindAnswer
	}
}

// purgeExpired süresi dolmuş kayıtları siler (bellek içi TTL temizliği).
func (r *MemoryRepository) purgeExpired() {
	now := time.Now()
	r.mu.Lock()
	defer r.mu.Unlock()
	for id, m := range r.data {
		if m.ExpiresAt != nil && now.After(*m.ExpiresAt) {
			delete(r.data, id)
		}
	}
}

func (r *MemoryRepository) MarkRead(_ context.Context, userID, id string, at time.Time) (Message, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	m, ok := r.data[id]
	if !ok {
		return Message{}, ErrNotFound
	}
	// Gönderen kendi bildirimini "okundu" işaretleyemez: zil sayacı ALICININ
	// okunmamış bildirimlerini sayar, gönderenin işareti sayacı bozardı.
	if m.ToUserID != userID {
		return Message{}, ErrForbidden
	}
	if m.ReadAt == nil {
		t := at
		m.ReadAt = &t
		r.data[id] = m
	}
	return m.Clone(), nil
}

func (r *MemoryRepository) Answer(_ context.Context, userID, questionID, answerText, fromName string, at time.Time) (Message, Message, error) {
	text := Normalize(answerText)
	if text == "" {
		return Message{}, Message{}, &ValidationError{
			Errors: []FieldError{{Field: "answer", Message: "yanıt boş olamaz"}}}
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	q, ok := r.data[questionID]
	if !ok {
		return Message{}, Message{}, ErrNotFound
	}
	// YALNIZCA sorunun alıcısı yanıtlar. Gönderen bile kendi sorusuna cevap veremez:
	// aksi halde kendi sorusunu "onaylamış" görürdü.
	if q.ToUserID != userID {
		return Message{}, Message{}, ErrForbidden
	}
	if q.Kind != KindQuestion {
		return Message{}, Message{}, &ValidationError{
			Errors: []FieldError{{Field: "questionId", Message: "bu bir soru değil"}}}
	}
	if q.AnsweredAt != nil {
		return Message{}, Message{}, &ValidationError{
			Errors: []FieldError{{Field: "questionId", Message: "soru zaten yanıtlandı"}}}
	}
	if q.ExpiresAt != nil && at.After(*q.ExpiresAt) {
		return Message{}, Message{}, ErrNotFound // süresi dolmuş: "bulunamadı" de
	}

	// Soruyu yanıtlandı yap.
	q.AnsweredAt = &at
	if q.ReadAt == nil {
		q.ReadAt = &at
	}
	r.data[q.ID] = q

	// Yanıtı sorana AYRI kayıt olarak yaz: soranın "sent" kutusunda görünsün.
	reply := Message{
		ID:             newID(),
		QuestionID:     q.ID,
		FromUserID:     q.ToUserID,   // yanıtlayan = alıcı
		FromName:       fromName,     // servis çözdü; boşsa arayüz "Bilinmeyen" der
		ToUserID:       q.FromUserID, // yanıt sorana gider
		Kind:           KindAnswer,
		Text:           q.Text, // sorunun metni (bağlam)
		AnswerText:     text,
		ConversationID: q.ConversationID,
		ReadAt:         &at, // gönderen kendi yanıtını okumuş sayılır
		CreatedAt:      at,
	}
	r.data[reply.ID] = reply
	return q.Clone(), reply.Clone(), nil
}

func (r *MemoryRepository) CountPending(_ context.Context, userID string) (int, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	now := time.Now()
	n := 0
	for _, m := range r.data {
		if m.ToUserID == userID && m.IsPending() && (m.ExpiresAt == nil || now.Before(*m.ExpiresAt)) {
			n++
		}
	}
	return n, nil
}

func (r *MemoryRepository) CountSentWindow(_ context.Context, userID string, since time.Time) (int, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	n := 0
	for _, m := range r.data {
		// Yanıtlar da sayıma girer: model üretmiyor ama sınırın amacı modelin
		// taşmasını engellemek; tek kullanıcı için iki yönlü sınır birlikte düşünülür.
		if m.FromUserID == userID && !m.CreatedAt.Before(since) {
			n++
		}
	}
	return n, nil
}

func (r *MemoryRepository) Delete(_ context.Context, userID, id string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	m, ok := r.data[id]
	if !ok {
		return ErrNotFound
	}
	// Katılım denetimi: ne alıcı ne gönderen başkasının bildirimini silemez.
	if m.ToUserID != userID && m.FromUserID != userID {
		return ErrForbidden
	}
	delete(r.data, id)
	return nil
}

// PendingQuestions, alıcının yanıt bekleyen sorularını EN ESKİDEN başlayarak döndürür.
//
// Neden ayrı? WS bağlantısı kurulduğunda kaçırılmış sorular geri yüklenir; List'in
// sayfalı gezinmesi yerine doğrudan tüm bekleyenler gerekir.
func (r *MemoryRepository) PendingQuestions(userID string) []Message {
	r.purgeExpired()
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := make([]Message, 0)
	for _, m := range r.data {
		if m.ToUserID == userID && m.IsPending() {
			out = append(out, m.Clone())
		}
	}
	sort.Slice(out, func(i, j int) bool {
		if !out[i].CreatedAt.Equal(out[j].CreatedAt) {
			return out[i].CreatedAt.Before(out[j].CreatedAt)
		}
		return out[i].ID < out[j].ID
	})
	return out
}

// UnreadCount, alıcının okunmamış bildirim sayısıdır (zil rozeti).
//
// Yanıtlar SAYILMAZ: onlar gönderene gider, gönderenin zili zaten kapalı olmalı.
func (r *MemoryRepository) UnreadCount(userID string) int {
	r.mu.RLock()
	defer r.mu.RUnlock()
	n := 0
	for _, m := range r.data {
		if m.ToUserID == userID && m.Kind != KindAnswer && m.ReadAt == nil {
			n++
		}
	}
	return n
}
