// util.go, kullanıcıya değer katan VE güvenli (ağ/yan etki yok) tool'ları toplar:
// fs.list, fs.search, fs.stat, calc.eval, text.stats.
//
// Neden ayrı dosya? Bunlar çekirdek fs.* tool'larının "kör noktasını" kapatır:
// read_text var ama dosya YOLUNU bilmek gerekiyor — yani `ls`/`grep` yoktu.
// Hepsi düşük risklidir; yalnızca read/execute izni ister ve hiçbir şeyi DEĞİŞTİRMEZ.
//
// Değişmez ilke: hiçbir tool kendi başına izin almaz; yetki kararı her zaman
// Executor'a aittir.
package builtin

import (
	"context"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"fiko-ai/internal/tool"
)

// maxWalkEntries gezinme sırasında işlenecek en çok girdi. Sonsuz döngüye ve bellek
// tükenmesine karşı ÜST SINIR: "tümünü tara" denirse sonuç kesilir ve kesildiği
// kullanıcıya açıkça söylenir (sessiz veri kaybı olmaz).
const maxWalkEntries = 5000

// uiEntries/fs.search/fs.stat çıktıları satır listeleridir; hesap ve metin
// araçları ise tek satırlık anahtar-değer özetidir.

// fsDefinitions, dosya sistemi tool'larının tanımlarını üretir.
//
// Üçü de aynı sandbox kuralına uyar: yol her zaman resolvePath ile kök altında
// çözülür ve KÖK DIŞI çıkış reddedilir (fs.read_text ile aynı güvenlik sözleşmesi).
func fsDefinitions(o Options) []Def {
	return []Def{
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fs.list",
				Title:       "Klasörü listele",
				Description: "TOOL_FS_ROOT altındaki bir klasörün içeriğini listeler. 'ls' karşılığıdır; bir dosyanın yolunu bilmeden önce bunu çağır.",
				Tags:        []string{"fs"},
				Risk:        tool.RiskLow,
				Permissions: []string{"read"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"path", str("Köke göreli klasör yolu; boşsa kökün kendisi"),
						"includeHidden", obj("type", "boolean", "description", "true ise . ile başlayanlar da listelenir", "default", false),
						"sortBy", obj("type", "string", "description", "name | size | modified", "default", "name", "enum", []any{"name", "size", "modified"}),
					),
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"path", str("Listelenen klasör"),
					"entries", obj("type", "array", "items", obj("type", "object")),
					"count", obj("type", "integer"),
				)),
				UI:        tool.UITable,
				TimeoutMs: 10_000,
			},
			Handler: listHandler(o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fs.search",
				Title:       "Dosya ara",
				Description: "TOOL_FS_ROOT altında dosya adı deseni ya da dosya içeriğinde regex arar. 'grep -r' karşılığıdır.",
				Tags:        []string{"fs"},
				Risk:        tool.RiskLow,
				Permissions: []string{"read"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"query", str("Arama metni (dosya adında geçer) veya içerik deseni"),
						"path", str("Arama yapılacak alt klasör; boşsa tüm kök"),
						"contentRegex", obj("type", "boolean", "description", "true ise query dosya İÇERİĞİNDE regex olarak aranır", "default", false),
						"limit", obj("type", "integer", "description", "En çok kaç sonuç", "default", 50),
					),
					"required", []any{"query"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"matches", obj("type", "array", "items", obj("type", "object")),
					"count", obj("type", "integer"),
				)),
				UI:        tool.UITable,
				TimeoutMs: 20_000,
			},
			Handler: searchHandler(o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name: "fs.stat", Title: "Dosya bilgisi",
				Description: "Bir dosyanın veya klasörün boyutu, türü ve değiştirilme zamanını döndürür (içeriğini okumaz).",
				Tags:        []string{"fs"},
				Risk:        tool.RiskLow,
				Permissions: []string{"read"},
				InputSchema: obj("type", "object",
					"properties", obj("path", str("Köke göreli yol")),
					"required", []any{"path"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"path", str("Yol"),
					"exists", obj("type", "boolean"),
				)),
				UI:        tool.UIForm,
				TimeoutMs: 5_000,
			},
			Handler: statHandler(o),
		},
	}
}

// calcDefinitions, güvenli aritmetik ve metin araçlarını üretir.
//
// calc.eval BİLEREK eval benzeri bir kaçış yolu kullanmaz: ifade kendi özyinelemeli
// çözümleyicisiyle ayrıştırılır. Böylece "tool" yüzeyi görünse de hiçbir Go/Python
// kodu çalıştırılamaz — yalnızca sayı üretilir.
func calcDefinitions(_ Options) []Def {
	return []Def{
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "calc.eval",
				Title:       "Hesapla",
				Description: "Aritmetik ifadeyi hesaplar (+ - * / ^ %, parantez; sqrt/log/sin/cos/min/max/round/ceil/floor/abs). Değişken dışında KOD ÇALIŞTIRMAZ.",
				Tags:        []string{"calc"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"expression", str("Örn. (1250*3)/4 + sqrt(16)"),
						"variables", obj("type", "object", "description", "İfade içindeki ada karşılık gelen sayılar (örn. {\"fiyat\": 25})"),
					),
					"required", []any{"expression"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"expression", str("Hesaplanan ifade"),
					"result", obj("type", "number"),
				)),
				UI:        tool.UIForm,
				TimeoutMs: 5_000,
			},
			Handler: evalHandler,
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "text.stats",
				Title:       "Metin istatistiği",
				Description: "Metnin karakter, kelime, satır, cümle sayısını ve tahmini okuma süresini verir; en sık kelimeleri listeler.",
				Tags:        []string{"text"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"text", str("İncelenecek metin"),
						"topWords", obj("type", "integer", "description", "En sık kaç kelime listelensin", "default", 10),
					),
					"required", []any{"text"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"characters", obj("type", "integer"),
					"words", obj("type", "integer"),
					"lines", obj("type", "integer"),
				)),
				UI:        tool.UIForm,
				TimeoutMs: 5_000,
			},
			Handler: textStatsHandler,
		},
	}
}

// listHandler bir klasörün içeriğini listeler (hiçbir yol kök dışına çıkamaz).
// Gizli girdiler varsayılan olarak gizlenir: ajanın gürültüyle uğraşmaması için.
func listHandler(o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		rel, _ := in["path"].(string)
		full, ok := resolvePath(o.fsRoot(), rel)
		if !ok {
			return nil, Fail(t.Name, "yol kök dışına çıkıyor: %q", rel)
		}
		dirEntries, err := os.ReadDir(full)
		if err != nil {
			return nil, Fail(t.Name, "klasör okunamadı: %s", baseErr(err))
		}
		includeHidden, _ := in["includeHidden"].(bool)
		sortBy, _ := in["sortBy"].(string)

		items := make([]os.FileInfo, 0, len(dirEntries))
		isDir := make(map[string]bool, len(dirEntries))
		for _, de := range dirEntries {
			if !includeHidden && strings.HasPrefix(de.Name(), ".") {
				continue
			}
			fi, err := de.Info()
			if err != nil {
				continue // erişilemeyen girdi: sessizce atla, tüm listeyi bozma
			}
			items = append(items, fi)
			isDir[fi.Name()] = de.IsDir()
		}

		sort.SliceStable(items, func(i, j int) bool {
			a, b := items[i], items[j]
			// Klasörler her zaman önce: kullanıcı "burada ne var" diye sordu.
			if isDir[a.Name()] != isDir[b.Name()] {
				return isDir[a.Name()]
			}
			switch sortBy {
			case "size":
				if a.Size() != b.Size() {
					return a.Size() < b.Size()
				}
			case "modified":
				if !a.ModTime().Equal(b.ModTime()) {
					return a.ModTime().Before(b.ModTime())
				}
			}
			return strings.ToLower(a.Name()) < strings.ToLower(b.Name())
		})

		out := make([]map[string]any, 0, len(items))
		for _, fi := range items {
			kind := "dosya"
			if isDir[fi.Name()] {
				kind = "klasör"
			}
			out = append(out, map[string]any{
				"name":     fi.Name(),
				"kind":     kind,
				"path":     filepath.ToSlash(filepath.Join(strings.TrimSpace(rel), fi.Name())),
				"size":     fi.Size(),
				"modified": fi.ModTime().UTC().Format("2006-01-02 15:04"),
			})
		}
		return map[string]any{
			"path":    filepath.ToSlash(strings.TrimSpace(rel)),
			"entries": out,
			"count":   len(out),
		}, nil
	}
}

// searchHandler kökün altında dosya adı veya içerik arar (grep -r karşılığı).
//
// İki mod vardır: varsayılan dosya ADINDA arama (büyük/küçük harf duyarsız),
// contentRegex=true ise dosya İÇERİĞİNDE regex arama.
//
// Sağlamlık: içerik araması yalnızca sınırlı boyutlu ve geçerli UTF-8 olan dosyaları
// okur; ikili dosyalar atlanır. Gezinme maxWalkEntries ile sınırlıdır.
func searchHandler(o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		query, _ := in["query"].(string)
		query = strings.TrimSpace(query)
		if query == "" {
			return nil, Fail(t.Name, "query boş olamaz")
		}
		byContent, _ := in["contentRegex"].(bool)

		var re *regexp.Regexp
		if byContent {
			// Hatalı regex anlaşılır hata döndürür (panic değil).
			var err error
			if re, err = regexp.Compile(query); err != nil {
				return nil, Fail(t.Name, "geçersiz regex: %s", err)
			}
		}

		// "path" opsiyoneldir: boş bırakılırsa TÜM KÖK taranır.
		sub, _ := in["path"].(string)
		if strings.TrimSpace(sub) == "" {
			sub = "."
		}
		base, ok := resolvePath(o.fsRoot(), sub)
		if !ok {
			return nil, Fail(t.Name, "yol kök dışına çıkıyor: %q", sub)
		}
		limit := 50
		if v, ok := toInt(in["limit"]); ok && v > 0 {
			limit = v
		}
		if limit > 500 {
			limit = 500
		}

		matches := []map[string]any{}
		truncated := false
		visited := 0
		needle := strings.ToLower(query)

		// WalkDir sembolik bağlantılara otomatik inmez; yine de kök dışına kaçış
		// savunma katmanı olarak denetlenir.
		err := filepath.WalkDir(base, func(p string, d os.DirEntry, err error) error {
			if err != nil {
				return nil // erişilemeyen alt ağaç: atla, taramayı durdurma
			}
			visited++
			if visited > maxWalkEntries {
				truncated = true
				return filepath.SkipAll
			}
			if _, inRoot := resolvePath(o.fsRoot(), relFrom(o.fsRoot(), p)); !inRoot {
				return filepath.SkipDir
			}
			if d.IsDir() {
				return nil
			}

			rel := relFrom(o.fsRoot(), p)
			found := strings.Contains(strings.ToLower(d.Name()), needle)
			lineNo, lineText := 0, ""

			if !found && byContent && re != nil {
				if data, ok := readSmall(p, o.maxRead()); ok {
					for i, ln := range strings.Split(string(data), "\n") {
						if re.MatchString(ln) {
							found, lineNo = true, i+1
							lineText = cap(strings.TrimSpace(ln), 200)
							break
						}
					}
				}
			}
			if found {
				if len(matches) >= limit {
					truncated = true
					return filepath.SkipAll
				}
				row := map[string]any{"path": filepath.ToSlash(rel), "name": d.Name()}
				if fi, ferr := d.Info(); ferr == nil {
					row["size"] = fi.Size()
					row["modified"] = fi.ModTime().UTC().Format("2006-01-02 15:04")
				}
				if byContent {
					row["line"] = lineNo
					row["match"] = lineText
				}
				matches = append(matches, row)
			}
			return nil
		})
		if err != nil {
			return nil, Fail(t.Name, "arama başarısız: %s", baseErr(err))
		}
		if truncated {
			// Kesildiğini AÇIKÇA bildir: kullanıcı eksik sonuçları bilmelidir.
			matches = append(matches, map[string]any{
				"name": fmt.Sprintf("(sonuçlar sınırlandı, ~%d girdi tarandı)", visited),
			})
		}
		return map[string]any{"matches": matches, "count": len(matches)}, nil
	}
}

// statHandler dosya/klasör meta verisini döndürür. Var OLMAYAN yol hata değildir:
// "bu dosya var mı" sorusu meşrudur, exists=false döner.
func statHandler(o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		rel, _ := in["path"].(string)
		full, ok := resolvePath(o.fsRoot(), rel)
		if !ok {
			return nil, Fail(t.Name, "yol kök dışına çıkıyor: %q", rel)
		}
		fi, err := os.Stat(full)
		if err != nil {
			if os.IsNotExist(err) {
				return map[string]any{"path": filepath.ToSlash(rel), "exists": false}, nil
			}
			return nil, Fail(t.Name, "dosya bilgisi alınamadı: %s", baseErr(err))
		}
		kind := "dosya"
		if fi.IsDir() {
			kind = "klasör"
		}
		return map[string]any{
			"path":     filepath.ToSlash(rel),
			"exists":   true,
			"kind":     kind,
			"size":     fi.Size(),
			"modified": fi.ModTime().UTC().Format("2006-01-02 15:04:05"),
		}, nil
	}
}

// readSmall dosyayı sınırlı okur. Başarısız/ikili (geçersiz UTF-8) veya çok büyük
// dosyalar için false döner; içerik araması bunları sessizce atlar.
func readSmall(path string, max int64) ([]byte, bool) {
	f, err := os.Open(path)
	if err != nil {
		return nil, false
	}
	defer f.Close()
	fi, err := f.Stat()
	if err != nil || fi.IsDir() || fi.Size() > max {
		return nil, false
	}
	data, err := io.ReadAll(io.LimitReader(f, max+1))
	if err != nil || int64(len(data)) > max || !utf8.Valid(data) {
		return nil, false
	}
	return data, true
}

// relFrom, tam yolu köke göreli yapar (kök dışındaysa taban adına düşer).
func relFrom(root, full string) string {
	if r, err := filepath.Rel(root, full); err == nil && r != "." && !strings.HasPrefix(r, "..") {
		return r
	}
	return filepath.Base(full)
}

func toInt(v any) (int, bool) {
	switch n := v.(type) {
	case float64:
		return int(n), true
	case int:
		return n, true
	}
	return 0, false
}

// ---------- calc.eval: güvenli ifade çözümleyicisi ----------

// maxParseDepth iç içe parantez sınırı: aşırı derin ifade stack taşması yerine
// anlaşılır hata üretsin.
const maxParseDepth = 64

// evalHandler aritmetik ifadeyi hesaplar. GÜVENLİK: ifade Python/Go DEĞİLDİR ve
// asla öyle yorumlanmaz — aşağıdaki özyinelemeli çözümleyici yalnızca sayıları,
// İZİNLİ fonksiyon adlarını ve tanımlı değişkenleri kabul eder.
func evalHandler(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
	expr, _ := in["expression"].(string)
	if strings.TrimSpace(expr) == "" {
		return nil, Fail(t.Name, "expression boş olamaz")
	}
	vars := map[string]float64{}
	if m, ok := in["variables"].(map[string]any); ok {
		for k, v := range m {
			n, ok := toFloat(v)
			if !ok {
				return nil, Fail(t.Name, "değişken %q sayı değil", k)
			}
			vars[strings.TrimSpace(k)] = n
		}
	}
	p := &parser{src: expr, vars: vars, toolName: t.Name}
	val, err := p.parseExpr()
	if err != nil {
		return nil, err
	}
	p.skipSpace()
	if p.pos != len(p.src) {
		return nil, Fail(t.Name, "anlaşılmayan karakter: %q", p.src[p.pos])
	}
	if math.IsNaN(val) || math.IsInf(val, 0) {
		return nil, Fail(t.Name, "sonuç geçerli bir sayı değil (sıfıra bölme?)")
	}
	// Yuvarlak: 0.1+0.2 gibi kayan nokta artıklarını gizle.
	return map[string]any{"expression": expr, "result": roundClean(val)}, nil
}

func roundClean(f float64) float64 {
	// 15 anlamli basamak: float64 gosteriminin gercek hassasiyeti.
	// (1e10 ile yuvarlamak 1/3 -> 0.3333333333 kesiyordu, 1e15 hem
	// kirli kayan nokta artiklarini giderir hem sonucu bozmaz.)
	r := math.Round(f*1e15) / 1e15
	if r == 0 {
		return 0 // -0 JSON çıktısında "-0" görünmesin
	}
	return r
}

type parser struct {
	src      string
	pos      int
	vars     map[string]float64
	toolName string
	depth    int
}

func (p *parser) fail(format string, args ...any) error {
	return Fail(p.toolName, format, args...)
}

func (p *parser) skipSpace() {
	for p.pos < len(p.src) {
		ch := p.src[p.pos]
		if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
			return
		}
		p.pos++
	}
}

// parseExpr, toplama/çıkarma (en düşük öncelik).
func (p *parser) parseExpr() (float64, error) {
	p.depth++
	if p.depth > maxParseDepth {
		return 0, p.fail("ifade çok derin iç içe")
	}
	defer func() { p.depth-- }()

	val, err := p.parseTerm()
	if err != nil {
		return 0, err
	}
	for {
		p.skipSpace()
		if p.pos >= len(p.src) {
			return val, nil
		}
		op := p.src[p.pos]
		if op != '+' && op != '-' {
			return val, nil
		}
		p.pos++
		rhs, err := p.parseTerm()
		if err != nil {
			return 0, err
		}
		if op == '+' {
			val += rhs
		} else {
			val -= rhs
		}
	}
}

// parseTerm, çarpma/bölme/mod.
func (p *parser) parseTerm() (float64, error) {
	val, err := p.parseUnary()
	if err != nil {
		return 0, err
	}
	for {
		p.skipSpace()
		if p.pos >= len(p.src) {
			return val, nil
		}
		op := p.src[p.pos]
		if op != '*' && op != '/' && op != '%' {
			return val, nil
		}
		p.pos++
		rhs, err := p.parseUnary()
		if err != nil {
			return 0, err
		}
		switch op {
		case '*':
			val *= rhs
		case '/':
			if rhs == 0 {
				return 0, p.fail("sıfıra bölme")
			}
			val /= rhs
		case '%':
			if rhs == 0 {
				return 0, p.fail("mod sıfır")
			}
			val = math.Mod(val, rhs)
		}
	}
}

// parseUnary, tek eksi/işaret.
func (p *parser) parseUnary() (float64, error) {
	p.skipSpace()
	if p.pos < len(p.src) && (p.src[p.pos] == '-' || p.src[p.pos] == '+') {
		sign := p.src[p.pos]
		p.pos++
		v, err := p.parseUnary()
		if err != nil {
			return 0, err
		}
		if sign == '-' {
			return -v, nil
		}
		return v, nil
	}
	return p.parsePower()
}

func (p *parser) parsePower() (float64, error) {
	base, err := p.parseAtom()
	if err != nil {
		return 0, err
	}
	p.skipSpace()
	// Üstel SAĞA BAĞLIDIR (2^3^2 = 2^9 = 512), konvansiyonel olduğu gibi.
	if p.pos < len(p.src) && p.src[p.pos] == '^' {
		p.pos++
		exp, err := p.parseUnary() // sağ taraf unary: 2^-3
		if err != nil {
			return 0, err
		}
		return math.Pow(base, exp), nil
	}
	return base, nil
}

// parseAtom, sayı / parantez / fonksiyon / değişken.
func (p *parser) parseAtom() (float64, error) {
	p.skipSpace()
	if p.pos >= len(p.src) {
		return 0, p.fail("ifade beklenmedik bitti")
	}
	ch := p.src[p.pos]

	if ch == '(' {
		p.pos++
		v, err := p.parseExpr()
		if err != nil {
			return 0, err
		}
		p.skipSpace()
		if p.pos >= len(p.src) || p.src[p.pos] != ')' {
			return 0, p.fail("kapanış parantezi eksik")
		}
		p.pos++
		return v, nil
	}
	if ch >= '0' && ch <= '9' || ch == '.' {
		return p.parseNumber()
	}

	// Tanımlayıcı: fonksiyon ya da değişken (örn. sqrt, fiyat).
	start := p.pos
	for p.pos < len(p.src) {
		c := p.src[p.pos]
		if c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c >= 128 {
			p.pos++
			continue
		}
		break
	}
	if start == p.pos {
		return 0, p.fail("anlaşılmayan karakter: %q", string(ch))
	}
	name := p.src[start:p.pos]

	if p.pos < len(p.src) && p.src[p.pos] == '(' {
		args, err := p.parseArgs()
		if err != nil {
			return 0, err
		}
		return p.applyFunc(name, args)
	}
	if v, ok := p.vars[name]; ok {
		return v, nil
	}
	// Bilinmeyen tanımlayıcı hata olur: sessizce geçmez.
	return 0, p.fail("bilinmeyen fonksiyon veya değişken: %q", name)
}

// parseArgs, virgülle ayrılmış argüman listesini okur.
func (p *parser) parseArgs() ([]float64, error) {
	p.pos++ // '('
	var args []float64
	p.skipSpace()
	if p.pos < len(p.src) && p.src[p.pos] == ')' {
		p.pos++
		return args, nil
	}
	for {
		v, err := p.parseExpr()
		if err != nil {
			return nil, err
		}
		args = append(args, v)
		p.skipSpace()
		if p.pos < len(p.src) && p.src[p.pos] == ',' {
			p.pos++
			continue
		}
		break
	}
	p.skipSpace()
	if p.pos >= len(p.src) || p.src[p.pos] != ')' {
		return nil, p.fail("kapanış parantezi eksik")
	}
	p.pos++
	return args, nil
}

func (p *parser) parseNumber() (float64, error) {
	start := p.pos
	for p.pos < len(p.src) && ((p.src[p.pos] >= '0' && p.src[p.pos] <= '9') || p.src[p.pos] == '.') {
		p.pos++
	}
	// Bilimsel gosterim: 1e5, 2.5E-3
	if p.pos < len(p.src) && (p.src[p.pos] == 'e' || p.src[p.pos] == 'E') {
		p.pos++
		if p.pos < len(p.src) && (p.src[p.pos] == '+' || p.src[p.pos] == '-') {
			p.pos++
		}
		for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' {
			p.pos++
		}
	}
	n, err := strconv.ParseFloat(p.src[start:p.pos], 64)
	if err != nil {
		return 0, p.fail("geçersiz sayı: %q", p.src[start:p.pos])
	}
	return n, nil
}

// applyFunc yalnızca İZİNLİ fonksiyonları uygular. Fonksiyon adı dışarıdan gelen
// metin olduğundan, buradaki sabit anahtarlı switch dışında hiçbir şey çağrılmaz.
func (p *parser) applyFunc(name string, a []float64) (float64, error) {
	need := func(n int) error {
		if len(a) != n {
			return p.fail("%s tam %d argüman bekler, %d verildi", name, n, len(a))
		}
		return nil
	}
	lower := strings.ToLower(name)
	switch lower {
	case "sqrt", "abs", "ln", "log", "exp", "sin", "cos", "tan", "round", "ceil", "floor":
		if err := need(1); err != nil {
			return 0, err
		}
		x := a[0]
		switch lower {
		case "sqrt":
			if x < 0 {
				return 0, p.fail("sqrt negatif sayı tanımsız")
			}
			return math.Sqrt(x), nil
		case "abs":
			return math.Abs(x), nil
		case "ln":
			if x <= 0 {
				return 0, p.fail("ln sıfırdan büyük olmalı")
			}
			return math.Log(x), nil
		case "log":
			if x <= 0 {
				return 0, p.fail("log sıfırdan büyük olmalı")
			}
			return math.Log10(x), nil
		case "exp":
			return math.Exp(x), nil
		case "sin":
			return math.Sin(x), nil
		case "cos":
			return math.Cos(x), nil
		case "tan":
			return math.Tan(x), nil
		case "round":
			return math.Round(x), nil
		case "ceil":
			return math.Ceil(x), nil
		default:
			return math.Floor(x), nil
		}

	case "min", "max":
		// Tek argüman anlamsız; en az iki istenir.
		if len(a) < 2 {
			return 0, p.fail("%s en az 2 argüman bekler", name)
		}
		acc := a[0]
		for _, v := range a[1:] {
			if lower == "min" {
				acc = math.Min(acc, v)
			} else {
				acc = math.Max(acc, v)
			}
		}
		return acc, nil

	case "pi":
		if err := need(0); err != nil {
			return 0, err
		}
		return math.Pi, nil
	}
	return 0, p.fail("bilinmeyen fonksiyon: %q", name)
}

// toFloat JSON sayısını float64'a çevirir; elle gelen metinli sayıları da kabul eder.
func toFloat(v any) (float64, bool) {
	switch n := v.(type) {
	case float64:
		return n, true
	case int:
		return float64(n), true
	case int64:
		return float64(n), true
	case int32:
		return float64(n), true
	case uint64:
		return float64(n), true
	case string:
		f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)
		return f, err == nil
	}
	return 0, false
}

// ---------- text.stats ----------

// textStatsHandler metnin temel sayısal ölçülerini döndürür. Kelime sayımı
// Türkçe'ye uygundur: noktalama ayrılır, "İ" gibi harfler doğru sayılır.
func textStatsHandler(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
	text, _ := in["text"].(string)
	if strings.TrimSpace(text) == "" {
		return nil, Fail(t.Name, "text boş olamaz")
	}
	// En sık kelimeler: küçük harfe indir, noktalama at, 2 harften kısayı eleme.
	freq := map[string]int{}
	for _, f := range strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
	}) {
		if utf8.RuneCountInString(f) > 2 {
			freq[f]++
		}
	}
	type kv struct {
		Word string
		N    int
	}
	list := make([]kv, 0, len(freq))
	for w, n := range freq {
		list = append(list, kv{w, n})
	}
	sort.Slice(list, func(i, j int) bool {
		if list[i].N != list[j].N {
			return list[i].N > list[j].N
		}
		return list[i].Word < list[j].Word // eşitlikte alfabetik: çıktı kararlı olsun
	})
	topN := 10
	if v, ok := toInt(in["topWords"]); ok && v > 0 {
		topN = v
	}
	if topN > 50 {
		topN = 50
	}
	top := make([]map[string]any, 0, len(list))
	for i := 0; i < len(list) && i < topN; i++ {
		top = append(top, map[string]any{"word": list[i].Word, "count": list[i].N})
	}

	words := len(strings.Fields(text))
	return map[string]any{
		"characters": utf8.RuneCountInString(text),
		"words":      words,
		"lines":      strings.Count(text, "\n") + 1,
		"sentences":  countSentences(text),
		"readingMin": roundClean(float64(words) / 200), // ~200 kelime/dakika
		"topWords":   top,
	}, nil
}

func countSentences(text string) int {
	n := 0
	for _, r := range text {
		if r == '.' || r == '!' || r == '?' {
			n++
		}
	}
	if n == 0 && strings.TrimSpace(text) != "" {
		return 1
	}
	return n
}
