package builtin

import (
	"bytes"
	"context"
	"errors"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
	"unicode/utf8"

	"fiko-ai/internal/tool"
)

// Def verilen tanım ve uygulamayı birlikte taşır (Registry.EnsureBuiltin'e verilir).
type Def struct {
	Tool    tool.Tool
	Handler tool.Handler
	// Enabled false ise tool kaydedilir ama KAPALI başlar (güvenlik varsayılanı).
	Enabled bool
}

// Definitions çekirdek tool kümesini üretir: her izin ve risk seviyesini temsil eden
// küçük, güvenli ve gerçekten çalışan bir çekirdek.
//
//	echo         : execute       / low      — hat ve şema akışını sınamak için
//	now          : execute       / low      — sunucu saati
//	fs.read_text : execute+read  / low      — kök içinden metin oku
//	fs.write_text: execute+write / medium   — kök içine metin yaz
//	fs.delete    : execute+delete/ high     — kök içinden sil
//	sys.exec     : execute+system/ critical — süreç çalıştır (VARSAYILAN KAPALI)
func Definitions(o Options) []Def {
	return []Def{
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fiko.echo",
				Title:       "Yankı",
				Description: "Girdiyi değiştirmeden geri döndürür. Bağlantı ve şema akışını sınamak için kullanılır.",
				Tags:        []string{"fiko", "test"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object",
					"properties", obj("message", str("Geri döndürülecek metin")),
				),
				OutputSchema: obj("type", "object",
					"properties", obj("message", str("Girdinin aynısı")),
				),
				TimeoutMs: 5_000,
			},
			Handler: echoHandler,
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fiko.now",
				Title:       "Şu an",
				Description: "Sunucudaki güncel zamanı döndürür.",
				Tags:        []string{"fiko", "test"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object", "properties", obj(
					"timezone", str("Saat dilimi (örn. Europe/Istanbul); boşsa UTC"),
				)),
				OutputSchema: obj("type", "object", "properties", obj(
					"iso", str("ISO-8601 biçiminde zaman"),
					"date", str("YYYY-MM-DD"),
					"time", str("HH:MM:SS"),
				)),
			},
			Handler: nowHandler(o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fs.read_text",
				Title:       "Dosya oku",
				Description: "TOOL_FS_ROOT altındaki bir metin dosyasını okur (kök dışına çıkış reddedilir).",
				Tags:        []string{"fs"},
				Risk:        tool.RiskLow,
				Permissions: []string{"read"},
				InputSchema: obj("type", "object",
					"properties", obj("path", str("Köke göreli yol (örn. notlar/plan.txt)")),
					"required", []any{"path"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"path", str("Okunan yol"),
					"content", str("Dosya içeriği"),
					"bytes", obj("type", "integer"),
				)),
				TimeoutMs: 10_000,
			},
			Handler: readTextHandler(o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fs.write_text",
				Title:       "Dosya yaz",
				Description: "TOOL_FS_ROOT altında metin dosyası oluşturur/üzerine yazar.",
				Tags:        []string{"fs"},
				Risk:        tool.RiskMedium,
				Permissions: []string{"write"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"path", str("Köke göreli yol"),
						"content", str("Yazılacak metin"),
						"append", obj("type", "boolean", "description", "true ise sona ekler", "default", false),
					),
					"required", []any{"path", "content"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"path", str("Yazılan yol"),
					"bytes", obj("type", "integer"),
				)),
				TimeoutMs: 10_000,
			},
			Handler: writeTextHandler(o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:        "fs.delete",
				Title:       "Dosya sil",
				Description: "TOOL_FS_ROOT altındaki bir dosyayı ya da klasörü siler (klasör için recursive=true gerekir).",
				Tags:        []string{"fs"},
				Risk:        tool.RiskHigh,
				Permissions: []string{"delete"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"path", str("Köke göreli yol"),
						"recursive", obj("type", "boolean", "description", "true ise klasör içeriğiyle silinir", "default", false),
					),
					"required", []any{"path"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"path", str("Silinen yol"),
					"deleted", obj("type", "boolean"),
				)),
				TimeoutMs: 20_000,
			},
			Handler: deleteHandler(o),
		},
	}
}

// DefinitionsWithExec, süreç çalıştırma tool'unu da içerir. Bu tool YALNIZCA açıkça izin
// verilen program yolları tanımlıysa eklenir: izin listesi boşken hiç oluşturulmaz,
// kaydı da görünmez. Yine de çalıştırmak için system izni + critical risk onayı gerekir.
func DefinitionsWithExec(o Options) []Def {
	defs := Definitions(o)
	if len(o.ExecAllowed) == 0 {
		return defs
	}
	return append(defs, Def{
		Enabled: true,
		Tool: tool.Tool{
			Name:        "sys.exec",
			Title:       "Süreç çalıştır",
			Description: "İzin listesindeki bir programı argümanlarıyla çalıştırır (kabuk/pipe yok) ve çıktısını döndürür.",
			Tags:        []string{"system"},
			Risk:        tool.RiskCritical,
			Permissions: []string{"system"},
			InputSchema: obj("type", "object",
				"properties", obj(
					"program", str("İzin listesindeki programın tam yolu"),
					"args", obj("type", "array", "description", "Argüman listesi",
						"items", obj("type", "string")),
				),
				"required", []any{"program"},
			),
			OutputSchema: obj("type", "object", "properties", obj(
				"stdout", str("Standart çıktı"),
				"stderr", str("Standart hata"),
				"exitCode", obj("type", "integer"),
			)),
			TimeoutMs: 60_000,
			Retry:     tool.Retry{MaxAttempts: 0}, // süreç çalıştırma TEKRAR EDİLMEZ (yan etkili olabilir)
		},
		Handler: execHandler(o),
	})
}

// allDefinitions çekirdek tool'lara, açıkça etkinleştirilmiş grupları ekler:
// internet (WebEnabled + güvenli HTTP istemcisi) ve şirket yönetimi (BizEnabled).
// İstemci verilmediyse web tool'ları HİÇ kaydedilmez: korumasız (SSRF'e açık) istemci kullanılmaz.
//
// fs.list/search/stat ile calc.eval/text.stats HER ZAMAN eklenir: yalnızca okuma
// yapar, ağ ya da yan etki üretmez ve açıkça kapatılana kadar kullanılabilir durumdadır.
func allDefinitions(o Options) ([]Def, error) {
	defs := DefinitionsWithExec(o)
	defs = append(defs, fsDefinitions(o)...)
	defs = append(defs, calcDefinitions(o)...)
	defs = append(defs, convertDefinitions(o)...)
	defs = append(defs, dateDefinitions(o)...)
	// ui.form: yan etkisiz bilgi formu (kullanıcıdan alan toplar); her zaman eklenir.
	defs = append(defs, formDefinitions(o)...)
	if o.WebEnabled && o.HTTPClient != nil {
		defs = append(defs, webDefinitions(o)...)
	}
	if o.BizEnabled {
		biz, err := bizDefinitions(o)
		if err != nil {
			return nil, err
		}
		defs = append(defs, biz...)
	}
	return defs, nil
}

// Register tüm builtin tool'ları Registry'ye yazar.
//
// İKİ KURAL birlikte çalışır:
//  1. EnsureBuiltin: kullanıcı alanlarına (status, tags, config) DOKUNMAZ — kullanıcı
//     arayüzden yaptığı düzenlemeyi korumamız gerekir.
//  2. SyncBuiltinFields: KOD TÜRETTİĞİ alanları (izin, risk, şema, açıklama) tazeler.
//
// Neden ikisi de? (Canlı hata 04.10.2026) Mongo'daki eski kayıtlarda "write" izni vardı;
// EnsureBuiltin kaydı güncellemediği için bellek araçları modele hiç sunulmuyordu. Aynı
// tuzak tüm araçlar için geçerli: yalnızca kodu düzeltmek yetmez, KAYIT da senkron olmalı.
func Register(ctx context.Context, reg *tool.Registry, o Options) error {
	defs, err := allDefinitions(o)
	if err != nil {
		return err
	}
	var closed []string
	for _, d := range defs {
		rec, _, err := reg.EnsureBuiltin(ctx, d.Tool, d.Handler, d.Enabled)
		if err != nil {
			return err
		}
		if err := reg.SyncBuiltinFields(ctx, d.Tool); err != nil {
			// Senkron başarısızsa AKIŞ DURMAZ: araç eski tanımla çalışmaya devam eder,
			// ama kullanıcı nedenini görebilsin diye açıkça uyarılır.
			o.log("tool alanları tazelenemedi (eski kayıt kullanılıyor)", "tool", d.Tool.Name, "err", err)
		}
		// Kapalı kalan araçlar modele sunulmaz; bu, "araç yok" sanılan canlı hataların
		// en sık nedenidir (kullanıcı kapatmıştır). Açılışta tek satırda söylenmeli.
		if !d.Enabled || (rec != nil && rec.Status == tool.StatusDisabled) {
			closed = append(closed, d.Tool.Name)
		}
	}
	if len(closed) > 0 {
		o.log("bazı araçlar KAPALI: modele sunulmaz ve çağrılamaz (izin/tanım ekranından açın)",
			"kapalı", closed)
	}
	return nil
}

func echoHandler(_ context.Context, _ *tool.Tool, in map[string]any) (map[string]any, error) {
	msg, _ := in["message"].(string)
	return map[string]any{"message": msg}, nil
}

func nowHandler(o Options) tool.Handler {
	return func(_ context.Context, _ *tool.Tool, in map[string]any) (map[string]any, error) {
		now := o.clock().UTC()
		if tz, ok := inputString(in, "timezone"); ok {
			loc, err := time.LoadLocation(tz)
			if err != nil {
				return nil, Fail("fiko.now", "geçersiz saat dilimi %q", tz)
			}
			now = now.In(loc)
		}
		return map[string]any{
			"iso":  now.Format(time.RFC3339),
			"date": now.Format("2006-01-02"),
			"time": now.Format("15:04:05"),
		}, nil
	}
}

// resolvePath kullanıcı yolunu kök altında çözer. KÖK DIŞINA KAÇIŞ ("..") reddedilir:
// tool'lar yalnızca kendi sandbox'ında çalışır. Mutlak yol da kabul edilmez.
func resolvePath(root, rel string) (string, bool) {
	rel = strings.TrimSpace(rel)
	if rel == "" || filepath.IsAbs(rel) {
		return "", false
	}
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return "", false
	}
	absFull, err := filepath.Abs(filepath.Join(absRoot, filepath.Clean(filepath.FromSlash(rel))))
	if err != nil {
		return "", false
	}
	// Sınırdan sonra ayırıcı şart: "<kök>-evil" kökün içi değildir.
	if absFull != absRoot && !strings.HasPrefix(absFull, absRoot+string(filepath.Separator)) {
		return "", false
	}
	return absFull, true
}

func readTextHandler(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)
		}
		f, err := os.Open(full)
		if err != nil {
			return nil, Fail(t.Name, "dosya açılamadı: %s", baseErr(err))
		}
		defer f.Close()
		info, err := f.Stat()
		if err != nil {
			return nil, Fail(t.Name, "dosya okunamadı: %s", baseErr(err))
		}
		if info.IsDir() {
			return nil, Fail(t.Name, "%q bir klasör", rel)
		}
		if info.Size() > o.maxRead() {
			return nil, Fail(t.Name, "dosya çok büyük (%d bayt, sınır %d)", info.Size(), o.maxRead())
		}
		// Limit ile oku: dosya okuma sırasında büyürse taşma olmasın.
		data, err := io.ReadAll(io.LimitReader(f, o.maxRead()+1))
		if err != nil {
			return nil, Fail(t.Name, "içerik okunamadı: %s", baseErr(err))
		}
		if int64(len(data)) > o.maxRead() {
			return nil, Fail(t.Name, "içerik sınırı aşıyor (%d bayt)", o.maxRead())
		}
		if !utf8.Valid(data) {
			return nil, Fail(t.Name, "dosya metin değil (geçersiz UTF-8)")
		}
		return map[string]any{
			"path":    filepath.ToSlash(rel),
			"content": string(data),
			"bytes":   len(data),
		}, nil
	}
}

func writeTextHandler(o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		rel, _ := in["path"].(string)
		content, _ := in["content"].(string)
		appendMode, _ := in["append"].(bool)
		if int64(len(content)) > o.maxWrite() {
			return nil, Fail(t.Name, "içerik sınırı aşıyor (%d bayt, sınır %d)", len(content), o.maxWrite())
		}
		full, ok := resolvePath(o.fsRoot(), rel)
		if !ok {
			return nil, Fail(t.Name, "yol kök dışına çıkıyor: %q", rel)
		}
		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
			return nil, Fail(t.Name, "klasör oluşturulamadı: %s", baseErr(err))
		}
		flags := os.O_CREATE | os.O_WRONLY
		if appendMode {
			flags |= os.O_APPEND
		} else {
			flags |= os.O_TRUNC
		}
		f, err := os.OpenFile(full, flags, 0o600)
		if err != nil {
			return nil, Fail(t.Name, "dosya açılamadı: %s", baseErr(err))
		}
		defer f.Close()
		if _, err := f.WriteString(content); err != nil {
			return nil, Fail(t.Name, "yazılamadı: %s", baseErr(err))
		}
		return map[string]any{"path": filepath.ToSlash(rel), "bytes": len(content)}, nil
	}
}

func deleteHandler(o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		rel, _ := in["path"].(string)
		recursive, _ := in["recursive"].(bool)
		full, ok := resolvePath(o.fsRoot(), rel)
		if !ok {
			return nil, Fail(t.Name, "yol kök dışına çıkıyor: %q", rel)
		}
		if full == strings.TrimRight(full, string(filepath.Separator)) && isRoot(o.fsRoot(), full) {
			return nil, Fail(t.Name, "kök dizinin kendisi silinemez")
		}
		var err error
		if recursive {
			err = os.RemoveAll(full)
		} else {
			err = os.Remove(full)
			if err != nil && isNotEmpty(err) {
				return nil, Fail(t.Name, "klasör boş değil; silmek için recursive=true gerekir")
			}
		}
		if err != nil {
			return nil, Fail(t.Name, "silinemedi: %s", baseErr(err))
		}
		return map[string]any{"path": filepath.ToSlash(rel), "deleted": true}, nil
	}
}

// execHandler izin listesindeki programı kabuk KULLANMADAN çalıştırır.
// Shell injection riskine karşı: program tam yol olarak eşleşmeli, args tek tek geçmeli.
func execHandler(o Options) tool.Handler {
	allowed := make(map[string]bool, len(o.ExecAllowed))
	for _, p := range o.ExecAllowed {
		if p = strings.TrimSpace(p); p != "" {
			allowed[filepath.Clean(p)] = true
		}
	}
	limit := o.ExecTimeout
	if limit <= 0 {
		limit = 60 * time.Second
	}
	return func(ctx context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		program, _ := in["program"].(string)
		if !allowed[filepath.Clean(program)] {
			return nil, Fail(t.Name, "program izin listesinde yok: %s", program)
		}
		var args []string
		if raw, ok := in["args"].([]any); ok {
			for _, a := range raw {
				s, ok := a.(string)
				if !ok {
					return nil, Fail(t.Name, "args yalnızca metin içerebilir")
				}
				args = append(args, s)
			}
		}
		runCtx, cancel := context.WithTimeout(ctx, limit)
		defer cancel()
		cmd := exec.CommandContext(runCtx, program, args...)
		var stdout, stderr bytes.Buffer
		cmd.Stdout, cmd.Stderr = &stdout, &stderr
		runErr := cmd.Run()
		code := 0
		if runErr != nil {
			var ee *exec.ExitError
			if errors.As(runErr, &ee) {
				code = ee.ExitCode() // program çalıştı, sıfırdan farklı bitti: araç hatası değil
			} else {
				return nil, Fail(t.Name, "süreç başlatılamadı: %s", baseErr(runErr))
			}
		}
		return map[string]any{
			"stdout":   cap(stdout.String(), 64<<10),
			"stderr":   cap(stderr.String(), 64<<10),
			"exitCode": code,
		}, nil
	}
}

// cap uzun metni sınırlar (LLM'e/kayda giren alanların şişmemesi için).
func cap(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max] + "...(kısaltıldı)"
}

// baseErr hatanın kullanıcıya gösterilebilir kısmını verir (yol/işletim sistemi ayrıntısı gerekmez).
func baseErr(err error) string {
	var pe *os.PathError
	if errors.As(err, &pe) {
		return pe.Err.Error()
	}
	return err.Error()
}

func isRoot(root, full string) bool {
	absRoot, err1 := filepath.Abs(root)
	absFull, err2 := filepath.Abs(full)
	return err1 == nil && err2 == nil && absRoot == absFull
}

// isNotEmpty, silme hatasının "klasör boş değil" olduğunu Windows ve Unix'te yakalar.
// (Windows'da Remove boş olmayan klasörde de "directory not empty" döner.)
func isNotEmpty(err error) bool {
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "not empty") || strings.Contains(msg, "dizin boş değil")
}
