// Package storage MongoDB bağlantısını kurar ve koleksiyonlara erişim sağlar.
//
// Tasarım kararları:
//   - Bağlantı açılışı ve index kurulumu burada; iş mantığı değil.
//   - Koleksiyon adları ve alan adları (bson etiketleri) `conversation` paketinde tanımlıdır;
//     bu paket depolamanın SCHEMA sahibidir. Depo uygulaması yalnızca erişim yapar.
//   - Ping ile bağlantı doğrulanır: uygulama açılışta anında hata vermek yerine
//     "degraded" çalışabilsin (bellek içi depoya düşme politikası main'de).
package storage

import (
	"context"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

// Koleksiyon adları. `conversation` paketinin şemasıyla birebir eşleşmelidir.
const (
	ConversationsCollection = "conversations"
	MessagesCollection      = "messages"
	SkillsCollection        = "skills"
	ToolsCollection         = "tools"

	// Bilgi Tabanı (Faz 8): belgeler ve aranabilir parçalar AYRI koleksiyonlardır.
	KnowledgeDocumentsCollection = "knowledge_documents"
	KnowledgeChunksCollection    = "knowledge_chunks"

	// Akışlar (Faz 9): akış tanımı ve yürütme kayıtları AYRI koleksiyonlardır.
	// Yürütme kaydı adım adım büyür; akışla aynı dokümanda tutulsaydı her adımda
	// akışın tamamı yazılırdı.
	WorkflowsCollection          = "workflows"
	WorkflowExecutionsCollection = "workflow_executions"

	// MemoriesCollection kalici bellek (P10): kullanicinin anilari (spec koleksiyon adi: memories).
	MemoriesCollection = "memories"
	// NotificationsCollection kullanicilar arasi bildirim/soru/yanit kayitlari.
	//
	// Neden ayri koleksiyon? Bildirim ALICIYA aittir ve gelen/gonen sorgulari
	// toUserId / fromUserId uzerinden yapilir. `users` koleksiyonunda kayit = HESAP,
	// burada kayit = MESAJ; ikisi karistirilirsa kullanici hesabi bildirim sanilir.
	NotificationsCollection = "notifications"
)

// Config bağlantı ayarlarıdır.
type Config struct {
	URI             string
	Database        string
	ConnectTimeout  time.Duration
	ServerSelection time.Duration // sunucu seçimi bekleme süresi
}

// DB açık bir Mongo bağlantısını tutar.
type DB struct {
	client *mongo.Client
	db     *mongo.Database
}

// Connect bağlantı kurar, ping atar ve indexleri oluşturur.
// Zaman aşımları kısa tutulur: kullanıcı arayüzü asılı kalmasın.
func Connect(ctx context.Context, cfg Config) (*DB, error) {
	if cfg.URI == "" {
		return nil, fmt.Errorf("storage: boş MongoDB URI")
	}
	if cfg.Database == "" {
		return nil, fmt.Errorf("storage: boş veritabanı adı")
	}
	if cfg.ConnectTimeout <= 0 {
		cfg.ConnectTimeout = 10 * time.Second
	}
	if cfg.ServerSelection <= 0 {
		cfg.ServerSelection = 5 * time.Second
	}

	opts := options.Client().
		ApplyURI(cfg.URI).
		SetConnectTimeout(cfg.ConnectTimeout).
		SetServerSelectionTimeout(cfg.ServerSelection).
		// Retriable yazmalar (WriteConcern) ağ dalgalanmalarında veri kaybını önler.
		SetRetryWrites(true).
		SetRetryReads(true)

	connectCtx, cancel := context.WithTimeout(ctx, cfg.ConnectTimeout)
	defer cancel()

	client, err := mongo.Connect(connectCtx, opts)
	if err != nil {
		return nil, fmt.Errorf("storage: bağlanılamadı: %w", err)
	}
	// Ping: bağlantı kabul edilse de sunucu erişilemez olabilir.
	if err := client.Ping(connectCtx, readpref.Primary()); err != nil {
		_ = client.Disconnect(context.Background())
		return nil, fmt.Errorf("storage: sunucuya ulaşılamadı: %w", err)
	}

	db := &DB{client: client, db: client.Database(cfg.Database)}
	if err := db.ensureIndexes(connectCtx); err != nil {
		_ = client.Disconnect(context.Background())
		return nil, fmt.Errorf("storage: index kurulamadı: %w", err)
	}
	return db, nil
}

// Database ham veritabanı erişimi verir (depo kurucuları kullanır).
func (d *DB) Database() *mongo.Database { return d.db }

// Client bağlantı havuzunu verir (Close için).
func (d *DB) Client() *mongo.Client { return d.client }

// Conversations konuşma koleksiyonu.
func (d *DB) Conversations() *mongo.Collection {
	return d.db.Collection(ConversationsCollection)
}

// Messages mesaj koleksiyonu.
func (d *DB) Messages() *mongo.Collection {
	return d.db.Collection(MessagesCollection)
}

// Skills skill koleksiyonu.
func (d *DB) Skills() *mongo.Collection {
	return d.db.Collection(SkillsCollection)
}

// Tools tool koleksiyonu.
func (d *DB) Tools() *mongo.Collection {
	return d.db.Collection(ToolsCollection)
}

// KnowledgeDocuments bilgi tabanı belge koleksiyonu.
func (d *DB) KnowledgeDocuments() *mongo.Collection {
	return d.db.Collection(KnowledgeDocumentsCollection)
}

// KnowledgeChunks bilgi tabanı parça koleksiyonu.
func (d *DB) KnowledgeChunks() *mongo.Collection {
	return d.db.Collection(KnowledgeChunksCollection)
}

// Workflows akış koleksiyonu.
func (d *DB) Workflows() *mongo.Collection {
	return d.db.Collection(WorkflowsCollection)
}

// WorkflowExecutions yürütme kaydı koleksiyonu.
func (d *DB) WorkflowExecutions() *mongo.Collection {
	return d.db.Collection(WorkflowExecutionsCollection)
}

// Memories kalici bellek koleksiyonudur.
func (d *DB) Memories() *mongo.Collection {
	return d.db.Collection(MemoriesCollection)
}

// Notifications kullanicilar arasi bildirim koleksiyonudur.
func (d *DB) Notifications() *mongo.Collection {
	return d.db.Collection(NotificationsCollection)
}

// Ping bağlantının sağlığını doğrular (health kontrolü için).
func (d *DB) Ping(ctx context.Context) error {
	return d.client.Ping(ctx, readpref.Primary())
}

// Close bağlantıyı kapatır.
func (d *DB) Close(ctx context.Context) error { return d.client.Disconnect(ctx) }

// ensureIndexes sorgu desenlerini karşılayan indexleri oluşturur.
// Idempotenttir: var olan indexler yeniden oluşturulmaz (CreateMany hata vermez).
func (d *DB) ensureIndexes(ctx context.Context) error {
	// Bileşik (compound) index'lerde anahtarlar SIRALI olmalıdır: map[string]any
	// sırasızdır ve Mongo "multi-key map passed for ordered parameter keys" hatası verir.
	// Bu yüzden bson.D (ordered document) kullanılır.
	//
	// Konuşma listesi: userId + updatedAt'e göre azalan sıralama (kullanıcının ana sorgusu).
	_, err := d.Conversations().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "updatedAt", Value: -1}},
			Options: options.Index().SetName("user_updated"),
		},
		// Konuşma silinince mesajları toplu silmek için.
		{
			Keys:    bson.D{{Key: "conversationId", Value: 1}},
			Options: options.Index().SetName("conv_messages"),
		},
	})
	if err != nil {
		return err
	}
	// Mesaj geçmişi: konuşma + oluşturulma sırası.
	_, err = d.Messages().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "conversationId", Value: 1}, {Key: "createdAt", Value: 1}},
			Options: options.Index().SetName("conv_created"),
		},
	})
	if err != nil {
		return err
	}
	// Skill adı benzersizdir. Bu index tek gerçek koruma: iki eşzamanlı Create'ten
	// biri duplicate key hatası alır (uygulama düzeyi "önce oku sonra yaz" yarışa açıktır).
	_, err = d.Skills().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "name", Value: 1}},
			Options: options.Index().SetName("skill_name_unique").SetUnique(true),
		},
	})
	if err != nil {
		return err
	}
	// Tool adı benzersizdir — aynı gerekçe (EnsureBuiltin her açılışta adla arar).
	_, err = d.Tools().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "name", Value: 1}},
			Options: options.Index().SetName("tool_name_unique").SetUnique(true),
		},
	})
	if err != nil {
		return err
	}
	// Bilgi tabanı belgeleri. (userId, hash) benzersizdir: aynı kullanıcının aynı içeriği iki kez
	// yüklemesini YALNIZCA bu index kesin engeller (eşzamanlı yüklemelerde "önce oku sonra yaz" yarışır).
	_, err = d.KnowledgeDocuments().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "hash", Value: 1}},
			Options: options.Index().SetName("kb_doc_user_hash_unique").SetUnique(true),
		},
		// Belge listesi: kullanıcı + en yeni önce.
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "createdAt", Value: -1}},
			Options: options.Index().SetName("kb_doc_user_created"),
		},
		// Açılışta yarım kalan ("indexing") belgeleri bulmak için.
		{
			Keys:    bson.D{{Key: "status", Value: 1}},
			Options: options.Index().SetName("kb_doc_status"),
		},
	})
	if err != nil {
		return err
	}
	// Bilgi tabanı parçaları: belge içi sıra (okuma/silme) ve kullanıcıya göre arama adayları.
	_, err = d.KnowledgeChunks().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "documentId", Value: 1}, {Key: "index", Value: 1}},
			Options: options.Index().SetName("kb_chunk_doc_index"),
		},
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "projectId", Value: 1}},
			Options: options.Index().SetName("kb_chunk_user_project"),
		},
	})
	if err != nil {
		return err
	}
	// Akış listesi: kullanıcı + en son güncellenen önce (WorkflowRepo.List'in ana sorgusu).
	_, err = d.Workflows().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "updatedAt", Value: -1}, {Key: "_id", Value: 1}},
			Options: options.Index().SetName("wf_user_updated"),
		},
		// Sahiplik denetimi her işlemde (Get/Update/Delete) userId + _id ile yapılır.
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "_id", Value: 1}},
			Options: options.Index().SetName("wf_user_id"),
		},
	})
	if err != nil {
		return err
	}
	// Yürütme geçmişi: akışın yürütmeleri (en yeni önce) ve açılışta yarım kalanlar.
	_, err = d.WorkflowExecutions().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "workflowId", Value: 1}, {Key: "startedAt", Value: -1}},
			Options: options.Index().SetName("wf_exec_user_wf_started"),
		},
		// Sunucu çöktüğünde "running" kalmış kayıtları bulmak için (Recover).
		{
			Keys:    bson.D{{Key: "status", Value: 1}},
			Options: options.Index().SetName("wf_exec_status"),
		},
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "status", Value: 1}},
			Options: options.Index().SetName("wf_exec_user_status"),
		},
	})
	if err != nil {
		return err
	}
	// Bellek (P10): ayni icerik ayni kullanicida ikinci kez saklanmaz (benzersiz ozet);
	// liste en son guncellenen once (MemoryRepo.List'in ana sorgusu).
	_, err = d.Memories().Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "hash", Value: 1}},
			Options: options.Index().SetName("mem_user_hash_unique").SetUnique(true),
		},
		{
			Keys:    bson.D{{Key: "userId", Value: 1}, {Key: "updatedAt", Value: -1}, {Key: "_id", Value: 1}},
			Options: options.Index().SetName("mem_user_updated"),
		},
	})
	if err != nil {
		return err
	}
	// Bildirim (kullanicilar arasi): gelen/gonen kutulari + otomatik sonum temizligi.
	//
	// TTL INDEX NEDEN ZORUNLU? Sorular ve yanitlar `expiresAt` tasir; kullanici hic
	// cevaplamazsa kayit KALICI OLARAK BIRIKMEZ. TTL index'i (Mongo'nun kendi
	// periyodik temizligi) bunu veritabani seviyesinde cozer — uygulama calmasa bile
	// kayitlar silinir. Bellek icindeki deponun "list sirasinda temizle" davranisinin
	// kalici karsiligidir; farki bakim gerektirmemesidir.
	_, err = d.Notifications().Indexes().CreateMany(ctx, []mongo.IndexModel{
		// Gelen kutusu: bana gelenler, en yeni once. ANA SORGUDUR.
		{
			Keys:    bson.D{{Key: "toUserId", Value: 1}, {Key: "createdAt", Value: -1}},
			Options: options.Index().SetName("notify_inbox"),
		},
		// Gonderiler: benim gonderdiklerim + bana gelen yanitlar.
		// AYNI index saatlik kota sayimini da karsilar (fromUserId + createdAt).
		// Ayri bir "notify_quota" index'i TANIMLAMAK CAKISMA uretir: ayni anahtar
		// dizisi farkli isimle tanimlanamaz (Mongo: IndexOptionsConflict
		// "index already exists with a different name"). Testte gercek Mongo'yla
		// yakalandi.
		{
			Keys:    bson.D{{Key: "fromUserId", Value: 1}, {Key: "createdAt", Value: -1}},
			Options: options.Index().SetName("notify_sent"),
		},
		// Bekleyen soru sayimi (alici siniri) ve soru cevaplama.
		{
			Keys: bson.D{{Key: "toUserId", Value: 1}, {Key: "kind", Value: 1},
				{Key: "answeredAt", Value: 1}},
			Options: options.Index().SetName("notify_pending"),
		},
		// SURESI DOLAN kayitlarin otomatik silinmesi (expireAfterSeconds=0 →
		// expiresAt ani gecilince silinir).
		{
			Keys:    bson.D{{Key: "expiresAt", Value: 1}},
			Options: options.Index().SetName("notify_ttl").SetExpireAfterSeconds(0),
		},
	})
	return err
}
