// Package agent bir kullanıcı mesajını alıp LLM'e götüren, cevabı akıtan ve kaydeden çekirdek orkestratördür.
// Phase 1: tek tur sohbet. Tool calling / skill seçimi sonraki fazlarda buraya eklenecek.
//
// Olay sözleşmesi (istemciye giden): her üretim şu sırayı izler
//
//	[conversation.updated]? → conversation.state THINKING → assistant.thinking
//	→ conversation.state SPEAKING → assistant.text.delta* → assistant.text.final
//	→ conversation.state IDLE
//
// text.final HER ZAMAN gönderilir (status: complete | interrupted | error); kesintide ek olarak
// assistant.interrupted, hatada ek olarak error olayı gelir. Böylece istemci mesaj balonunu
// her durumda tek bir terminal olayla kapatabilir.
package agent

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"time"
	"unicode/utf8"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/ctxmgr"
	"fiko-ai/internal/event"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/prompt"
	"fiko-ai/internal/protocol"
	"fiko-ai/pkg/id"
)

// Emit istemciye bir olay yollar. Bloklamamalıdır (WS katmanı tamponlu kuyruğa yazar).
type Emit func(protocol.Outbound)

// Config ajanın çalışma parametreleridir; sıfır değerler varsayılanla değiştirilir.
type Config struct {
	MaxHistory        int           // LLM'e gönderilecek en çok mesaj (varsayılan 60)
	MaxInputRunes     int           // tek mesaj üst sınırı (varsayılan 8000)
	GenerationTimeout time.Duration // tek üretim üst süresi (varsayılan 5 dk)
	Timezone          string        // sistem istemindeki saat dilimi
	ContextWindow     int           // ContextSize 0 iken varsayılan bağlam penceresi, token (varsayılan 131072)
	Options           llm.Options   // model parametreleri (statik)

	// MaxIterations tool çağırma döngüsünün üst sınırıdır (varsayılan 4, en fazla 8).
	// Spec: "Agent sonsuz döngüye girmemeli." 0 = varsayılan.
	MaxIterations int

	// OptionsFunc verilirse Options YOKSAYILIR ve üretim anında çağrılır.
	// Böylece ayarlar (temperature/topP) çalışma zamanında değişebilir:
	// bir sonraki mesaj yeni değerleri kullanır.
	OptionsFunc func() llm.Options

	// PromptEditsFunc, sistem istemi parçalarının yönetici düzenlemelerini döndürür.
	// Aynı gerekçe: ayarlar çalışma zamanında değişebilir, yani yönetici istem
	// metnini arayüzden değiştirdiğinde BİR SONRAKİ MESAJdan itibaren geçerli olsun
	// (sunucu yeniden başlatmasın). nil ise kod varsayılanları kullanılır.
	PromptEditsFunc func() (prompt.Overrides, prompt.Disabled)

	// ModelFunc verilirse üretim anında çağrılır ve AYARDAKİ model adını döndürür.
	//
	// Neden gerekli? İstemci `text.message` payload'ında model GÖNDERMEZ; protokol
	// sözleşmesi "boşsa sunucu varsayılanı" der (bkz. TextMessagePayload.Model).
	// Bu yüzden UI'dan seçilen modelin tek taşıyıcısı bu geri çağırmadır.
	// Bağlanmazsa her üretim .env'deki OLLAMA_MODEL'e (muse-glimmer:30b) düşer ve
	// ayar ekranındaki model seçimi sessizce ETKİSİZ kalır.
	ModelFunc func() string

	// ConfirmFunc verilirse üretim anında çağrılır ve AYARDAKİ onay politikasını
	// döndürür (always|risky|missing|never).
	//
	// Neden fonksiyon? Politika kullanıcı tarafından ayar ekranından değiştirilir ve
	// değişiklik ANINDA geçerli olmalıdır. Değeri açılışta kopyalasak ayar ekranı
	// boşuna çalışırdı (bkz. ModelFunc/OptionsFunc ile aynı desen).
	ConfirmFunc func() string
}

// Input tek bir kullanıcı mesajı isteğidir.
type Input struct {
	UserID         string
	ConversationID string
	RequestID      string // istemci istek kimliği; yanıt olaylarında replyTo olarak döner
	Payload        protocol.TextMessagePayload
}

// Agent eşzamanlı kullanıma güvenlidir.
type Agent struct {
	svc    *conversation.Service
	llm    llm.Provider
	prompt *prompt.Builder
	ctxm   *ctxmgr.Manager // bağlam penceresi yöneticisi (Phase 2)
	bus    event.Bus       // nil olabilir
	log    *slog.Logger
	cfg    Config

	// Tool çağırma (Faz 5). Üçü de nil olabilir → ajan tool'suz (tek tur) çalışır.
	runner    ToolRunner
	catalog   ToolCatalog
	knowledge KnowledgeRetriever // Faz 8: nil ise bilgi tabanı (RAG) kullanılmaz
	skills    SkillRetriever     // Faz 5c: nil ise skill retrieval yapılmaz (tüm tool'lar sunulur)
	emit      Emit               // tool olaylarını istemciye akıtmak için (HandleText dışında)

	memory  MemoryRecaller // Faz 10: nil ise kalıcı anılar isteme girmez
	learner MemoryLearner  // Faz 10: nil ise turlardan otomatik öğrenme yapılmaz
	claims  ClaimWriter    // Iddia Hafızası: nil ise sohbetten claim yakalanmaz
	// claimRead Iddia Hafızası okuması (Faz 4): nil ise ekip hafızası ne isteme ne cevaba girer.
	claimRead ClaimReader

	// personas, kullanıcıya atanmış davranış profillerini okur (yönetici tanımı).
	// nil ise profil kuralları isteme hiç girmez — sistem eskisi gibi çalışır.
	personas PersonaSource

	// Yazma/silme onay formları (bkz. confirm.go). Model geri alınamaz ya da onay
	// gerektiren bir işlem istediğinde araç form döndürür; burada o form istemciye
	// gönderilir ve KULLANICI onaylayana kadar tool tekrar çalıştırılmaz.
	confirm PendingConfirm
	// Not: soru gönderme yolu burada DEĞİLDİR. Kullanıcıya sormak `memory.ask`
	// tool'unun işidir (bkz. tool/registry); ajan yalnızca tool'u sunar.

	// Arka plan işleri (anı öğrenme). Close iptal eder ve bitmesini bekler.
	bg       context.Context
	bgCancel context.CancelFunc

	mu     sync.Mutex
	closed bool
	active map[string]*run
	wg     sync.WaitGroup
}

type run struct {
	cancel context.CancelFunc
	reason string // Cancel ile verilen sebep
	emit   Emit   // bu turun olay çıktısı (tool olayları da buradan çıkar)
}

func New(svc *conversation.Service, p llm.Provider, pb *prompt.Builder, bus event.Bus, log *slog.Logger, cfg Config) *Agent {
	if cfg.MaxHistory < 1 {
		cfg.MaxHistory = 60
	}
	if cfg.MaxInputRunes < 1 {
		cfg.MaxInputRunes = 8000
	}
	if cfg.GenerationTimeout <= 0 {
		cfg.GenerationTimeout = 5 * time.Minute
	}
	if cfg.MaxIterations < 1 {
		cfg.MaxIterations = 4
	}
	if cfg.MaxIterations > MaxIterationsCap {
		// Spec: "Agent sonsuz döngüye girmemeli." Aşırı büyük değer bir yapılandırma hatasıdır;
		// sınırsız döngüye yol açmasın diye burada kesiyoruz (sessizce 8'e düşürmüyoruz).
		cfg.MaxIterations = MaxIterationsCap
	}
	if pb == nil {
		pb = prompt.Default()
	}
	if log == nil {
		log = slog.Default()
	}
	bg, bgCancel := context.WithCancel(context.Background())
	return &Agent{svc: svc, llm: p, prompt: pb, ctxm: ctxmgr.New(ctxmgr.Config{DefaultWindow: cfg.ContextWindow}),
		bg: bg, bgCancel: bgCancel,
		bus: bus, log: log, cfg: cfg, active: make(map[string]*run),
		confirm: NewPendingConfirm()}
}

// SetSkills, skill retrieval kaynağını bağlar (Faz 5c). nil vermek retrieval'ı
// kapatır ve sistem "tüm izinli tool'lar" davranışına döner.
func (a *Agent) SetSkills(s SkillRetriever) { a.skills = s }

// SetTools tool çağırma yeteneklerini bağlar (Faz 5).
//
// Olay çıktısı (emit) BURADA VERİLMEZ: tool olayları, turu başlatan WS oturumunun
// emit'inden çıkar (bkz. begin/emitFor). Böylece eşzamanlı iki konuşmada tool olayları
// doğru oturuma gider. Kurulum sırasında (eşzamanlı kullanım başlamadan) çağrılmalıdır.
func (a *Agent) SetTools(runner ToolRunner, catalog ToolCatalog) {
	a.runner, a.catalog = runner, catalog
}

// Cancel konuşmadaki aktif üretimi durdurur. Aktif üretim yoksa false döner.
func (a *Agent) Cancel(conversationID, reason string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	r, ok := a.active[conversationID]
	if !ok {
		return false
	}
	if r.reason == "" {
		r.reason = reason
	}
	r.cancel()
	return true
}

// Busy konuşmada üretim sürüyor mu?
func (a *Agent) Busy(conversationID string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	_, ok := a.active[conversationID]
	return ok
}

// Close yeni üretimleri reddeder, süren hepsini iptal eder ve bitmelerini bekler.
func (a *Agent) Close(ctx context.Context) error {
	a.mu.Lock()
	a.closed = true
	if a.bgCancel != nil {
		a.bgCancel() // arka plan öğrenmeyi iptal et
	}
	for _, r := range a.active {
		if r.reason == "" {
			r.reason = "shutdown"
		}
		r.cancel()
	}
	a.mu.Unlock()

	done := make(chan struct{})
	go func() { a.wg.Wait(); close(done) }()
	select {
	case <-done:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

// begin aktif bir tur açar: süre sınırı bağlar, emit'i kaydeder ve kilit alır.
// emit TUR BAŞINA saklanır (global alan DEĞİL): iki konuşma eşzamanlı üretirken
// tool olayları yanlış oturuma sızmasın.
func (a *Agent) begin(parent context.Context, convID string, emit Emit) (context.Context, func(), bool) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.closed {
		return nil, nil, false
	}
	if _, busy := a.active[convID]; busy {
		return nil, nil, false
	}
	ctx, cancel := context.WithTimeout(parent, a.cfg.GenerationTimeout)
	a.active[convID] = &run{cancel: cancel, emit: emit}
	a.wg.Add(1)
	return ctx, func() {
		cancel()
		a.mu.Lock()
		delete(a.active, convID)
		a.mu.Unlock()
		a.wg.Done()
	}, true
}

// emitFor bir turun olay çıktısını döndürür (tur yoksa nil: olay sessizce düşer).
func (a *Agent) emitFor(convID string) Emit {
	a.mu.Lock()
	defer a.mu.Unlock()
	if r, ok := a.active[convID]; ok {
		return r.emit
	}
	return nil
}

func (a *Agent) takeReason(convID string) string {
	a.mu.Lock()
	defer a.mu.Unlock()
	if r, ok := a.active[convID]; ok {
		return r.reason
	}
	return ""
}

func (a *Agent) publish(ctx context.Context, t event.Type, in Input, data map[string]any) {
	if a.bus == nil {
		return
	}
	a.bus.Publish(ctx, event.Event{Type: t, UserID: in.UserID, ConversationID: in.ConversationID, Data: data})
}

// HandleText bir kullanıcı mesajını baştan sona işler ve tamamlanana kadar bloklar
// (WS katmanı bunu ayrı bir goroutine'de çağırır). Tüm sonuçlar emit ile olay olarak iletilir.
// ctx bağlantı/sunucu ömrüdür; iptali "interrupted" olarak sonuçlanır.
func (a *Agent) HandleText(ctx context.Context, in Input, emit Emit) {
	send := func(t protocol.ServerType, payload any) {
		o := protocol.NewOutbound(t, in.ConversationID, payload)
		o.ReplyTo = in.RequestID
		emit(o)
	}
	fail := func(code, msg string, retryable bool) {
		send(protocol.ServerError, protocol.ErrorPayload{Code: code, Message: msg, Retryable: retryable})
	}

	text := strings.TrimSpace(in.Payload.Text)
	switch {
	case in.ConversationID == "":
		fail(protocol.ErrCodeBadRequest, "conversationId gerekli", false)
		return
	case text == "":
		fail(protocol.ErrCodeBadRequest, "mesaj boş olamaz", false)
		return
	case utf8.RuneCountInString(text) > a.cfg.MaxInputRunes:
		fail(protocol.ErrCodeTooLarge, fmt.Sprintf("mesaj en fazla %d karakter olabilir", a.cfg.MaxInputRunes), false)
		return
	}

	runCtx, release, ok := a.begin(ctx, in.ConversationID, emit)
	if !ok {
		fail(protocol.ErrCodeBusy, "bu konuşmada zaten bir yanıt üretiliyor", true)
		return
	}
	defer release()

	// Kayıt işlemleri, üretim iptal edilse bile tamamlanmalı.
	persist := func() (context.Context, context.CancelFunc) {
		return context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
	}

	pctx, pcancel := persist()
	_, newTitle, err := a.svc.AddUserMessage(pctx, in.UserID, in.ConversationID, text)
	pcancel()
	if err != nil {
		if errors.Is(err, conversation.ErrNotFound) {
			fail(protocol.ErrCodeBadRequest, "konuşma bulunamadı", false)
		} else {
			a.log.Error("kullanıcı mesajı kaydedilemedi", "err", err, "conversation", in.ConversationID)
			fail(protocol.ErrCodeInternal, "mesaj kaydedilemedi", true)
		}
		return
	}
	if newTitle != "" {
		send(protocol.ServerConversationUpdated, protocol.ConversationUpdatedPayload{Title: newTitle})
	}
	a.publish(ctx, event.UserMessageReceived, in, nil)

	pctx, pcancel = persist()
	history, err := a.svc.Messages(pctx, in.UserID, in.ConversationID, a.cfg.MaxHistory)
	pcancel()
	if err != nil {
		a.log.Error("geçmiş okunamadı", "err", err, "conversation", in.ConversationID)
		fail(protocol.ErrCodeInternal, "konuşma geçmişi okunamadı", true)
		return
	}

	messageID := id.New("msg")
	send(protocol.ServerConversationState, protocol.StatePayload{State: protocol.StateThinking})
	send(protocol.ServerAssistantThinking, protocol.ThinkingPayload{MessageID: messageID})
	a.publish(ctx, event.AgentStarted, in, map[string]any{"messageId": messageID})

	// Bağlam yöneticisi: geçmişi modelin penceresine sığdırır (Phase 2).
	opts := a.llmOptions()
	// Araç sunulacak mı? (istem kurulumu da buna göre değişir)
	toolsEnabled := a.toolsEnabled(in.UserID)
	// SKILL RETRIEVAL → TOOL RETRIEVAL (Faz 5c): önce skill, sonra o skill'lerin tool'u.
	tp := a.plan(runCtx, in.UserID, text)
	// BİLGİ TABANI (Faz 8) + KALICI BELLEK (Faz 10): ikisi de tool'dan bağımsızdır ve sorgu
	// embedding'i üretir; bu yüzden PARALEL alınır (ses gecikmesi iki katana çıkmasın).
	memCh := a.recallMemoriesAsync(runCtx, in.UserID, text)
	claimCh := a.readClaimsAsync(runCtx, in, text) // Faz 4: ekip ortak hafızası (bayrak kapalıysa anında boş)
	tp.knowledge = a.retrieveKnowledge(runCtx, in.UserID, text)
	tp.memories = <-memCh
	cc := <-claimCh
	tp.teamMemory = cc.Prompt
	fit := a.buildMessages(history, opts, toolsEnabled, tp)
	if fit.Dropped > 0 || fit.Clipped {
		a.log.Info("bağlam kırpıldı", "conversation", in.ConversationID,
			"dropped", fit.Dropped, "clipped", fit.Clipped,
			"estimatedTokens", fit.EstimatedTokens, "window", fit.Window)
	}
	// İstek generate() içinde kurulur: tool döngüsünde her tur yeniden kurılması gerekir.
	started := time.Now()
	speaking := false
	var st runState
	res, toolRuns, genErr := a.generate(runCtx, in, &st, tp.tools, fit.Messages, opts, func(delta string) error {
		if !speaking {
			speaking = true
			send(protocol.ServerConversationState, protocol.StatePayload{State: protocol.StateSpeaking})
		}
		send(protocol.ServerAssistantTextDelta, protocol.TextDeltaPayload{MessageID: messageID, Delta: delta})
		return nil
	})
	// Liste sorusunda liste SUNUCUDA üretildi: modelin giriş cümlesinin ARDINA aynen eklenir (model
	// satır atlayamaz/uyduramaz). Yalnızca üretim başarılıysa; iptal/hata yarım cevaba liste yapıştırmaz.
	if genErr == nil && cc.Append != "" {
		extra := cc.Append
		if strings.TrimSpace(res.Content) != "" {
			extra = "\n\n" + extra
		}
		if !speaking {
			speaking = true
			send(protocol.ServerConversationState, protocol.StatePayload{State: protocol.StateSpeaking})
		}
		send(protocol.ServerAssistantTextDelta, protocol.TextDeltaPayload{MessageID: messageID, Delta: extra})
		res.Content += extra
	}
	latency := time.Since(started).Milliseconds()
	reason := a.takeReason(in.ConversationID)

	status := conversation.StatusComplete
	var terminal func() // asistan mesajı kaydedildikten sonra gönderilecek ek olaylar
	finalState := protocol.StateIdle

	switch {
	case genErr == nil && strings.TrimSpace(res.Content) == "":
		status = conversation.StatusError
		finalState = protocol.StateError
		terminal = func() { fail(protocol.ErrCodeLLM, "model boş yanıt döndürdü", true) }
		a.publish(ctx, event.AgentFailed, in, map[string]any{"messageId": messageID, "error": "empty response"})
	case genErr == nil:
		a.publish(ctx, event.AgentCompleted, in, map[string]any{"messageId": messageID, "latencyMs": latency})
	case errors.Is(genErr, context.Canceled):
		status = conversation.StatusInterrupted
		finalState = protocol.StateInterrupted
		if reason == "" {
			reason = "user"
			if ctx.Err() != nil {
				reason = "disconnect" // bağlantı/üst bağlam koptu; sunucu kapanışı Close ile "shutdown" olarak işaretlenir
			}
		}
		partial := res.Content
		terminal = func() {
			send(protocol.ServerAssistantInterrupted, protocol.InterruptedPayload{MessageID: messageID, Partial: partial, Reason: reason})
		}
		a.publish(ctx, event.ConversationInterrupted, in, map[string]any{"messageId": messageID, "reason": reason})
	case errors.Is(genErr, context.DeadlineExceeded):
		status = conversation.StatusError
		finalState = protocol.StateError
		terminal = func() { fail(protocol.ErrCodeLLM, "yanıt süresi aşıldı", true) }
		a.publish(ctx, event.AgentFailed, in, map[string]any{"messageId": messageID, "error": "timeout"})
	default:
		status = conversation.StatusError
		finalState = protocol.StateError
		a.log.Warn("LLM üretimi başarısız", "err", genErr, "conversation", in.ConversationID)
		msg := "LLM isteği başarısız: " + genErr.Error()
		terminal = func() { fail(protocol.ErrCodeLLM, msg, true) }
		a.publish(ctx, event.AgentFailed, in, map[string]any{"messageId": messageID, "error": genErr.Error()})
	}

	// Kısmi içerik de değerlidir: boş olmayan her sonuç kaydedilir.
	//
	// Model adı: Ollama cevaptaki adı döndürür, ama BULUT modellerinde soneki düşer
	// ("gemma4:31b-cloud" isteği -> "gemma4:31b"). Bu, kayıtta ve arayüzde modelin
	// DEĞİŞTİĞİ izlenimi uyandırıyordu (kullanıcı ayar ekranına bakıp "sıfırlandı"
	// sanıyordu) ve bir sonraki seçimde listede eşleşmiyordu. Gerçekten kullanılan
	// model = çözülen model adı; istenen de bu. Ollama boş dönerse o da bu olur.
	model := a.resolveModel(in.Payload.Model)
	if strings.TrimSpace(res.Model) != "" {
		model = res.Model
	}
	// Form kartı açıldıysa içerik boş olsa da mesaj kaydedilir: ekranda görünen form,
	// yenilemede ve geçmişte kaybolmamalı.
	if strings.TrimSpace(res.Content) != "" || len(st.confirms) > 0 {
		pctx, pcancel = persist()
		_, saveErr := a.svc.AddAssistantMessage(pctx, in.ConversationID, conversation.AssistantMessage{
			ID:               messageID,
			Content:          res.Content,
			Status:           status,
			Model:            model,
			PromptTokens:     res.PromptTokens,
			CompletionTokens: res.CompletionTokens,
			LatencyMs:        latency,
			ToolRuns:         toolRuns,
			ToolConfirms:     st.confirms,
		})
		if saveErr != nil {
			a.log.Error("asistan mesajı kaydedilemedi", "err", saveErr, "conversation", in.ConversationID)
		} else if len(st.confirms) > 0 {
			// Tur kaydedilirken cevaplanan formlar (erken cevap) şimdi karta işlenir.
			a.applyEarlyResolutions(pctx, in, st.confirms)
		}
		pcancel()
	}

	// Tamamlanan turdan kalıcı bilgi öğren (arka plan; turu BEKLETMEZ, hata sohbeti bozmaz).
	if status == conversation.StatusComplete {
		a.learn(in, text, model, opts)
		if !cc.List { // liste isteği yeni bilgi taşımaz: boşuna çıkarım çağrısı yapma
			a.captureClaims(in, text, model, opts)
		}
	}

	send(protocol.ServerAssistantTextFinal, protocol.TextFinalPayload{
		MessageID:        messageID,
		Content:          res.Content,
		Status:           string(status),
		Model:            model,
		PromptTokens:     res.PromptTokens,
		CompletionTokens: res.CompletionTokens,
		LatencyMs:        latency,
	})
	if terminal != nil {
		terminal()
	}
	send(protocol.ServerConversationState, protocol.StatePayload{State: finalState})
	if finalState != protocol.StateIdle {
		send(protocol.ServerConversationState, protocol.StatePayload{State: protocol.StateIdle})
	}
}

// generate model üretimini, gerekiyorsa tool çağrılarıyla birlikte YÖNETİR (Faz 5).
//
// Döngü şöyledir (spec: USER INPUT → CONTEXT → SKILL → TOOL → LLM → DECISION):
//
//	LLM'e tool tanıtılır → tool çağrısı gelirse çalıştır → sonucu geçmişe koy → tekrar üretim
//	→ çağrı yok ise döngü biter ve SON YANIT kullanıcıya akar.
//
// Güvenlik ve dayanıklılık:
//   - Tool YOKSA veya sağlayıcı desteklemiyorsa tek turlu üretime düşer (bugünkü davranış).
//   - Her tool çalıştırması ToolRunner'ın İÇİNDE yetki denetiminden geçer.
//   - Hatalı tool sonucu LLM'e "hata" metni olarak geri beslenir: model kendini düzeltebilir.
//   - Döngü MaxIterations ile sınırlıdır: model sonsuza kadar tool çağıramaz.
//   - İptal (barge-in) ctx ile tüm turlara yayılır.
//
// st, turun durumudur (tool kayıtları + kalıcı onay kartları). Dışarıdan verilir: üretim hata
// ya da iptalle bitse bile çağıran, o ana dek açılmış kartları kaydedebilsin (ekranda görünen
// bir form, tur yarım kalsa da geçmişte kaybolmamalı).
// fitTools, araç tanımları kullanıcının SABİTLEDİĞİ pencereye sığmıyorsa listeyi
// KADEMELİ olarak küçültür.
//
// Neden var? (Canlı hata 05.10.2026) Araç tanımları isteğin "tools" alanında
// gönderilir ve modelin bağlamında YER TUTAR. 34 araç ~2-3.5k token tutuyor; buna
// sistem istemi ve geçmiş eklenince num_ctx aşılıyor ve model cevabı sessizce
// ortasından kesiyordu ("...Hangi" diye biten mesajlar).
//
// İKİ KATMANLI ÇÖZÜM (bölünmüş):
//   1. Pencere büyütme → ctxmgr.FitWithTools (yalnız kullanıcı ayar VERMEDİYSE).
//      Böylece araç görünürlüğü korunurken TARİHÇE bütçesi AZALMAZ.
//      (Araç maliyetini doğrudan bütçeden düşmek tarihçeyi %33 düşürüyordu;
//      kullanıcı "AI her mesajda sıfırlanıyor" dedi.)
//   2. Kırpma (burada) → yalnız kullanıcı pencereyi SABİTLEDİYSE devreye girer.
//
// Kırpma İLGİ SIRASIyla yapılır, alfabetik değil: tools en ilgili başta gelir,
// sondan kesmek "en az ilgili" aracı düşürür. MaxToolSpecs'in ilk halindeki
// hatanın (alfabetik kesme) tam tersi.
func (a *Agent) fitTools(msgs []llm.Message, tools []llm.ToolSpec, opts llm.Options) ([]llm.ToolSpec, llm.Options) {
	if len(tools) == 0 {
		return tools, opts
	}
	// Kullanıcı sabit pencere vermemişse ctxmgr zaten büyüttü; tekrar dokunma.
	if opts.ContextSize == 0 {
		return tools, opts
	}
	window := opts.ContextSize
	if window < ctxmgr.MinWindow {
		window = ctxmgr.MinWindow
	}
	msgCost := ctxmgr.TotalCost(msgs)
	reserve := opts.MaxTokens
	if reserve <= 0 {
		reserve = 1024
	}
	if reserve > window/2 {
		reserve = window / 2
	}
	margin := window / 20
	if margin < 48 {
		margin = 48
	}
	room := window - msgCost - reserve - margin

	cost := ctxmgr.ToolTokenCost(tools)
	if cost <= room {
		return tools, opts
	}

	// Sondan (en az ilgili) kırp.
	const minTools = 4
	kept := tools
	for len(kept) > minTools && ctxmgr.ToolTokenCost(kept) > room {
		kept = kept[:len(kept)-1]
	}
	a.log.Warn("bağlam penceresi dar; araç listesi kırpıldı (en ilgili araçlar korundu)",
		"pencere", window, "kalanAraç", len(kept), "istenenAraç", len(tools),
		"mesajToken", msgCost, "toolToken", cost)
	return kept, opts
}

func (a *Agent) generate(ctx context.Context, in Input, st *runState, tools []llm.ToolSpec, messages []llm.Message, opts llm.Options, onDelta llm.DeltaFunc) (llm.Result, []ToolRun, error) {
	// Model bir kez, üretimin başında çözülür: tool döngüsünde turlar aynı modeli
	// kullanır (aksi hâlde ayar araya girerse turdan tura model değişebilirdi).
	model := a.resolveModel(in.Payload.Model)

	// tools, bu tur için HAZIRLANMIŞ listeydir (Faz 5c: skill retrieval ile daraltılmış olabilir).
	// Katalog burada BİR DAHA sorgulanmaz: plan() zaten sorguladı (çift sorgu hem yavaş
	// hem de iki farklı anda farklı izin görüntüsü demektir).
	provider, ok := a.llm.(llm.ToolProvider)
	if !ok || len(tools) == 0 {
		// Tool yok: klasik tek tur (tool'suz sistemlerin davranışı DEĞİŞMEZ).
		res, err := a.llm.GenerateStream(ctx, llm.Request{Model: model, Messages: messages, Options: opts}, onDelta)
		return res, nil, err
	}

	maxIter := a.cfg.MaxIterations
	if maxIter < 1 {
		maxIter = 1
	}

	var last llm.Result
	conv := messages
	// Araç tanımları promptun parçasıdır; pencereye sığmıyorsa liste kademeli küçültülür
	// (canlı hata 05.10.2026: model cevabı "...Hangi" diye yarım bırakıyordu).
	tools, opts = a.fitTools(conv, tools, opts)
	names := toolNameSet(tools)
	// ÖN ÇALIŞTIRMA (bkz. prefetch.go): modele sormadan, sunucuda çalıştırılması güvenli
	// ve ucuz araçların sonucu buraya eklenir. Küçük model tool çağırmayı unutsa bile
	// elinde GERÇEK veri olur. Hata halinde sessizce atlanır, döngü bozulmaz.
	conv = a.prefetch(ctx, in, st, tools, conv, in.Payload.Text)
	for iter := 0; iter < maxIter; iter++ {
		if err := ctx.Err(); err != nil {
			return last, st.runs, err
		}
		// Çağrıyı düz metin olarak yazan modeller için süzgeç (bkz. textcall.go): çağrıya
		// benzeyen başlangıç kullanıcıya akıtılmaz, sonda çağrıya çevrilir.
		sn := newCallSniffer(names, onDelta)
		res, err := provider.GenerateStreamWithTools(ctx, llm.Request{
			Model: model, Messages: conv, Options: opts,
		}, tools, sn.Delta)
		if err != nil {
			if sn.mode != sniffHold { // yarım kalmış çağrı metni kullanıcıya gösterilmez
				_ = sn.flush() // karar bekleyen kısa metin kaybolmasın
			}
			last = res
			return last, st.runs, err
		}
		if converted, ferr := sn.Finish(&res); ferr != nil {
			last = res
			return last, st.runs, ferr
		} else if converted {
			a.log.Info("tool çağrısı düz metin olarak geldi; çağrıya çevrildi",
				"tool", res.ToolCalls[0].Name, "conversation", in.ConversationID)
		}
		last = res
		if len(res.ToolCalls) == 0 {
			return last, st.runs, nil
		}

		// Model hem konuştu hem tool istediyse konuşma kısmı ikinci turda tekrar akmasın diye
		// yalnızca tool turu eklenir.
		if txt := strings.TrimSpace(res.Content); txt != "" {
			conv = append(conv, llm.Message{Role: llm.RoleAssistant, Content: txt})
		}
		for _, c := range res.ToolCalls {
			// **Kendi çağrısını geçmişe yaz.** Bu satır olmadan model bir sonraki turda
			// "ben hiçbir şey yapmadım" sanar ve AYNI tool çağrısını tekrar tekrarlar
			// (canlı testte 4 turda aynı çağrıyı tekrarladı, döngü sınırına dayandı).
			conv = append(conv, llm.Message{Role: llm.RoleAssistant, Content: toolCallText(c)})
			conv = a.runToolCall(ctx, in, st, conv, c, tools)
		}
	}

	// Sınır aşıldı: model her turda tool istemeye devam ediyor. Kullanıcı sonsuz bekletilmez;
	// döngü durdurulur ve neden açıklanır.
	a.log.Warn("tool döngüsü sınırı aşıldı", "iterations", maxIter, "conversation", in.ConversationID)
	if onDelta != nil {
		_ = onDelta(fmt.Sprintf("\n\n[Sistem: tool çağrısı %d kez denendi; döngü durduruldu.]", maxIter))
	}
	last.Content = strings.TrimSpace(last.Content + fmt.Sprintf("\n\n[Sistem: tool çağrısı %d kez denendi; döngü durduruldu.]", maxIter))
	return last, st.runs, nil
}

// runToolCall tek bir tool çağrısını çalıştırır, olaylarını yayar, KALICI kaydını
// toplar ve sonucu geçmişe ekler.
//
// TEKRAR KORUMASI: aynı araç aynı girdiyle ikinci kez çağrılırsa GERÇEKTEN çalıştırılmaz.
// Gerekçe: tool geçmişi düz metin olarak geri beslendiği için model bazen aynı çağrıyı
// tekrarlar (canlı testte oldu). Tekrar, hem zaman hem yan etki (dosya yazma/silme)
// hem de sonsuz döngü riskidir; tekrarı sessizce çalıştırmak yanlış olurdu.
// Modele "zaten çalıştı" bilgisi verilerek bir sonraki adıma geçmesi sağlanır.
//
// Hata DÖNGÜYÜ KIRMAMALIDIR: model hatayı görüp alternatif deneyebilir.
//
// tools, bu tur için sunulan araç listeleridir: argümanları aracın şemasına göre
// düzeltmek için gerekir (bkz. args.go normalizeArgsInPlace).
func (a *Agent) runToolCall(ctx context.Context, in Input, st *runState, conv []llm.Message,
	c llm.ToolCall, tools []llm.ToolSpec) []llm.Message {
	if err := ctx.Err(); err != nil {
		return conv
	}
	// GÜVENLİK: onay bayrağını YALNIZCA ajan verebilir. Modelin (özellikle küçük bir
	// modelin) `confirmed:true` uydurup kullanıcı onayını atlamasını engelle.
	if agentConfirmedTool(c.Name) {
		c.Arguments = withoutKey(c.Arguments, "confirmed")
	}
	// BOZUK ARGÜMAN DÜZELTMESİ (bkz. args.go): model "5"/"true"/"{...}" gibi metin
	// gönderebiliyor; araç şemasına göre düzeltilir. "proje sil" → ref bulunamadı,
	// "500 tl ekle" → 500 sayı değil diye hesap yapılamadı: ikisi de canlı hata sınıfı.
	c.Arguments = normalizeArgsInPlace(c.Name, toolSchemaOf(tools, c.Name), c.Arguments)

	// Tekrar mı? Çalıştırma, sonucu yeniden kullan.
	if prev, dup := st.duplicate(c.Name, c.Arguments); dup {
		// Olay YINE de yayılır: istemci her çağrı için tool.result bekler (bkz. API.md).
		a.send(in, protocol.ServerToolResult, protocol.ToolResultPayload{
			CallID:     id.New("tc"),
			Name:       c.Name,
			Status:     "success",
			Output:     prev.Output,
			DurationMs: 0,
			Error:      "",      // hata değil; "tekrar" bilgisi modele aşağıda gider
			UI:         prev.UI, // önbellekten gelen sonuç aynı görünümde sunulur
		})
		a.publish(ctx, event.ToolExecuted, in, map[string]any{"tool": c.Name, "result": "cached"})
		return append(conv, llm.Message{Role: llm.RoleUser, Content: fmt.Sprintf(
			"[%s tool sonucu — TEKRAR ÇAĞRI, yeniden çalıştırılmadı] %s", c.Name, cachedText(prev.Output))})
	}

	callID := id.New("tc")
	a.send(in, protocol.ServerToolStart, protocol.ToolStartPayload{CallID: callID, Name: c.Name, Input: c.Arguments})

	started := time.Now()
	run, err := a.runTool(ctx, in, c.Name, c.Arguments, func(step string, percent int) {
		a.send(in, protocol.ServerToolProgress, protocol.ToolProgressPayload{CallID: callID, Step: step, Percent: percent})
	})
	payload := protocol.ToolResultPayload{CallID: callID, Name: c.Name, DurationMs: time.Since(started).Milliseconds()}

	// Kalıcı kayıt: kullanıcı sayfayı yenilediğinde "ne çalıştı?" sorusunu
	// cevaplayabilmelidir. Büyük girdi/çıktı sınırlanır.
	record := ToolRun{
		CallID: callID, Name: c.Name, Status: "success",
		DurationMs: payload.DurationMs,
		Input:      ToolPreview(c.Arguments, ToolPreviewMax),
	}

	if err != nil {
		payload.Status = "error"
		payload.Error = err.Error()
		record.Status, record.Error, record.DurationMs = "error", err.Error(), payload.DurationMs
		a.log.Warn("tool çalıştırma başarısız", "tool", c.Name, "err", err, "conversation", in.ConversationID)
		a.publish(ctx, event.ToolExecuted, in, map[string]any{"tool": c.Name, "result": "error", "error": err.Error()})
		conv = append(conv, llm.Message{Role: llm.RoleUser, Content: toolErrorText(c.Name, err)})
	} else {
		payload.Status = "success"
		payload.Output = run.Output
		payload.DurationMs = run.DurationMs
		payload.UI = run.UI
		record.DurationMs = run.DurationMs
		record.Output = ToolPreview(run.Output, ToolPreviewMax)
		record.UI = run.UI

		// ---- ONAY FORMU ----
		// Yazma/silme aracı çalışmak yerine form döndürdüyse: formu istemciye yolla ve
		// modele "işlem yapılmadı" de. Tool'un çıktısı tool.result olayında da görünür,
		// böylece kullanıcı hem formu hem kaydı görür (kayıt "onay bekliyor" durumundadır).
		if confirmID, isForm := a.handleConfirmForm(in, callID, c.Name, run.Output); isForm {
			payload.UI = "form"
			record.Output = ToolPreview(run.Output, ToolPreviewMax)
			a.send(in, protocol.ServerToolResult, payload)
			a.publish(ctx, event.ToolExecuted, in,
				map[string]any{"tool": c.Name, "result": "pending_confirm", "confirmId": confirmID})
			conv = append(conv, llm.Message{Role: llm.RoleUser, Content: fmt.Sprintf(
				"[%s ONAY BEKLENİYOR] İşlem YAPILMADI. Kullanıcı ekrandaki formu onaylayacak veya "+
					"vazgeçecek. Onay gelene kadar aynı aracı TEKRAR ÇAĞIRMA. Bilmen gerekenler: %s",
				c.Name, confirmHint(run.Output))})
			st.add(record)
			// Kalıcı kart: yenilemede ve geçmişte form geri gelsin (bkz. conversation.ToolConfirm).
			if p, ok := a.confirm.Payload(confirmID); ok {
				st.addConfirm(toolConfirmRecord(p, time.Now()))
			}
			return conv
		}

		a.publish(ctx, event.ToolExecuted, in, map[string]any{"tool": c.Name, "result": "success", "durationMs": run.DurationMs})
		conv = append(conv, llm.Message{Role: llm.RoleUser, Content: toolResultText(c.Name, run.Output)})
	}
	st.add(record)
	st.remember(c.Name, c.Arguments, ToolResult{Output: run.Output, DurationMs: run.DurationMs, UI: run.UI})
	a.send(in, protocol.ServerToolResult, payload)
	return conv
}

// cachedText, tekrar çağrıda modele verilen sonuç metnidir (toolResultText ile aynı biçim).
func cachedText(out map[string]any) string {
	raw, err := json.Marshal(out)
	if err != nil {
		return "(sonuç okunamadı)"
	}
	return string(raw)
}

// runTool aracı çalıştırır; araç onay formu döndürdüyse ve politika form GEREKTİRMİYORSA
// (risky modda ekleme/güncelleme, `never` modu) aracı AJANIN verdiği onayla bir kez daha çalıştırır.
//
// CANLI HATA (04.10.2026): biz.add / biz.delete `confirmed` gelmedikçe HER ZAMAN form
// döndürür. Politika "form gerekmez" dese de aracı onayla yeniden çalıştıran kimse yoktu:
// model ham form JSON'unu "araç sonucu" diye alıyor, kayıt YAZILMIYOR, model ise
// "tamam, ekledim" diyordu. Form GEREKLİYSE aracı yeniden çalıştırmayız: form kullanıcıya
// gider ve çalıştırma ConfirmAnswer'da yapılır.
func (a *Agent) runTool(ctx context.Context, in Input, name string, args map[string]any, progress func(string, int)) (ToolResult, error) {
	res, err := a.runner.Run(ctx, name, args, in.UserID, progress)
	if err != nil {
		return res, err
	}
	cf, ok := formOf(res.Output)
	if !ok || a.confirmRequired(cf, strsOf(cf["missingFields"])) {
		return res, nil
	}
	approved := cloneArgs(args)
	approved["confirmed"] = true
	return a.runner.Run(ctx, name, approved, in.UserID, progress)
}

// confirmHint, onay bekleyen tool çıktısından modele verilecek kısa özettir.
// Model "ekranda ne görüyor" bilgisine ihtiyaç duyar: eksik alanlar varsa kullanıcıdan
// isteyeceği şeyi önceden bilebilmelidir.
func confirmHint(out map[string]any) string {
	cf, _ := formOf(out)
	if cf == nil {
		return "onay bekleniyor."
	}
	if strOf(cf["action"]) == askAction {
		// Bilgi formu: model "onay" değil "cevap" bekler; yazıyla soru tekrarlamasın.
		return "Bu bir BİLGİ FORMU. Kullanıcı doldurup gönderince cevaplar 'Form cevabı' diye başlayan " +
			"yeni bir mesajla gelecek. Soruları yazıyla tekrarlama; tek kısa cümleyle formu doldurmasını söyle ve dur."
	}
	var b strings.Builder
	if title := strOf(cf["title"]); title != "" {
		b.WriteString(title)
	}
	if msg := strOf(cf["message"]); msg != "" {
		b.WriteString(" — ")
		b.WriteString(msg)
	}
	if missing := strsOf(cf["missingFields"]); len(missing) > 0 {
		b.WriteString(" Eksik alanlar: ")
		b.WriteString(strings.Join(missing, ", "))
		b.WriteString(". Kullanıcı bunları form üzerinde dolduracak; sen soru yazma.")
	}
	return b.String()
}

// toolCallText modelin yaptığı çağrıyı geçmişe düz metin olarak yazar.
// Sağlayıcının tool_calls + tool_name kuralları sağlayıcıdan sağlayıcıya değiştiği için
// (Ollama "tool", OpenAI "function") tek bir taşınabilir biçim kullanıyoruz.
func toolCallText(c llm.ToolCall) string {
	args, err := json.Marshal(c.Arguments)
	if err != nil || len(args) == 0 {
		args = []byte("{}")
	}
	return fmt.Sprintf("→ %s(%s)", c.Name, args)
}

// toolResultText tool çıktısını modele geri beslenen metne çevirir.
// Ham JSON çoğu zaman okunabilir; yine de kısaltılır (bağlam şişmesin).
func toolResultText(name string, out map[string]any) string {
	raw, err := json.Marshal(out)
	if err != nil {
		return fmt.Sprintf("%s çalıştı, çıktı okunamadı.", name)
	}
	const max = 4000
	s := string(raw)
	if len(s) > max {
		s = s[:max] + "...(kısaltıldı)"
	}
	return fmt.Sprintf("[%s tool sonucu] %s", name, s)
}

// toolErrorText hatayı modele anlaşılır biçimde verir (yeniden denemesi söylenmez;
// Executor zaten geçici olanı geçici diye işaretledi ve denedi).
func toolErrorText(name string, err error) string {
	return fmt.Sprintf("[%s tool HATASI] %s. Kullanıcıya bu hatayı bildir veya başka bir yol dene.", name, err.Error())
}

// send tek bir olayı turu başlatan oturuma yollar (metin ve tool olayları AYNI emit'ten çıkar).
func (a *Agent) send(in Input, t protocol.ServerType, payload any) {
	emit := a.emitFor(in.ConversationID)
	if emit == nil {
		return
	}
	out := protocol.NewOutbound(t, in.ConversationID, payload)
	out.ReplyTo = in.RequestID
	emit(out)
}

// llmOptions üretim anındaki LLM parametrelerini döndürür.
// OptionsFunc verilmişse çağrılır (çalışma zamanı ayarları), yoksa statik Options kullanılır.
// Panic olursa üretim DURMAZ: statik değere düşer (named return sayesinde).
func (a *Agent) llmOptions() (opts llm.Options) {
	opts = a.cfg.Options
	if a.cfg.OptionsFunc == nil {
		return opts
	}
	defer func() {
		if r := recover(); r != nil {
			a.log.Error("ayarlar okunamadı, statik varsayılana düşüldü", "panic", r)
			opts = a.cfg.Options
		}
	}()
	return a.cfg.OptionsFunc()
}

// resolveModel, üretimde kullanılacak model adını belirler.
//
// Öncelik sırası:
//  1. İstekte AÇIKÇA verilen model (payload.model) — istemci geçici olarak geçersiz kılabilir.
//  2. Ayarlarda kayıtlı model (ModelFunc) — UI ayar ekranından seçilen model BUDUR.
//  3. Boş string: sağlayıcının kendi varsayılanı (Ollama: .env'deki OLLAMA_MODEL).
//
// Neden 2. adım şart? `protocol.TextMessagePayload.Model` sözleşmesi "boşsa sunucu
// varsayılanı" der ve UI boş gönderir. "Sunucu varsayılanı" ayardaki KAYITLI model
// olmalıdır; .env yalnızca AÇILIŞTAKİ başlangıç değeridir. Bu bağlanmadığı için
// ayar ekranından hangi model seçilirse seçilsin üretim muse-glimmer:30b ile
// yapılıyordu (canlı testte yakalandı).
//
// ModelFunc paniklerse üretim DURMAZ: 3. adıma düşülür (llmOptions ile aynı politika).
func (a *Agent) resolveModel(requested string) (model string) {
	model = strings.TrimSpace(requested)
	if model != "" || a.cfg.ModelFunc == nil {
		return model
	}
	defer func() {
		if r := recover(); r != nil {
			a.log.Error("model ayarı okunamadı, sağlayıcı varsayılanına düşüldü", "panic", r)
			model = ""
		}
	}()
	return strings.TrimSpace(a.cfg.ModelFunc())
}

// toolsEnabled, bu kullanıcıya çalıştırılabilir bir araç sunulup sunulmayacağını söyler.
// Hem sistem istemini hem de döngüyü bu BİR gerçeğe bağlıyoruz; ikisi ayrı hesaplanırsa
// "tool kuralları yazık ama araç yok" gibi tutarsız durumlar oluşur.
func (a *Agent) toolsEnabled(userID string) bool {
	if a.catalog == nil || a.runner == nil {
		return false
	}
	if _, ok := a.llm.(llm.ToolProvider); !ok {
		return false
	}
	return len(toolSpecs(a.catalog.Available(context.Background(), userID))) > 0
}

// turnPlan, bir turun "hangi bağlamla" üretileceğini belirler (SKILL → TOOL → LLM sırası).
//
// DÖNÜŞÜM YOK: retrieval boş dönerse veya hata verirse plan sıfır değerlidir ve sistem
// bugünkü davranışıyla (tüm izinli tool'lar, skillsiz istem) çalışır. Retrieval bir
// YAKINLIK ÖNERİSİDİR; üretimi hiçbir zaman engellememelidir.
type turnPlan struct {
	skills    []RetrievedSkill
	tools     []llm.ToolSpec
	knowledge []prompt.Passage // Faz 8: RAG alıntıları (tool durumundan bağımsız)

	// Faz 10: kalıcı anılar (tool durumundan bağımsız).
	memories []prompt.Remembered

	// Faz 4: ekip ortak hafızası bloğu (liste yönergesi ya da bilinen bilgiler); boşsa eklenmez.
	teamMemory string

	// userID, bu turun konuşulduğu kullanıcıdır. Sistem istemi kullanıcıya
	// ÖZEL olduğu için (profil kuralları) kimlik tur planında taşınır.
	userID string
}

func (p turnPlan) promptSkills() []prompt.Skill {
	out := make([]prompt.Skill, 0, len(p.skills))
	for _, s := range p.skills {
		out = append(out, prompt.Skill{Name: s.Name, Description: s.Description, Tool: s.Tool, Score: s.Score})
	}
	return out
}

// plan, kullanıcı mesajına göre skill ve tool seçimi yapar.
func (a *Agent) plan(ctx context.Context, userID, userMessage string) turnPlan {
	// userID HER ZAMAN taşınır: sistem istemi kullanıcıya özeldir (profil kuralları).
	// Aşağıdaki erken çıkışların hepsinde de korunmalıdır; bu yüzden her dal
	// kendi turnPlan'ında userID'yi yazar.
	p := turnPlan{userID: userID}
	if a.catalog == nil || a.runner == nil {
		return p
	}
	if _, ok := a.llm.(llm.ToolProvider); !ok {
		return p
	}
	// Katalog TAM alınır; kesme/seçim pickTools'tadır (ilgiye göre, alfabetik DEĞİL).
	all := a.catalog.Available(ctx, userID)
	if len(all) == 0 {
		return p
	}

	var skills []RetrievedSkill
	if a.skills != nil && strings.TrimSpace(userMessage) != "" {
		skills = selectSkills(a.skills.TopSkills(ctx, userMessage, MaxSkillsPerTurn*2))
	}
	// TOOL RETRIEVAL: skill'lerin işaret ettiği tool'lar + mesajla güçlü eşleşenler.
	// Skill yoksa tüm izinli tool'lar (en çok MaxToolSpecs, ilgiye göre sıralı).
	narrowed := pickTools(all, userMessage, toolsOfSkills(skills))
	if len(narrowed) != len(all) {
		names := make([]string, 0, len(skills))
		for _, s := range skills {
			names = append(names, s.Name)
		}
		a.log.Info("tool listesi seçildi",
			"skilller", names,
			"onceden", len(all), "sonra", len(narrowed), "tools", toolNames(narrowed))
	}
	p.skills = skills
	p.tools = narrowed
	return p
}

// promptEdits, yöneticinin istem düzenlemelerini okur (yoksa boş — kod varsayılanı).
//
// neden burada? Agent ayar KATMANINI bilmez (dependency inversion); main.go bir
// geri çağırma bağlar. Testlerde nil döner ve davranış kod varsayılanıyla aynı kalır.
func (a *Agent) promptEdits() (prompt.Overrides, prompt.Disabled) {
	if a.cfg.PromptEditsFunc != nil {
		over, dis := a.cfg.PromptEditsFunc()
		return over, dis
	}
	return nil, nil
}

// systemPrompt, modele araç sunulduğunda tool kurallarını, retrieval sonucu varsa
// seçilen skill'leri ekler. Tool'suz oturumda parçalar eklenmez: modele var olmayan
// yetenek anlatmak yanıltıcı olur.
func (a *Agent) systemPrompt(toolsEnabled bool, tp turnPlan) string {
	return a.systemPromptFor(context.Background(), tp.userID, toolsEnabled, tp)
}

// systemPromptFor, kullanıcı kimliğini de alan istem üreticisidir.
//
// Neden ayrı metot? systemPrompt (kimliksiz) testler ve bellek yolu için kalır;
// asıl yol kimlikle çalışır. Profil kuralları KULLANICIYA özeldir, bu yüzden
// kimlik olmadan profil eklenemez.
func (a *Agent) systemPromptFor(ctx context.Context, userID string, toolsEnabled bool, tp turnPlan) string {
	pb := a.prompt
	// Yönetici düzenlemeleri her turda yeniden okunur (sunucu yeniden başlatmasın).
	over, dis := a.promptEdits()
	if len(over) > 0 || len(dis) > 0 {
		pb = prompt.Base(over, dis)
	}
	if toolsEnabled {
		if p, ok := prompt.OptPart("tool-usage", over, dis, true); ok {
			pb = pb.With(p)
		}
		// Veri uydurmayı engelleyen kurallar tool'lar AÇIKKEN eklenir. Neden burada?
		// Şirket verisi yalnızca tool sonuçlarından gelir; tool sunulmıyorsa bu kuralın
		// konusu yoktur ve sistem istemi gereksiz şişer (bkz. NoFabrication yorumu).
		if p, ok := prompt.OptPart("no-fabrication", over, dis, true); ok {
			pb = pb.With(p)
		}
		if s := tp.promptSkills(); len(s) > 0 {
			pb = pb.With(prompt.SkillSelector(s))
		}
	}
	// Hafıza yeteneği, kayıt YAPILIP yapILMAMASINDAN BAĞIMSIZ her turda anlatılır.
	//
	// Neden? (Canlı hata) Kullanıcı "Hafızaya kaydet" dediğinde model "kalıcı saklayamıyorum"
	// diyordu: Memories() parçası yalnızca anı GERİ ÇAĞRILDIĞINDA ekleniyordu, yani tam da
	// yeni bilgi öğrenildiği anda model yeteneğini hiç bilmiyordu. Yetenek, öğrenmenin
	// kendisine bağlı olmamalıdır: anı boşken de model "saklayabilirim" demelidir.
	if a.memory != nil || a.learner != nil {
		if p, ok := prompt.OptPart("memory", over, dis, true); ok {
			pb = pb.With(p)
		}
	}
	if len(tp.memories) > 0 {
		pb = pb.With(prompt.Memories(tp.memories)) // tool'dan bağımsız: anılar her durumda sunulur
	}
	if len(tp.knowledge) > 0 {
		pb = pb.With(prompt.Knowledge(tp.knowledge)) // tool'dan bağımsız: belgeler her durumda sunulur
	}
	if tp.teamMemory != "" {
		pb = pb.With(prompt.TeamMemory(tp.teamMemory)) // tool'dan bağımsız: ortak hafıza her durumda sunulur
	}
	// YÖNETİCİ TANIMLI KULLANICI PROFİLLERİ.
	//
	// SIRA ÖNEMLİ: profiller, güvenlik kurallarından SONRA ve YETENEK
	// açıklamalarından SONRA eklenir. Böylece "veri uydurma" gibi kurallar
	// profile ezdirilemez; profil yalnız davranış tonunu/akışını yönlendirir.
	//
	// ETKİNLİK: profil yoksa eklenmez → istem şişmez, davranış değişmez.
	if profiles := a.PersonaProfiles(ctx, userID); len(profiles) > 0 {
		pb = pb.With(prompt.Personas(profiles))
	}
	return pb.Build(prompt.Context{Now: time.Now(), Timezone: a.cfg.Timezone})
}

// buildMessages sistem istemini ve geçmişi LLM mesajlarına çevirir, ardından bağlam
// yöneticisiyle opts'taki pencereye sığdırır.
// Boş içerikler ve hatayla biten asistan mesajları bağlama alınmaz.
func (a *Agent) buildMessages(history []conversation.Message, opts llm.Options, toolsEnabled bool, tp turnPlan) ctxmgr.Result {
	system := a.systemPromptFor(context.Background(), tp.userID, toolsEnabled, tp)
	msgs := make([]llm.Message, 0, len(history)+1)
	if system != "" {
		msgs = append(msgs, llm.Message{Role: llm.RoleSystem, Content: system})
	}
	for _, m := range history {
		if strings.TrimSpace(m.Content) == "" {
			continue
		}
		if m.Role == conversation.RoleAssistant && m.Status == conversation.StatusError {
			continue
		}
		msgs = append(msgs, llm.Message{Role: llm.Role(m.Role), Content: m.Content})
	}
	// Tool TANIMLARI da promptun bir parçasıdır (istekte "tools" alanında gider).
	// Yalnız mesajları sayarsak bütçe gerçekte aşılmış olur ve model cevabı ortasından
	// sessizce keser (canlı hata 05.10.2026: "...Hangi" diye biten mesajlar).
	// tp.tools bu turun sunulacak araçları zaten taşıyor; sıralı/kesilmiş HALİ sunulur.
	return a.ctxm.FitWithTools(msgs, tp.tools, opts)
}
