package tool

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"fiko-ai/internal/audit"
	"fiko-ai/internal/event"
	"fiko-ai/internal/schema"
)

// maxInputBytes girdi gövdesinin üst sınırı: devasa bir JSON, handler'a ulaşmadan reddedilir.
const maxInputBytes = 64 << 10 // 64 KiB

// Recorder denetim kaydı arayüzüdür (audit.Logger bu sözleşmeyi karşılar).
// Arayüz olarak tutulur ki testte dosya yazmadan doğrulanabilsin.
type Recorder interface{ Record(audit.Entry) }

// Executor tool çalıştırma zincirinin tek yeridir ve sıra KESİNTİSİZ değiştirilemez:
//
//	çözümle → kapalı mı → doğrula (varsayılanlar + şema) → yetki → süre sınırı → çalıştır → denetim kaydı
//
// Sıraların gerekçesi:
//   - Doğrulama yetkiden ÖNCE: kullanıcının izni olmadan bile veri sızdırılmaz (hata mesajı
//     yalnızca kendi girdisini anlatır).
//   - Yetki çalıştırmadan ÖNCE: reddedilen bir tool hiçbir yan etki bırakmaz.
//   - Her çalıştırma (reddedilse bile) denetim kaydına girer: "kimin neyi denediği" sorusu
//     cevaplanabilir olmalıdır (spec: her işlem audit log'a yazılmalı).
type Executor struct {
	reg     *Registry
	grants  *GrantStore
	audit   Recorder // nil olabilir
	bus     event.Bus
	maxWait time.Duration // tool tanımının aşamayacağı üst sınır
	maxTry  int           // retry üst sınırı
	log     *slog.Logger
	now     func() time.Time
}

// ExecutorOptions Executor ayarlarıdır.
type ExecutorOptions struct {
	// MaxWait tool'ın isteyebileceği en uzun süre (0 ise MaxTimeoutMs).
	MaxWait time.Duration
	// MaxAttempts en fazla kaç deneme yapılabileceği (0 ise 1 = tek deneme).
	MaxAttempts int
	Logger      *slog.Logger
}

// NewExecutor Executor kurar. grants nil ise varsayılan politika (yalnızca execute) kullanılır.
func NewExecutor(reg *Registry, grants *GrantStore, rec Recorder, bus event.Bus, opts ExecutorOptions) *Executor {
	if grants == nil {
		grants = NewGrantStore(DefaultPolicy())
	}
	if opts.MaxWait <= 0 || opts.MaxWait > MaxTimeoutMs*time.Millisecond {
		opts.MaxWait = MaxTimeoutMs * time.Millisecond
	}
	// MaxAttempts verilmezse tool kendi politikasıyla sınırlı kalsın: üst sınır
	// (MaxRetryAttempts+1) uygulanır. 1'e sabitlemek, tool tanımındaki retry'ı
	// sessizce etkisizleştirirdi.
	if opts.MaxAttempts <= 0 {
		opts.MaxAttempts = MaxRetryAttempts + 1
	}
	if opts.MaxAttempts > MaxRetryAttempts+1 {
		opts.MaxAttempts = MaxRetryAttempts + 1
	}
	if opts.Logger == nil {
		opts.Logger = slog.Default()
	}
	return &Executor{
		reg: reg, grants: grants, audit: rec, bus: bus,
		maxWait: opts.MaxWait, maxTry: opts.MaxAttempts,
		log: opts.Logger, now: func() time.Time { return time.Now().UTC() },
	}
}

// Request tek bir çalıştırma isteğidir.
type Request struct {
	// Ref tool adı ya da kimliği ("fiko.echo" veya "tool_...").
	Ref string
	// Input tool'un girdisi; şemadaki varsayılanlar uygulanır, ardından doğrulanır.
	Input map[string]any
	// Actor "user" (arayüzden elle) veya "agent" (Faz 5'te LLM kararı).
	Actor string
	// UserID yetki bu kimliğe göre denetlenir; boşsa varsayılan politika.
	UserID string
	// ConversationID denetim kaydına yazılır (hangi sohbette çalıştı).
	ConversationID string
	// Progress ara ilerleme bildirimi (WS tool.progress olayı için; nil olabilir).
	Progress func(step string, percent int)
}

// Result başarılı bir çalıştırmanın özetidir.
type Result struct {
	ToolID     string         `json:"toolId"`
	ToolName   string         `json:"toolName"`
	Output     map[string]any `json:"output"`
	Attempts   int            `json:"attempts"`
	DurationMs int64          `json:"durationMs"`
	// UI çıktının hangi görünümde sunulacağıdır (arayüz sözleşmesi).
	// Tool tanımında UIAuto ise ad ön ekinden türer (bkz. ResolveUI).
	// Arayüz "hangi UI" kararını istemciye bırakmaz: tek doğru yer burasıdır.
	UI UI `json:"ui,omitempty"`
}

// ResolveUI, bir tool'un arayüzde hangi görünümde sunulacağını kesinleştirir.
//
// Kural: tanımda açık bir görünüm varsa O kullanılır; UIAuto (ya da boş) ise
// ön ek tablosundan türer. Tablo bilinmeyen ön ek için JSON döner: hiçbir tool
// arayüzsüz kalmaz, en kötü durumda ham veri görünür.
//
// TEK DOĞRULUK KAYNAĞI burasıdır. builtin.DeriveUI yalnızca okunabilir bir
// açıklama/test noktasıdır ve bu fonksiyona devreder (mantık kopyalanmaz).
func ResolveUI(t *Tool) UI {
	if t == nil {
		return UIJson
	}
	if t.UI != "" && t.UI != UIAuto {
		return t.UI
	}
	prefix, _, ok := strings.Cut(t.Name, ".")
	if ok {
		switch strings.ToLower(prefix) {
		case "biz", "memory", "knowledge":
			return UITable
		case "fs":
			return UIFile
		case "web":
			return UIList
		case "sys":
			return UICode
		}
	}
	return UIJson
}

// Execute istenen tool'u yetkisi varsa çalıştırır ve sonucu döndürür.
//
// Hata sözleşmesi (API katmanı bunları HTTP durumuna eşler):
//   - ErrNotFound / ErrNotExecutable / ErrDisabled : 404 / 501 / 409
//   - *InputError (şema ihlali)                   : 400 (alan bazlı)
//   - *PermissionError                            : 403
//   - context.DeadlineExceeded (tool zaman aşımı) : 504
//   - *FailedError (tool'un kendi hatası)         : 502
func (e *Executor) Execute(ctx context.Context, req Request) (*Result, error) {
	started := e.now()

	t, handler, err := e.reg.Resolve(ctx, req.Ref)
	if err != nil {
		e.record(req, t, audit.ResultError, err, 0, started)
		return nil, err
	}
	if t.Status != StatusEnabled {
		err := ErrDisabled
		e.record(req, t, audit.ResultError, err, 0, started)
		return nil, err
	}

	in, err := prepareInput(t, req.Input)
	if err != nil {
		e.record(req, t, audit.ResultError, err, 0, started)
		return nil, err
	}

	policy := e.grants.PolicyFor(req.UserID)
	if err := policy.Authorize(t); err != nil {
		e.record(req, t, audit.ResultDenied, err, 0, started)
		return nil, err
	}

	out, attempts, err := e.run(ctx, t, handler, in, req)
	if err != nil {
		e.record(req, t, audit.ResultError, err, attempts, started)
		return nil, err
	}
	res := &Result{
		ToolID: t.ID, ToolName: t.Name, Output: out, Attempts: attempts,
		DurationMs: e.now().Sub(started).Milliseconds(),
		UI:         ResolveUI(t),
	}
	e.record(req, t, audit.ResultSuccess, nil, attempts, started)
	e.publishExecuted(req, res)
	return res, nil
}

// prepareInput girdiyi hazırlar: boyut sınırı, varsayılanlar, şema doğrulaması.
// Çıktı DERİN kopyadır: handler çalıştırıldığında çağıranın haritasını değiştiremez.
func prepareInput(t *Tool, in map[string]any) (map[string]any, error) {
	if in == nil {
		in = map[string]any{}
	}
	if jsonLen(in) > maxInputBytes {
		return nil, &InputError{Tool: t.Name, Errors: []FieldError{{Field: "", Message: "girdi çok büyük"}}}
	}
	prepared, _ := schema.ApplyDefaults(t.InputSchema, in).(map[string]any)
	if prepared == nil {
		prepared = map[string]any{}
	}
	if vs := schema.Validate(t.InputSchema, prepared); len(vs) > 0 {
		errs := make([]FieldError, 0, len(vs))
		for _, v := range vs {
			field := v.Path
			if field == "" {
				field = "input" // kök hata: hangi alan olduğu yok
			}
			errs = append(errs, FieldError{Field: field, Message: v.Message})
		}
		return nil, &InputError{Tool: t.Name, Errors: errs}
	}
	return prepared, nil
}

// run tool'u süre sınırı ve gerekiyorsa tekrar deneme (retry) ile çalıştırır.
// Yalnızca hata geçici olarak işaretlenmişse (FailedError.Retryable) tekrar denenir.
func (e *Executor) run(ctx context.Context, t *Tool, handler Handler, in map[string]any, req Request) (map[string]any, int, error) {
	wait := time.Duration(t.TimeoutMs) * time.Millisecond
	if wait <= 0 || wait > e.maxWait {
		wait = e.maxWait
	}
	tries := t.Retry.MaxAttempts + 1
	if tries > e.maxTry {
		tries = e.maxTry
	}
	if tries < 1 {
		tries = 1
	}

	backoff := time.Duration(t.Retry.BackoffMs) * time.Millisecond
	var lastErr error
	for attempt := 1; attempt <= tries; attempt++ {
		if err := ctx.Err(); err != nil { // çağıran vazgeçti: tekrar denemek anlamsız
			return nil, attempt - 1, err
		}
		out, err := e.runOnce(ctx, t, handler, in, wait, req)
		if err == nil {
			return out, attempt, nil
		}
		lastErr = err
		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
			return nil, attempt, err // iptal/süre aşımı tekrarlanmaz
		}
		var fe *FailedError
		if !errors.As(err, &fe) || !fe.Retryable() {
			return nil, attempt, err
		}
		if attempt == tries {
			break
		}
		if err := sleepCtx(ctx, backoff); err != nil {
			return nil, attempt, err
		}
		e.log.Warn("tool yeniden deneniyor", "tool", t.Name, "deneme", attempt+1, "err", err)
	}
	return nil, tries, lastErr
}

// Config anahtarları: çalıştırma bağlamını tool'a taşıyan köprü.
const (
	// ConfigUserID, tool'u çalıştıran kimliğin tool.Config içindeki karşılığıdır.
	// YANLIŞLIKLA DEĞİŞTİRİLEMEZ: değer executor tarafından Request'ten yazılır,
	// model girdiyle değiştiremez.
	ConfigUserID = "userId"
	// ConfigConversationID, tool'un çalıştığı sohbetin kimliğidir (denetim/birleştirme için).
	ConfigConversationID = "conversationId"
)

// runOnce tek deneme: kendi süre sınırıyla handler'ı çağırır, paniği yakalar.
func (e *Executor) runOnce(parent context.Context, t *Tool, handler Handler, in map[string]any,
	wait time.Duration, req Request) (out map[string]any, err error) {

	ctx, cancel := context.WithTimeout(parent, wait)
	defer cancel()

	defer func() {
		if r := recover(); r != nil {
			// Tool kodu paniklediyse sunucu çökmemeli; hata işlenmiş gibi döner.
			e.log.Error("tool panikledi", "tool", t.Name, "panic", r)
			out, err = nil, &FailedError{Tool: t.Name, Reason: fmt.Sprintf("panik: %v", r)}
		}
	}()

	if req.Progress != nil {
		req.Progress("başlıyor", 0)
	}

	// ÇALIŞTIRMA BAĞLAMINI TOOL'A VER. Handler imzası yalnızca (ctx, tool, input) alır;
	// oysa kullanıcı kimliği Request'te taşınır. Köprü olmadan handler KENDİ
	// KULLANICISINI GÖREMEZ.
	//
	// Canlı hata: bellek tool'ları (memory.remember/update/recall/ask) çağrılıyordu ama
	// userId hep boş geldiği için "userId gerekli" hatası veriyor ya da kimliksiz
	// kayıt deniyordu. Bu köprü olmadan HİÇBİR tool kullanıcı kapsamını göremez.
	//
	// Güvenlik: değerler executor'ın İŞLETMİŞ olduğu Request'ten gelir; model girdiden
	// değiştiremez. Tool tanımının KENDİ config'i (dosya kökü vb.) korunur.
	if uid := strings.TrimSpace(req.UserID); uid != "" {
		// Clone değer döndürür ve Config'i de kopyalar; kök tanımın Config'i bozulmaz.
		c := t.Clone()
		c.Config[ConfigUserID] = uid
		if cid := strings.TrimSpace(req.ConversationID); cid != "" {
			c.Config[ConfigConversationID] = cid
		}
		t = &c
	}

	out, herr := handler(ctx, t, in)
	switch {
	case herr == nil:
		return out, nil
	case errors.Is(herr, context.DeadlineExceeded) || errors.Is(parent.Err(), context.DeadlineExceeded):
		return nil, &FailedError{Tool: t.Name, Reason: "zaman aşımı", err: herr}
	case errors.Is(herr, context.Canceled) || errors.Is(parent.Err(), context.Canceled):
		return nil, herr // çağıran iptal etti: FailedError'e sarmalamayız (499 dönmeli)
	case errors.Is(herr, ErrHTTPBlocked):
		// SSRF koruması: kullanıcı nedenini görmeli ("tool hata döndürdü" işe yaramaz).
		return nil, &FailedError{Tool: t.Name, Reason: "hedef adres güvenlik nedeniyle engellendi (iç ağ/özel adres)", err: herr}
	case errors.Is(herr, ErrHTTPDisabled):
		return nil, &FailedError{Tool: t.Name, Reason: "uzak tool çalıştırma kapalı (TOOL_HTTP_ENABLED=false)", err: herr}
	case errors.Is(herr, ErrHTTPRefused):
		return nil, &FailedError{Tool: t.Name, Reason: "uzak servis isteği reddetti (4xx)", err: herr}
	default:
		var fe *FailedError
		if errors.As(herr, &fe) {
			return nil, fe
		}
		// Gerçek nedeni SUNUCU LOGUNA yaz: kullanıcıya giden mesaj genel kalır ama
		// "tool hata döndürdü" tek başına teşhis edilemiyordu.
		e.log.Warn("tool hata döndürdü", "tool", t.Name, "user", req.UserID, "err", herr)
		return nil, &FailedError{Tool: t.Name, Reason: "tool hata döndürdü", err: herr}
	}
}

// sleepCtx bekleme sırasında iptal edilirse erken döner (yeniden deneme zinciri asılı kalmasın).
func sleepCtx(ctx context.Context, d time.Duration) error {
	if d <= 0 {
		return ctx.Err()
	}
	timer := time.NewTimer(d)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

// record her çalıştırmayı (başarılı, hatalı veya reddedilmiş) denetim kaydına yazar.
// Girdi/çıktı gövdesi KAYDEDİLMEZ: yalnızca araç adı, deneme sayısı ve hata özeti.
func (e *Executor) record(req Request, t *Tool, result string, cause error, attempts int, started time.Time) {
	if e.audit == nil {
		return
	}
	entry := audit.Entry{
		Actor:          actorOr(req.Actor),
		UserID:         strings.TrimSpace(req.UserID),
		ConversationID: strings.TrimSpace(req.ConversationID),
		Action:         "tool.execute",
		Result:         result,
		Meta: map[string]any{
			"ref":       req.Ref,
			"attempts":  attempts,
			"elapsedMs": e.now().Sub(started).Milliseconds(),
		},
	}
	if t != nil {
		entry.Target = t.Name
		entry.Meta["risk"] = string(t.Risk)
	}
	if cause != nil {
		entry.Detail = audit.Sanitize(cause.Error(), 300)
	}
	e.audit.Record(entry)
}

func actorOr(a string) string {
	switch strings.TrimSpace(a) {
	case "agent":
		return "agent"
	case "user":
		return "user"
	default:
		return "system"
	}
}

func (e *Executor) publishExecuted(req Request, res *Result) {
	if e.bus == nil {
		return
	}
	e.bus.Publish(context.Background(), event.Event{
		Type:           event.ToolExecuted,
		UserID:         strings.TrimSpace(req.UserID),
		ConversationID: strings.TrimSpace(req.ConversationID),
		Data: map[string]any{
			"tool":       res.ToolName,
			"durationMs": res.DurationMs,
			"attempts":   res.Attempts,
		},
	})
}
