// Package ctxmgr (Context manager, Phase 2) LLM'e gönderilecek mesaj listesini modelin bağlam
// penceresine SIĞDIRIR.
//
// Strateji (kayan pencere):
//  1. Sistem istemi her zaman korunur.
//  2. Son mesaj (az önce gelen kullanıcı mesajı) her zaman korunur; tek başına sığmazsa
//     ortası kırpılır (baş + son korunur), asla tamamen atılmaz.
//  3. Geri kalan geçmiş YENİDEN ESKİYE doğru doldurulur; sığmayan ilk mesajda durulur
//     (sürekli bir kuyruk korunur, ortadan mesaj atlanmaz — tutarlı bağlam).
//  4. Kırpılmış geçmiş asla asistan mesajıyla başlamaz (sohbet şablonları user ile başlar).
//  5. Mesaj atıldıysa sistem istemine kısa bir not eklenir; model "konuşmanın başını
//     göremediğini" bilir.
//
// Token sayımı tahminîdir (gerçek tokenizer yok). Tahmin kasıtlı olarak MUHAFAZAKÂRDIR
// (Türkçe için ~3 karakter/token): fazla tahmin etmek bağlamı biraz erken keser,
// eksik tahmin etmek ise modelin sessizce baştan kırpmasına yol açar — ikincisi daha kötüdür.
//
// Sonraki adım (Phase 10): atılan mesajların LLM ile özetlenip sistem istemine eklenmesi.
// Bu paket yalnızca kesme stratejisini sağlar.
package ctxmgr

import (
	"encoding/json"
	"strconv"
	"strings"

	"fiko-ai/internal/llm"
)

const (
	// DefaultWindow ContextSize 0 (sağlayıcı varsayılanı) iken varsayılan pencere (token).
	//
	// 4096 → 8192 → 131072 (canlı hatalar 05.10.2026):
	//   4096: 34 aracın tanımı (~2.2k token) pencerenin yarısını yiyordu; tarihçe
	//   için ~1.8k kalıyor, birkaç turda geçmiş atılıyordu → "AI her mesajda sıfırlanıyor".
	//   8192: araçlar + birkaç tur geçmiş sığıyordu ama uzun sohbetlerde yetersiz kaldı.
	//   131072 (128k): kullanıcı isteğiyle tam model penceresi kullanılır.
	//
	// NOT: Bu paket yalnızca HESAP yapar; gerçek pencere Ollama'ya CHAT_DEFAULT_
	// CONTEXT_TOKENS (config) ve OLLAMA_MAX_CONTEXT üzerinden gider. Buradaki
	// sabit o ikisi VERİLMEZSE kullanılır (testler, varsayılan kurulum).
	//
	// UYARI: Ollama num_ctx değerini modelin KV cache'ine birebir yansıtır. 128k
	// pencere, büyük modellerde (30B+) onlarca GB VRAM isteyebilir ve model
	// yüklenemeyebilir. VRAM sınırlıysa 16384–32768 güvenlidir.
	DefaultWindow = 131072
	// DefaultReserve MaxTokens 0 iken cevap için ayrılan token sayısı.
	DefaultReserve = 1024
	// MinWindow bu değerin altındaki pencere anlamsızdır; yukarı çekilir.
	MinWindow = 512

	perMessageOverhead = 4  // rol/ayraç maliyeti (token)
	minMargin          = 48 // güvenlik payı alt sınırı (token) — kırpma notu da buradan karşılanır
	clipMarker         = "\n[…]\n"
)

// Estimator bir metnin tahmini token sayısını döndürür.
type Estimator func(s string) int

// Config yöneticinin parametreleridir; sıfır değerler varsayılanla değiştirilir.
type Config struct {
	DefaultWindow  int       // ContextSize verilmediğinde kullanılacak pencere
	DefaultReserve int       // MaxTokens verilmediğinde cevaba ayrılacak pay
	Estimator      Estimator // nil ise EstimateTokens
}

// Manager eşzamanlı kullanıma güvenlidir (durum tutmaz).
type Manager struct {
	cfg Config
}

func New(cfg Config) *Manager {
	if cfg.DefaultWindow < MinWindow {
		cfg.DefaultWindow = DefaultWindow
	}
	if cfg.DefaultReserve < 1 {
		cfg.DefaultReserve = DefaultReserve
	}
	if cfg.Estimator == nil {
		cfg.Estimator = EstimateTokens
	}
	return &Manager{cfg: cfg}
}

// Result Fit çıktısıdır.
type Result struct {
	Messages        []llm.Message // LLM'e gönderilecek liste (girdi dilimi DEĞİŞTİRİLMEZ)
	Dropped         int           // bağlama sığmadığı için atılan geçmiş mesajı sayısı
	Clipped         bool          // son mesajın ortası kırpıldı mı
	EstimatedTokens int           // Messages için tahmini toplam
	Budget          int           // istem için ayrılan token bütçesi (tool maliyeti DÜŞÜLMÜŞ)
	Window          int           // kullanılan pencere (token)
	ToolTokens      int           // tool tanımlarının bütçeden düşülen tahmini maliyeti
}

// budget pencere ve cevaba ayrılan paya göre istem bütçesini hesaplar.
func (m *Manager) budget(opts llm.Options) (window, budget int) {
	window = opts.ContextSize
	if window <= 0 {
		window = m.cfg.DefaultWindow
	}
	if window < MinWindow {
		window = MinWindow
	}
	reserve := opts.MaxTokens
	if reserve <= 0 {
		reserve = m.cfg.DefaultReserve
	}
	// Cevap payı pencerenin yarısını geçemez; yoksa istem için yer kalmaz.
	if reserve > window/2 {
		reserve = window / 2
	}
	margin := window / 20 // %5
	if margin < minMargin {
		margin = minMargin
	}
	budget = window - reserve - margin
	if budget < 1 {
		budget = 1
	}
	return window, budget
}

// ToolTokenCost, bir tool tanımının (ad + açıklama + şema) prompta eklediği
// TAHMİNİ token maliyetidir.
//
// NAYDIR VAR? (canlı hata 05.10.2026) Tool tanımları isteğin "tools" alanında
// GÖNDERİLİR, mesaj listesinde değildir. Fit() yalnızca mesajları saydığı için bu
// maliyet bütçeye HİÇ girmiyordu. Sonuç: istem bütçeye göre "sığıyor" görünüyor,
// gerçekte num_ctx'i aşıyordu ve model cevabı ortasından sessizce kesiyordu
// ("...Hangi" diye biten mesajlar). Araç sayısı arttıkça hata büyüdü.
//
// NOT: Ollama ve OpenAI uyumlu sağlayıcılarda şema modelin kendi token
// biçimine dönüştürülür; bu tahmin (JSON metin olarak) MUHAFAZAKÂRDIR ve gerçek
// maliyetten büyüktür. Fazla saymak güvenlidir — mesaj geçmişini biraz daha erken
// kırpar; az saymak cevabın ortasından kesilmesine yol açar.
func ToolTokenCost(specs []llm.ToolSpec) int {
	if len(specs) == 0 {
		return 0
	}
	var sb strings.Builder
	for _, s := range specs {
		sb.WriteString(s.Name)
		sb.WriteByte('\n')
		sb.WriteString(s.Description)
		sb.WriteByte('\n')
		if len(s.InputSchema) > 0 {
			if b, err := json.Marshal(s.InputSchema); err == nil {
				sb.Write(b)
			}
		}
		sb.WriteByte('\n')
	}
	return EstimateTokens(sb.String())
}

// Fit msgs listesini opts'taki pencereye sığdırır. msgs başında (opsiyonel) system mesajları,
// sonunda bu turun kullanıcı mesajı bulunmalıdır. Boş girdi boş sonuç döndürür.
//
// tools verilirse onların maliyeti de bütçeden DÜŞÜLÜR: tanımlar promptun bir
// parçasıdır, sayılmazsa pencere aşılır ve model cevabı yarım bırakır (bkz.
// ToolTokenCost ve canlı hata 05.10.2026).
func (m *Manager) Fit(msgs []llm.Message, opts llm.Options) Result {
	return m.FitWithTools(msgs, nil, opts)
}

// FitWithTools, tool tanımlarının maliyetini de hesaba katan Fit'tir.
//
// BÜTÇE KURALI (canlı hata 05.10.2026, "her mesajda sıfırlanıyor"):
// Araç tanımları istem bütçesinden düşülür — sayılmazsa num_ctx aşılır ve model
// cevabı sessizce ortasından keser. Ancak TARI HÇE bu bedeli ödememelidir: önceki
// denemede araç maliyeti (~2.2k token) doğrudan bütçeden düşüldüğü için tarihçe
// alanı 6759 → 4526'ya (%33) düştü ve kısa sohbetlerde geçmiş atılır hâle geldi.
//
// Kullanıcı pencereyi SABİTLEMEDİYSE (opts.ContextSize == 0) araçların sığacağı
// kadar pencere büyütülür; böylece bütçe AZALMAZ, tarihçe korunur. Kullanıcı
// sabit bir pencere verdiyse o değere saygı duyulur ve araçlar bütçeden düşülür
// (gerekirse ajan.fitTools listeyi kısar).
func (m *Manager) FitWithTools(msgs []llm.Message, tools []llm.ToolSpec, opts llm.Options) Result {
	window, budget := m.budget(opts)
	toolCost := ToolTokenCost(tools)
	reserved := window - budget // rezerv + margin
	if toolCost > 0 && opts.ContextSize == 0 {
		need := toolCost + reserved
		if need > window {
			window = need
			budget = window - reserved // tarihçe bütçesi KORUNUR
		}
	} else if toolCost > 0 {
		budget -= toolCost
	}
	if budget < 1 {
		budget = 1
	}
	res := Result{Budget: budget, Window: window, ToolTokens: toolCost}
	if len(msgs) == 0 {
		return res
	}

	// 1) baştaki system mesajları
	nSys := 0
	for nSys < len(msgs) && msgs[nSys].Role == llm.RoleSystem {
		nSys++
	}
	system := msgs[:nSys]
	rest := msgs[nSys:]

	sysCost := 0
	for _, s := range system {
		sysCost += m.cost(s.Content)
	}
	if len(rest) == 0 {
		res.Messages = append([]llm.Message(nil), system...)
		res.EstimatedTokens = sysCost
		return res
	}

	// 2) son mesaj zorunlu; kalan bütçeye sığmazsa kırp.
	last := rest[len(rest)-1]
	lastBudget := budget - sysCost
	if floor := budget / 4; lastBudget < floor {
		lastBudget = floor // sistem istemi çok büyükse bile kullanıcıya yer bırak
	}
	lastCost := m.cost(last.Content)
	if lastCost > lastBudget {
		last.Content = m.clip(last.Content, lastBudget-perMessageOverhead)
		lastCost = m.cost(last.Content)
		res.Clipped = true
	}

	// 3) geçmişi yeniden eskiye doldur
	used := sysCost + lastCost
	older := rest[:len(rest)-1]
	keepFrom := len(older) // older[keepFrom:] korunur
	for i := len(older) - 1; i >= 0; i-- {
		c := m.cost(older[i].Content)
		if used+c > budget {
			break
		}
		used += c
		keepFrom = i
	}
	kept := older[keepFrom:]

	// 4) asistan mesajıyla başlama; user ile başla
	for len(kept) > 0 && kept[0].Role != llm.RoleUser {
		used -= m.cost(kept[0].Content)
		kept = kept[1:]
	}
	res.Dropped = len(older) - len(kept)

	// 5) çıktıyı kur (girdi dilimine dokunma)
	out := make([]llm.Message, 0, len(system)+len(kept)+2)
	out = append(out, system...)
	if res.Dropped > 0 {
		note := dropNote(res.Dropped)
		if n := len(out); n > 0 {
			out[n-1].Content += "\n\n" + note // out kopya dilim; system[i] değeri kopyalandı
		} else {
			out = append(out, llm.Message{Role: llm.RoleSystem, Content: note})
		}
		used += m.cfg.Estimator(note)
	}
	out = append(out, kept...)
	out = append(out, last)
	res.Messages = out
	res.EstimatedTokens = used
	return res
}

func dropNote(n int) string {
	return "[Bağlam notu: konuşmanın en eski " + strconv.Itoa(n) +
		" mesajı bağlam sınırı nedeniyle çıkarıldı. Gerekirse kullanıcıdan eksik bilgiyi tekrar iste.]"
}

// TotalCost, mesaj listesinin tahmini toplam token maliyetidir (rol yükü dahil).
func TotalCost(msgs []llm.Message) int {
	total := 0
	for _, m := range msgs {
		total += EstimateTokens(m.Content) + perMessageOverhead
	}
	return total
}

func (m *Manager) cost(s string) int { return m.cfg.Estimator(s) + perMessageOverhead }

// clip s'yi tahmini maxTokens'a sığacak şekilde kırpar: baş ve son korunur, ortası "[…]" olur.
func (m *Manager) clip(s string, maxTokens int) string {
	if maxTokens < 1 {
		maxTokens = 1
	}
	if m.cfg.Estimator(s) <= maxTokens {
		return s
	}
	runes := []rune(s)
	build := func(keep int) string {
		head := keep / 2
		tail := keep - head
		return string(runes[:head]) + clipMarker + string(runes[len(runes)-tail:])
	}
	lo, hi := 0, len(runes) // en büyük sığan keep
	for lo < hi {
		mid := (lo + hi + 1) / 2
		if m.cfg.Estimator(build(mid)) <= maxTokens {
			lo = mid
		} else {
			hi = mid - 1
		}
	}
	return build(lo)
}

// EstimateTokens muhafazakâr token tahminidir.
// ASCII ≈ 1/3 token, Latin-genişletilmiş/Kiril/Yunan (Türkçe ç ğ ı ö ş ü dahil) ≈ 1/2 token,
// CJK ve diğer geniş yazılar ≈ 1 token / karakter. Birim: 1/6 token.
func EstimateTokens(s string) int {
	if s == "" {
		return 0
	}
	units := 0
	for _, r := range s {
		switch {
		case r < 0x80:
			units += 2
		case r < 0x2E80:
			units += 3
		default:
			units += 6
		}
	}
	return (units + 5) / 6
}
