package agent

// args.go: modelin BOZUK gönderdiği tool argümanlarını aracın şemasına göre düzeltir.
//
// Neden var? (Canlı gözlem 04.10.2026) Küçük yerel modeller şemaya uymayan argüman
// yolluyor: sayı gerekirken "5" (metin), boolean gerekirken "true" (metin), nesne
// gerekirken '{"a":1}' (metin), dosya yolunu "/notlar/x.txt" ya da
// "data/tools/notlar/x.txt" biçiminde. Hepsi düzeltilebilir ve düzeltmek DOĞRU davranış:
// kullanıcı "projemi sil" dedi, model ref'i bulamadı, kullanıcı "500 TL ekle" dedi,
// model 500'ü metin sanıp hesaplayamadı. Sessizce düzeltip çalıştırmak her ikisinden
// iyidir; düzeltilemiyorsa AÇIKÇA hata döner (araç zaten doğrulayacak).
//
// GÜVENLİK: yalnızca TİP dönüşümü yapılır, değer YOK sayılmaz/değiştirilmez.
// Şeması bilinmeyen alanlara dokunulmaz.

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

	"fiko-ai/internal/llm"
)

// normalizeArgs, verilen tool şemasına göre argüman haritasını düzeltir.
// Hiçbir şey değişmezse girdinin KENDİSİ döner (yeni harita üretilmez).
func normalizeArgs(schema map[string]any, args map[string]any) map[string]any {
	if len(args) == 0 || len(schema) == 0 {
		return args
	}
	props, _ := schema["properties"].(map[string]any)
	if len(props) == 0 {
		return args
	}
	out := make(map[string]any, len(args))
	changed := false
	for k, v := range args {
		prop, _ := props[k].(map[string]any)
		if prop == nil {
			out[k] = v
			continue
		}
		if fixed, ok := coerceValue(prop, v); ok {
			out[k], changed = fixed, true
			continue
		}
		out[k] = v
	}
	if !changed {
		return args
	}
	return out
}

// coerceValue, tek bir alanı şema tipine göre düzeltir.
//
// DÖNÜŞÜM GEREKMİYORSA false döner: değer zaten doğru tipteyse dokunulmaz. Bu sayede
// normalizeArgs yalnızca GERÇEKTEN bozuk gönderilen alanlarda yeni harita üretir
// (gereksiz kopya, gereksiz ToolRun kaydı ve olay önizlemesi olmaz).
func coerceValue(prop map[string]any, v any) (any, bool) {
	switch ty, _ := prop["type"].(string); ty {
	case "number", "integer":
		switch v.(type) {
		case float64, float32, int, int64, json.Number:
			return nil, false // zaten sayı
		}
		f, ok := toFloat(v)
		if !ok {
			return nil, false
		}
		if ty == "integer" {
			return int(f), true
		}
		return f, true
	case "boolean":
		if _, ok := v.(bool); ok {
			return nil, false // zaten boolean
		}
		if s, ok := v.(string); ok {
			switch strings.ToLower(strings.TrimSpace(s)) {
			case "true", "evet", "yes", "on", "var", "1":
				return true, true
			case "false", "hayır", "hayir", "no", "off", "yok", "0":
				return false, true
			}
			return nil, false
		}
		if f, ok := toFloat(v); ok { // 1 / 0
			return f != 0, true
		}
		return nil, false
	case "object":
		if _, ok := v.(map[string]any); ok {
			return nil, false // zaten nesne
		}
		if s, ok := v.(string); ok {
			var m map[string]any
			if err := json.Unmarshal([]byte(strings.TrimSpace(s)), &m); err == nil && m != nil {
				return m, true
			}
		}
		return nil, false
	case "array":
		if _, ok := v.([]any); ok {
			return nil, false // zaten dizi
		}
		if s, ok := v.(string); ok {
			var arr []any
			if err := json.Unmarshal([]byte(strings.TrimSpace(s)), &arr); err == nil && arr != nil {
				return arr, true
			}
		}
		return nil, false
	case "string":
		if _, ok := v.(string); ok {
			return nil, false // zaten metin
		}
		if f, ok := toFloat(v); ok {
			return strconv.FormatFloat(f, 'f', -1, 64), true
		}
		if b, ok := v.(bool); ok {
			return strconv.FormatBool(b), true
		}
	}
	return nil, false
}

// toFloat, metin/sayı değerlerini float64'a çevirir; yapamazsa false döner.
//
// Türkçe yazımda binlik ayracı NOKTA, ondalık ayracı VİRGÜLdür ("1.234,56").
// Model İngilizce yazım gönderdiğinde ("1,234.56") virgülü noktaya çevirmek yanlış olurdu;
// bu yüzden yalnızca virgülle AYRIŞAN iki haneli kısım varsa virgül ondalık kabul edilir.
func toFloat(v any) (float64, bool) {
	switch x := v.(type) {
	case float64:
		return x, true
	case float32:
		return float64(x), true
	case int:
		return float64(x), true
	case int64:
		return float64(x), true
	case json.Number:
		if f, err := x.Float64(); err == nil {
			return f, true
		}
		return 0, false
	case string:
		return parseLooseFloat(strings.TrimSpace(x))
	}
	return 0, false
}

// parseLooseFloat, insan yazımındaki sayıları tolere eder.
//
// AYRAÇ KURALI (belirsizlik burada çözülür): iki ayraç varsa hangisi SON ise odur
// ondalıktır — "1.234,56" → 1234.56 (Türkçe), "1,234.56" → 1234.56 (İngilizce).
// Tek ayraç varsa: virgülden sonra TAM 3 hane varsa binlik ayracıdır ("1.500" → 1500),
// değilse ondalıktır ("1,50" → 1.5). Nokta için aynı varsayım uygulanır çünkü bu
// sistem Türkçe konuşur ve para tutarları "1.500" biçiminde yazılır.
func parseLooseFloat(s string) (float64, bool) {
	if s == "" {
		return 0, false
	}
	clean := strings.ReplaceAll(s, " ", "")
	clean = strings.ReplaceAll(clean, "%", "")
	lastDot, lastComma := strings.LastIndex(clean, "."), strings.LastIndex(clean, ",")
	switch {
	case lastDot >= 0 && lastComma >= 0:
		if lastComma > lastDot { // virgül ondalık
			clean = strings.ReplaceAll(clean, ".", "")
			clean = strings.ReplaceAll(clean, ",", ".")
		} else { // nokta ondalık
			clean = strings.ReplaceAll(clean, ",", "")
		}
	case lastComma >= 0:
		clean = joinIfThousands(clean[:lastComma], clean[lastComma+1:])
	case lastDot >= 0:
		clean = joinIfThousands(clean[:lastDot], clean[lastDot+1:])
	}
	f, err := strconv.ParseFloat(clean, 64)
	if err != nil {
		return 0, false
	}
	return f, true
}

// joinIfThousands, ondalık kesir tam üç haneliyse ayracı binlik ayracı sayar (yoksa ondalık).
func joinIfThousands(intPart, frac string) string {
	if len(frac) == 3 && len(intPart) > 0 && len(intPart) <= 3 {
		return intPart + frac
	}
	return intPart + "." + frac
}

// normalizePath, modelin yazdığı dosya yolunu araçların beklediği KÖKE GÖRELİ biçime getirir.
//
// Neden? fs.* araçları kök dışına çıkan yolu reddeder; küçük modeller mutlak yol ("/notlar/x.txt")
// ya da kökün kendisini ("data/tools/notlar/x.txt") yazıyor. Reddedilen yol "araç çalışmıyor"
// gibi görünüyor. Kök öneki TOOLS.go'ndaki varsayılandır; kurulum değiştirirse de göreli yol
// doğru çalışır, en kötü hâlde kullanıcıdan tam yol istenir.
func normalizePath(p string) string {
	s := strings.TrimSpace(strings.ReplaceAll(p, "\\", "/"))
	for _, prefix := range []string{"data/tools/", "./data/tools/"} {
		if strings.HasPrefix(s, prefix) {
			s = strings.TrimPrefix(s, prefix)
			break
		}
	}
	s = strings.TrimPrefix(s, "./")
	s = strings.TrimPrefix(s, "/")
	return strings.TrimSpace(s)
}

// toolSchemaOf, sunulan araç listesinde aracın giriş şemasını bulur (yoksa nil).
//
// Neden liste? Argüman düzeltmesi ŞEMAYA bakar (bkz. normalizeArgs). Sunulmayan bir araç
// çağrıldığında şema bilinmez ve girdi olduğu gibi gider — bu doğrudur: gizli aracın
// şemasını tahmin etmek, hatalı dönüşüm yapmaktan kötüdür.
func toolSchemaOf(tools []llm.ToolSpec, name string) map[string]any {
	for _, t := range tools {
		if t.Name == name {
			return t.InputSchema
		}
	}
	return nil
}

// normalizeArgsInPlace, araç adına göre ek düzeltmeler yapar: dosya yolu olan araçlarda
// `path` alanı göreli biçime getirilir. Şema bilinmiyorsa bile çalışır.
func normalizeArgsInPlace(name string, schema map[string]any, args map[string]any) map[string]any {
	out := normalizeArgs(schema, args)
	if strings.HasPrefix(name, "fs.") {
		if p, ok := out["path"].(string); ok {
			if fixed := normalizePath(p); fixed != p {
				cp := make(map[string]any, len(out))
				for k, v := range out {
					cp[k] = v
				}
				cp["path"] = fixed
				return cp
			}
		}
	}
	return out
}
