package agent

// prefetch.go: modelden BAĞIMSIZ, sunucu tarafında önceden çalıştırılan araçlar.
//
// CANLI HATA (04.10.2026): "dolar kuru ne" sorusunda TCMB'den USD satış 49,0582 geldi
// ama model aracı hiç çağırmadı ve kullanıcıya kuru söyleyemedi. Sebep iki katmanlıydı:
//   1. Araç seçimi (toolrank.go) uzun süre alfabetik kesme yapıyordu → araç sunulmuyordu.
//   2. Araç sunulsa bile küçük model tool çağrısı üretmeyebiliyor; "elimde bilgi yok"
//      deyip tahmin yürütüyor ya da "ulaşamıyorum" diyordu.
//
// Çözüm: bu tür SORU-KARARLIK veri isteyen ve UCUZ olan araçlar, modele sorulmadan
// önce SUNUCUDA çalıştırılır ve sonucu modele gerçek veri olarak verilir. Model artık
// kur SORMAK zorunda değildir; eline tutar verilmiş olur ve yalnızca cümleleştirir.
// Küçük model bile doğru cevabı verir.
//
// GÜVENLİK/ETİK: yalnızca SALT OKUNUR, YAN ETKİSİZ ve ucuz araçlar bu yola girer
// (şu an yalnızca web.exchange_rate). Yazma/silme araçları buraya ASLA eklenmez:
// onlar kullanıcı onayı ve model kararı gerektirir.

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"time"

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

// prefetchSpec, bir aracın ne zaman önceden çalıştırılacağını tanımlar.
type prefetchSpec struct {
	// Tool çalıştırılacak araç adı.
	Tool string
	// MinScore, mesajın bu araca ilgi puanı için gereken alt sınırdır (intentScores).
	// Düşük eşik yanlış tetiklemeye yol açar: kullanıcı "dolar" demeden çalıştırmamalı.
	MinScore int
	// Args, kullanıcı mesajından aracın girdisini türetir (nil = girdi gerektirmez).
	Args func(message string) map[string]any
}

// prefetches, önceden çalıştırılabilecek araçların listesi.
//
// NOT: Buraya yalnızca (a) okuma yapabilen, (b) dışarıya yazmayan, (c) ucuz ve
// (d) kullanıcı sorusunu DOĞRUDAN yanıtlayan araçlar eklenir. "Her şeyi ön çalıştır" hırsı
// prefetch, sunucu tarafında ön çalıştırma yapar ve sonucu modele geri besler.
//
// Kurallar:
//   - Araç bu kullanıcıya SUNULMUYORSA çalıştırılmaz (izin yoktur; sızma olur).
//   - Kullanıcının mesajı yeterince ilgili değilse çalıştırılmaz (trafik ve gecikme).
//   - Hata turu BOZMAZ: araç çalışmazsa model normal akışına devam eder.
//   - Sonuç hem isteme (kalıcı kayıt + arayüz rozeti) hem modele yazılır.
func (a *Agent) prefetch(ctx context.Context, in Input, st *runState, tools []llm.ToolSpec,
	conv []llm.Message, userMessage string) []llm.Message {

	if a.runner == nil || strings.TrimSpace(userMessage) == "" {
		return conv
	}
	scores := intentScores(userMessage)
	available := toolNameSet(tools)
	for _, spec := range prefetches {
		if !available[spec.Tool] || scores[spec.Tool] < spec.MinScore {
			continue
		}
		if err := ctx.Err(); err != nil {
			break
		}
		args := map[string]any{}
		if spec.Args != nil {
			if args = spec.Args(userMessage); len(args) == 0 {
				continue
			}
		}
		conv = a.prefetchOne(ctx, in, st, conv, spec.Tool, args)
	}
	return conv
}

// prefetchOne tek bir aracı çalıştırır, olaylarını yayar ve sonucu konuşmaya ekler.
func (a *Agent) prefetchOne(ctx context.Context, in Input, st *runState, conv []llm.Message,
	name string, args map[string]any) []llm.Message {

	// Aynı girdiyle daha önce çalıştıysak tekrar çalıştırma (aynı turda iki kez aynı soru).
	if prev, ok := st.duplicate(name, args); ok {
		return append(conv, llm.Message{Role: llm.RoleUser, Content: fmt.Sprintf(
			"[%s tool sonucu] %s", name, cachedText(prev.Output))})
	}

	callID := id.New("tc")
	a.send(in, protocol.ServerToolStart, protocol.ToolStartPayload{CallID: callID, Name: name, Input: args})
	started := time.Now()
	res, err := a.runner.Run(ctx, name, args, in.UserID, nil)
	dur := time.Since(started).Milliseconds()

	payload := protocol.ToolResultPayload{CallID: callID, Name: name, DurationMs: dur}
	record := ToolRun{CallID: callID, Name: name, Status: "success",
		Input: ToolPreview(args, ToolPreviewMax)}
	if err != nil {
		payload.Status, payload.Error, payload.DurationMs = "error", err.Error(), dur
		record.Status, record.Error = "error", err.Error()
		a.log.Warn("ön çalıştırma başarısız", "tool", name, "err", err, "conversation", in.ConversationID)
		a.publish(ctx, event.ToolExecuted, in, map[string]any{"tool": name, "result": "error", "error": err.Error()})
		a.send(in, protocol.ServerToolResult, payload)
		st.add(record)
		return conv // hata modele BESLENMEZ: turu bozmak, kullanıcıyı asıl sorudan koparmak demektir
	}

	payload.Output, payload.UI = res.Output, res.UI
	record.Output, record.UI, record.DurationMs = ToolPreview(res.Output, ToolPreviewMax), res.UI, dur
	a.send(in, protocol.ServerToolResult, payload)
	a.publish(ctx, event.ToolExecuted, in,
		map[string]any{"tool": name, "result": "success", "via": "prefetch", "durationMs": dur})
	st.add(record)
	st.remember(name, args, res)

	return append(conv, llm.Message{Role: llm.RoleUser, Content: prefetchNote(name, toolResultText(name, res.Output))})
}

// prefetchNote, önceden getirilen verinin modele anlatımıdır.
//
// Model küçük olduğu için "şu veri senin için getirildi, KENDİN ARAYIP BULMA, sadece
// kullanıcıya anlat" cümlesi şart: aksi halde model kendi ezberinden eski kur söyler.
func prefetchNote(name, result string) string {
	return fmt.Sprintf(
		"[%s — SUNUCU SENİN İÇİN ŞİMDİ ÇALIŞTIRDI, GEÇERLİ VE GÜNCEL VERİDİR]\n%s\n"+
			"Kullanıcıya bu değeri aktar. Başka kaynaktan/yatırımdan kur UYDURMA.",
		name, result)
}

// canlı hatadır: gecikme artar, kullanıcı "neden 3 saniye bekledim" diye sorar.
// ---- web.exchange_rate girdi türetme ----

// dövizSözcükleri, Türkçe para birimi adlarını araçtaki koda eşler (aksan katlanmış).
var dövizSözcükleri = map[string]string{
	"dolar": "USD", "usd": "USD", "amerikan dolari": "USD",
	"euro": "EUR", "eur": "EUR", "avro": "EUR",
	"sterlin": "GBP", "gbp": "GBP", "ingiliz sterlini": "GBP",
	"isvicre frangi": "CHF", "chf": "CHF",
	"japon yeni": "JPY", "jpy": "JPY",
	"kanada dolari": "CAD", "cad": "CAD",
	"avustralya dolari": "AUD", "aud": "AUD",
}

// exchangeRateArgs, kullanıcı mesajından döviz kuru aracının girdisini türetir.
//
// Neden "amount" da dener? "500 dolar kaç TL" sorusunda modelin tutarı çıkarması küçük
// modellerde en sık yapılan hatadır; tutarı sunucu vermek sonucu kesinleştirir.
func exchangeRateArgs(message string) map[string]any {
	folded := foldTR(message)
	words := wordsOf(folded)
	code := ""
	for _, w := range words {
		if c, ok := dövizSözcükleri[w]; ok {
			code = c
			break
		}
	}
	if code == "" {
		// "ABD doları" gibi iki sözcüklü adlar: katlanmış metinde alt dize olarak ara.
		for phrase, c := range dövizSözcükleri {
			if strings.Contains(folded, phrase) {
				code = c
				break
			}
		}
	}
	if code == "" {
		return nil
	}
	out := map[string]any{"currency": code}
	if amt, ok := firstAmount(folded); ok {
		out["amount"] = amt
	}
	return out
}

// firstAmount, mesajdaki ilk sayısal tutarı döndürür ("500 dolar kaç tl" → 500).
//
// Neden SÖZCÜK listesinden değil de ham metinden? wordsOf "1.500" ifadesini iki parçaya
// böler ("1", "500") ve ilk parça 1 sanılır — 1000 katı hata. Ham metin tarandığı için
// binlik ayraçlı yazım bozulmaz.
//
// Neden ilk sayı? "2 hafta önce 500 dolara aldım" cümlesinde amaçlanan genelde tutardır;
// tarih içeren sorularda araç zaten tarih bilgisiyle çalışmaz. Tutar yoksa HİÇ gönderilmez:
// araç kuru TL karşılığı olmadan da döndürür.
func firstAmount(folded string) (float64, bool) {
	for _, loc := range sayıBul.FindAllStringIndex(folded, -1) {
		if f, ok := toFloat(folded[loc[0]:loc[1]]); ok {
			return f, true
		}
	}
	return 0, false
}

// sayıBul, rakam + isteğe bağlı binlik/ondalık ayraçlı bir sayıyı bulur.
var sayıBul = regexp.MustCompile(`\d+(?:[.,]\d+)*`)
var prefetches = []prefetchSpec{
	{Tool: "web.exchange_rate", MinScore: 3, Args: exchangeRateArgs},
}
