package main

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"fiko-ai/config"
	"fiko-ai/internal/agent"
	"fiko-ai/internal/event"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/memory"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/tool"
	"strings"
)

// memoryParts kalıcı belleğin (Faz 10) çalışma zamanı parçalarıdır.
type memoryParts struct {
	svc      *memory.Service
	recaller agent.MemoryRecaller
	// learner nil ise otomatik öğrenme kapalıdır (MEMORY_AUTO_LEARN=false); elle eklenen anılar yine çalışır.
	learner agent.MemoryLearner
	// questions, LLM'in emin olmadığı bilgiler için kullanıcıya sorulan soruları tutar.
	// Depo süreç içindedir: sunucu kapanınca sorular da düşer (kullanıcının onaylamadığı
	// hiçbir bilgi kalıcılaşmaz).
	questions *memory.QuestionStore
	// emitter, soruları istemciye (WebSocket) gönderen yayıncıdır. main.go tarafından
	// hub hazır olduğunda yerleştirilir; nil ise soru tool'u kaydedilmez.
	emitter func(userID, conversationID string, payload protocol.MemoryQuestionPayload)
}

// memoryQuestions, ajanın ürettiği soruları bellek deposuna yazar VE istemciye gönderir.
//
// İki sorumluluk bilinçli olarak tek yerde toplandı: soru hem kalıcı olmadan yaşamalı
// (TTL) hem de kullanıcıya anında ulaşmalı (emit).
type memoryQuestions struct {
	store *memory.QuestionStore
	emit  func(userID, conversationID string, payload protocol.MemoryQuestionPayload)
	log   *slog.Logger
	// answer, kullanıcının cevabını uygular (ws katmanından gelir).
	answer func(ctx context.Context, questionID string, a memory.Answer) error
}

// memAnswerFunc, kullanıcının soruya verdiği yanıtı uygulayan işlevi döndürür.
// Hem ajanın Submit yolu hem ws katmanı AYNI fonksiyonu kullanır.
// memAnswerFunc, kullanıcının bellek sorusuna verdiği yanıtı uygular.
//
// userID DIŞARIDAN gelir ve karşılaştırma hem burada (ws) hem `ApplyAnswer`
// içinde yapılır. İkinci katman, ilkini atlayan bir çağrıyı yakalar.
func memAnswerFunc(p *memoryParts) func(context.Context, string, string, memory.Answer) error {
	return func(ctx context.Context, userID, questionID string, a memory.Answer) error {
		_, err := memory.ApplyAnswer(ctx, p.svc, p.questions, userID, questionID, a)
		return err
	}
}

// questionOptions, bellek sorusunun seçeneklerini protokol biçimine çevirir.
func questionOptions(opts []memory.QuestionOption) []protocol.MemoryQuestionOption {
	if len(opts) == 0 {
		return nil
	}
	out := make([]protocol.MemoryQuestionOption, 0, len(opts))
	for _, o := range opts {
		out = append(out, protocol.MemoryQuestionOption{
			Text: o.Text, Confirmed: o.Confirmed, SaveAsNew: o.SaveAsNew,
		})
	}
	return out
}

// Ask, `memory.ask` TOOL'unun soru gönderme yoludur (bkz. memory/tools.go).
//
// Bu, soru üretmenin TEK yoludur: model, sohbet sırasında tool çağrısı yapar. Eskiden
// arka plan öğrenme de ayrı bir soru üretiyordu; iki yol birbirini bölüyordu ve
// kullanıcının onaylanmamış bilgisi iki farklı depoya dağılmış oluyordu.
func (m *memoryQuestions) Ask(userID, conversationID string, q memory.Question) string {
	if m == nil || m.store == nil || q.ID == "" {
		return ""
	}
	m.store.Put(q)
	m.log.Info("bellek sorusu gönderiliyor (tool)", "soru", q.ID, "conversation", conversationID)
	m.emit(userID, conversationID, protocol.MemoryQuestionPayload{
		QuestionID: q.ID, Text: q.Text, Options: questionOptions(q.Options),
		Previous: q.Previous, NewValue: q.Content, TargetID: q.TargetID,
		ExpiresAt: q.ExpiresAt.UTC().Format(time.RFC3339),
	})
	return q.ID
}

// RegisterTools, bellek tool'larını tool registry'ye kaydeder.
//
// Neden registry üzerinden? Böylece modele tool olarak sunulurlar; izin, zaman aşımı,
// denetim kaydı ve ToolStart/ToolResult olayları DİĞER tool'larla AYNI mekanizmayla çalışır.
// Elle kaydedilen bellek tool'u (memory.remember) ile otomatik öğrenme AYNI servise yazar.
func (p *memoryParts) RegisterTools(ctx context.Context, reg *tool.Registry, log *slog.Logger) error {
	if p == nil || p.svc == nil || reg == nil {
		return nil
	}
	var ask memory.AskSink
	if p.questions != nil && p.emitter != nil {
		ask = &memoryQuestions{store: p.questions, emit: p.emitter, log: log}
	}
	set := memory.NewToolSet(p.svc, p.questions, ask)
	defs := set.Definitions()
	if len(defs) == 0 {
		return nil
	}
	enabled := true
	for _, d := range defs {
		if _, _, err := reg.EnsureBuiltin(ctx, d.Tool, d.Handler, enabled); err != nil {
			return fmt.Errorf("bellek tool kaydı başarısız (%s): %w", d.Tool.Name, err)
		}
		// KOD DEĞİŞTİYSE KAYITLARI TAZELE. EnsureBuiltin mevcut kaydı dokunmadan
		// bırakır (kullanıcının yaptığı düzenlemeler korunur); ama builtin alanları
		// (izin, risk, şema) KODUN kaynak gerçeğidir ve aksi hâlde eski bir izin
		// modele sunulmayı engelleyebilir.
		//
		// Canlı hata: bellek tool'ları ilk sürümde "write" izniyle kaydedilmişti.
		// Varsayılan politikada write olmadığı için modele hiç SUNULMUYORDULAR ve
		// kullanıcı "güncellemen lazım" / "2013" dediğinde ne güncelleme ne soru oluyordu.
		// Kayıt veritabanında eski izin kaldığı için yalnızca kodu düzeltmek yetmiyordu.
		if err := reg.SyncBuiltinFields(ctx, d.Tool); err != nil {
			log.Warn("bellek tool alanları tazelenemedi", "tool", d.Tool.Name, "err", err)
		}
	}
	names := make([]string, 0, len(defs))
	for _, d := range defs {
		names = append(names, d.Tool.Name)
	}
	log.Info("bellek tool'ları kaydedildi", "tool", strings.Join(names, ","), "soru", ask != nil)
	return nil
}

// setupMemory kalıcı belleği kurar. MEMORY_ENABLED=false ise nil döner (hiçbir şey bağlanmaz).
//
// Embedding modeli yoksa (OLLAMA_EMBED_MODEL boş) bellek yalnızca anahtar kelimeyle aranır ve
// "aynı konudaki eski anının yerini alma" (supersede) çalışmaz — yalnızca birebir aynı içerik tekrarlanmaz.
// Bu bilinçli bir düşüştür; açılışta açıkça söylenir.
func setupMemory(repo memory.Repository, bus event.Bus, provider llm.Provider, emb llm.Embedder, cfg config.Config, log *slog.Logger) *memoryParts {
	m := cfg.Memory
	if !m.Enabled {
		log.Info("kalıcı bellek KAPALI (MEMORY_ENABLED=false): anı getirme, öğrenme ve /api/memories devre dışı")
		return nil
	}

	svc := memory.NewService(repo, bus, memory.Options{Logger: log, MaxPerUser: m.MaxPerUser})
	if cfg.Ollama.EmbedModel != "" && emb != nil {
		svc.SetEmbedder(emb, cfg.Ollama.EmbedModel)
	} else {
		log.Warn("kalıcı bellek embedding olmadan çalışıyor (OLLAMA_EMBED_MODEL boş): arama yalnızca anahtar kelimeyle, " +
			"aynı konudaki eski anı güncellenmez")
	}

	p := &memoryParts{svc: svc, recaller: memoryRecaller{svc: svc, minScore: m.RecallMinScore}}

	// Soru deposu otomatik ÖĞRENMEDEN BAĞIMSIZDIR.
	//
	// Neden? `memory.ask` tool'u sohbet sırasında çalışır ve kullanıcıya soru sorar;
	// bu, arka plan öğrenmesinin işi değildir. Depoyu AutoLearn'e bağlı kurmak,
	// MEMORY_AUTO_LEARN=false iken kullanıcıya SORAMAZ hale getiriyordu: tool'lar
	// hiç sunulmuyor, kart hiç çıkmıyordu.
	p.questions = memory.NewQuestionStore()

	if m.AutoLearn {
		p.learner = memoryLearner{
			learner: memory.NewLearner(svc, memory.NewExtractor(provider, log), log),
			model:   m.LearnModel,
		}
	}
	log.Info("kalıcı bellek ETKİN",
		"otomatikÖğrenme", m.AutoLearn, "sorular", p.questions != nil,
		"embedding", cfg.Ollama.EmbedModel != "",
		"minSkor", m.RecallMinScore, "öğrenmeModeli", learnModelLabel(m.LearnModel))
	return p
}

// learnModelLabel logda boş modeli açıklayıcı yazar.
func learnModelLabel(model string) string {
	if model == "" {
		return "(sohbet modeli)"
	}
	return model
}

// closeMemory arka plan işlerini (kullanım sayacı, yeniden indeksleme) bitirir.
func closeMemory(p *memoryParts, log *slog.Logger) {
	if p == nil || p.svc == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := p.svc.Close(ctx); err != nil {
		log.Warn("bellek servisi zamanında kapanmadı", "err", err)
	}
}

// memoryRecaller memory.Service'i agent.MemoryRecaller arayüzüne uyarlar.
// Ajan memory paketini bilmez (knowledgeRetriever ile aynı desen).
type memoryRecaller struct {
	svc      *memory.Service
	minScore float64
}

func (r memoryRecaller) Recall(ctx context.Context, userID, query string, limit int) []agent.Recalled {
	// Service.Recall zaten HATA DÖNMEZ ve bellek boşken sorgu embedding'i üretmez (ses gecikmesine yük bindirmez).
	hits := r.svc.Recall(ctx, userID, query, limit, r.minScore)
	if len(hits) == 0 {
		return nil
	}
	out := make([]agent.Recalled, 0, len(hits))
	for _, h := range hits {
		out = append(out, agent.Recalled{Kind: string(h.Kind), Content: h.Content, Score: h.Score})
	}
	return out
}

// memoryLearner, otomatik ARKA PLAN öğrenmesini agent.MemoryLearner arayüzüne uyarlar.
//
// AYRIM (önemli): bu tür YALNIZCA arka plan öğrenmeyi besler. Kullanıcıya soru sormak
// ve cevabı uygulamak `memory.ask` TOOL'u ile `memory.ApplyAnswer` üzerinden yürür;
// MEMORY_AUTO_LEARN=false olsa da o akış çalışır. Bu yüzden burada Answer YOKTUR:
// soru üretmek öğrenmenin, cevabı uygulamak servisin işidir.
type memoryLearner struct {
	learner *memory.Learner
	// model doluysa çıkarım bu modelle yapılır; boşsa turun sohbet modeli kullanılır.
	model string
}

func (l memoryLearner) Learn(ctx context.Context, req agent.LearnRequest) int {
	model := req.Model
	if l.model != "" {
		model = l.model
	}
	// Arka plan öğrenme yalnızca KAYDEDER; soru üretmez. Belirsiz bilgi sohbet
	// sırasında `memory.ask` tool'u ile kullanıcıya sorulur.
	return len(l.learner.Learn(ctx, memory.LearnInput{
		UserID:         req.UserID,
		ConversationID: req.ConversationID,
		Text:           req.Text,
		Model:          model,
		Options:        req.Options,
	}))
}
