// tools.go ajanın tool çağırma döngüsünün gerektirdiği iki ARAYÜZÜ tanımlar.
//
// Neden arayüz? `agent` paketi `tool` paketini bilmez; gerçek uygulamalar
// cmd/server/tools.go bağlantısında (DI) takılır. Bu sayede ajan testleri dosya sistemi,
// izin veya Mongo olmadan çalışır.
package agent

import (
	"context"
	"encoding/json"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/llm"
)

// ToolResult, ToolRunner'ın döndürdüğü ANLIK çalıştırma sonucudur (kalıcı kayıt DEĞİL).
type ToolResult struct {
	Output     map[string]any
	DurationMs int64
	// UI çıktının arayüz görünümüdür (tool.UI). Boş olabilir; o durumda arayüz
	// "auto"yu JSON sanar. Ajan paketi tool paketini BİLMEZ; alan burada taşınır.
	UI string
}

// ToolRunner ajanın ihtiyaç duyduğu tool çalıştırma yeteneğidir.
// YETKİ KONTROLÜ UYGULAMANIN İÇİNDEDİR (tool.Executor): ajan yalnızca hata iletisini görür.
// Progress ara ilerleme bildirimi için kullanılır (nil olabilir).
type ToolRunner interface {
	Run(ctx context.Context, name string, args map[string]any, userID string, progress func(step string, percent int)) (ToolResult, error)
}

// ToolCatalog hangi tool'ların modele sunulacağını söyler.
// Kural: yalnızca ETKİN, ÇALIŞTIRILABİLİR ve kullanıcıya AÇIK tool'lar sunulur —
// modelin çağırdığı ama yetkisi olmayan bir tool hata olarak döner (403) ve döngü bozulur.
type ToolCatalog interface {
	Available(ctx context.Context, userID string) []llm.ToolSpec
}

// SkillRetriever, kullanıcı mesajına en uygun AKTİF skill'leri bulur (SKILL RETRIEVAL).
//
// Neden ayrı arayüz? `agent` paketi `skill` paketini bilmez (skill.ToolExecutor köprüsünde
// olduğu gibi). Gerçek uygulama cmd/server'da `skill.Service.Search` üzerinden takılır.
type SkillRetriever interface {
	TopSkills(ctx context.Context, query string, limit int) []RetrievedSkill
}

// RetrievedSkill, ajana giden ince skill görünümüdür (alan modelinin tamamı taşınmaz).
type RetrievedSkill struct {
	Name        string
	Description string
	Tool        string // executorType=tool ise bağlı olduğu tool adı; boşsa yalnızca bilgi
	Score       float64
}

// MaxSkillsPerTurn bir turda bağlama alınan en çok skill sayısıdır.
// Çok fazla skill istemi kalabalıklaştırır ve tool seçimini bozar; 3-5 yeterlidir.
const MaxSkillsPerTurn = 4

// SkillMinScore, seçilen skill'in asgari ilgililik skorudur.
// Düşük eşik, alakasız skill'lerin prompt'a sızmasına yol açar (ölçüm: %67 hit@1).
const SkillMinScore = 0.35

// selectSkills, retrieval sonucunu sınırlar: en fazla MaxSkillsPerTurn, eşiğin üstünde.
// Tool adı tekrar eden skill'ler elenir (aynı tool iki kez anlatılmamalı).
func selectSkills(all []RetrievedSkill) []RetrievedSkill {
	out := make([]RetrievedSkill, 0, MaxSkillsPerTurn)
	seenTool := map[string]bool{}
	for _, s := range all {
		if len(out) >= MaxSkillsPerTurn {
			break
		}
		if s.Score < SkillMinScore {
			continue
		}
		if s.Tool != "" {
			if seenTool[s.Tool] {
				continue
			}
			seenTool[s.Tool] = true
		}
		out = append(out, s)
	}
	return out
}

// toolsOfSkills, seçilen skill'lerin bağlı olduğu tool adlarını döndürür (TOOL RETRIEVAL).
// Sıra korunur, tekrar elenir: modele aynı tool iki kez sunulmaz.
func toolsOfSkills(skills []RetrievedSkill) []string {
	var out []string
	seen := map[string]bool{}
	for _, s := range skills {
		if s.Tool == "" || seen[s.Tool] {
			continue
		}
		seen[s.Tool] = true
		out = append(out, s.Tool)
	}
	return out
}

// filterTools, sunulan tool listesini skill'lerin işaret ettiği tool'lara indirger.
// Kural: BOŞ liste, hiçbir şeyi sınırlamaz (tüm izinli tool'lar sunulur).
// Böylece skill katalogu boşken ya da retrieval boş dönerken sistem eski davranışında kalır.
func filterTools(specs []llm.ToolSpec, allowed []string) []llm.ToolSpec {
	if len(allowed) == 0 {
		return specs
	}
	keep := make(map[string]bool, len(allowed))
	for _, n := range allowed {
		keep[n] = true
	}
	out := make([]llm.ToolSpec, 0, len(specs))
	for _, s := range specs {
		if keep[s.Name] {
			out = append(out, s)
		}
	}
	return out
}

// MaxToolSpecs modele gönderilecek en çok tool tanımıdır.
// KESME ALFABETİK DEĞİLDİR: hangi tool'ların sunulacağını toolrank.go (pickTools)
// ilgiye göre belirler. (Eskiden `list[:20]` ada göre sıralı kestiği için web.*,
// unit.*, text.* hiç sunulmuyordu.)
//
// 50 NEDEN? (05.10.2026) Katalog 34 araçtır; 14'lük sınırda 18 araç hiçbir mesajda
// sunulmuyordu ve kullanıcı "araçları bulamıyorum" diyordu. Sınır, KATALOGUN
// TAMAMINDAN büyük seçildi: artık pratikte hiçbir araç kesilmiyor, sıralama yalnızca
// "en ilgili önce" olmak için çalışıyor. Gerçek kısıt artık izinler/aktiflik
// (bkz. cmd/server agentCatalog.Available), araç sayısı değil.
//
// NOT: Modele 34 araç tanımı göndermek küçük yerel modellerde bağlamı şişirir.
// Zamanla katalog büyürse bu değer düşürülebilir; ancak düşürmek "araç görünmez"
// hatasına geri dönüştüğü için varsayılan olarak geniş tutulur.
const MaxToolSpecs = 50

// MaxIterationsCap tool döngüsü için MUTLAK üst sınırdır (spec: sonsuz döngü olmasın).
const MaxIterationsCap = 8

// ToolRun ajanın döngü sırasında biriktirdiği KALICI çalıştırma kaydıdır (execution tracking).
// Canlı rozet (tool.* olayları) kalıcı değildir; yeniden bağlanınca kaybolur.
// Bu kayıt mesajla birlikte saklanır (bkz. conversation.ToolRun).
type ToolRun = conversation.ToolRun

// runState bir turda yapılan tool çağrılarını toplar ve TEKRAR çağrıları engeller.
type runState struct {
	runs []ToolRun
	seen map[string]ToolResult // aynı araç + aynı girdi → önceki sonuç
	// confirms bu turda kullanıcıya gösterilen onay/bilgi formlarıdır (kalıcı kart olarak
	// asistan mesajıyla kaydedilir; bkz. conversation.ToolConfirm).
	confirms []conversation.ToolConfirm
}

// MaxConfirmsPerTurn tek cevapta kaydedilecek en çok form kartıdır (toolRuns sınırıyla aynı gerekçe).
const MaxConfirmsPerTurn = 16

// addConfirm formu kalıcı kart olarak ekler (aynı kimlik tekrar eklenmez, üst sınır uygulanır).
func (st *runState) addConfirm(c conversation.ToolConfirm) {
	if c.ConfirmID == "" || len(st.confirms) >= MaxConfirmsPerTurn {
		return
	}
	for _, e := range st.confirms {
		if e.ConfirmID == c.ConfirmID {
			return
		}
	}
	st.confirms = append(st.confirms, c)
}

// key, (araç adı + girdi) çiftini belirleyen sabit anahtardır.
// Girdi JSON'u olduğu gibi kullanılır: iki çağrı "anlamca" değil, METİNCE aynıysa eşleşir
// (determinist ve açıklanabilir; hash çakışması gibi konuşmayan bir belirsizlik yok).
func callKey(name string, args map[string]any) string {
	raw, err := json.Marshal(args)
	if err != nil {
		return name + "|?"
	}
	return name + "|" + string(raw)
}

// duplicate, bu çağrının aynı turda daha önce yapılıp yapılmadığını ve sonucu döndürür.
func (st *runState) duplicate(name string, args map[string]any) (ToolResult, bool) {
	if st.seen == nil {
		return ToolResult{}, false
	}
	r, ok := st.seen[callKey(name, args)]
	return r, ok
}

// remember başarılı bir sonucu saklar (tekrarları yakalamak için).
func (st *runState) remember(name string, args map[string]any, r ToolResult) {
	if st.seen == nil {
		st.seen = map[string]ToolResult{}
	}
	st.seen[callKey(name, args)] = r
}

// add kaydı listeye ekler (üst sınır uygulanır).
func (st *runState) add(r ToolRun) {
	if len(st.runs) >= MaxToolRunsPerTurn {
		return // aşırı kayıt üretmek istemciyi/monitörü boğar; döngü sınırı zaten var
	}
	st.runs = append(st.runs, r)
}

// MaxToolRunsPerTurn tek cevapta kaydedilecek en çok tool çalıştırmasıdır.
const MaxToolRunsPerTurn = 16

// ToolPreviewMax, kalıcı kayda giren tool girdi/çıktısının en büyük JSON boyutudur (bayt).
// 2 kB: kullanıcı gösterebilsin ama mesaj şişmesin.
const ToolPreviewMax = 2000

// ToolPreview, arayüzde gösterilecek tool girdi/çıktısını HAZIRLAR.
// Tool çıktısı dosya içeriği gibi çok büyük olabilir; kayda giren her değer sınırlanır.
// Modele geri beslenen metin bu KISALTILMIŞ hali DEĞİLDİR (toolResultText kendi sınırını kullanır).
func ToolPreview(v map[string]any, maxBytes int) map[string]any {
	if len(v) == 0 {
		return nil
	}
	raw, err := json.Marshal(v)
	if err != nil {
		return map[string]any{"_error": "çıktı okunamadı"}
	}
	if len(raw) <= maxBytes {
		return v
	}
	// Çıktıyı olduğu gibi saklamak hem Mongo'yu şişirir hem istemciye onlarca KB gönderir.
	// Kullanıcıya "bir şey döndü, ancak büyük" bilgisini veriyoruz.
	return map[string]any{
		"_truncated": true,
		"_size":      len(raw),
		"_preview":   string(raw[:maxBytes/2]) + "…",
	}
}

// toolSpecs katalogdaki tool'ları sınırlar.
func toolSpecs(list []llm.ToolSpec) []llm.ToolSpec {
	if len(list) > MaxToolSpecs {
		return list[:MaxToolSpecs]
	}
	return list
}
