package workflow

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// ---- Bağımlılıklar (paket diğer paketleri BİLMEZ; cmd/server'da gerçek uygulamalara bağlanır) ----

// LLMRequest tek turluk metin üretimidir (akış yok; akış sohbet içindir).
type LLMRequest struct {
	UserID      string
	System      string
	Prompt      string
	Model       string   // boş = ayarlardaki model
	Temperature *float64 // nil = ayarlardaki değer
}

// LLMResult üretimin sonucu ve token sayılarıdır.
type LLMResult struct {
	Text             string
	PromptTokens     int
	CompletionTokens int
}

// LLM llm node'unun ihtiyacıdır.
type LLM interface {
	Complete(ctx context.Context, req LLMRequest) (LLMResult, error)
}

// ToolRunner tool node'unun ihtiyacıdır. Yetki ve doğrulama uygulamanın İÇİNDE denetlenir
// (tool.Executor ile aynı zincir): akış, ajanın aşamayacağı bir kapıyı aşamaz.
type ToolRunner interface {
	RunTool(ctx context.Context, userID, name string, input map[string]any) (map[string]any, error)
}

// Retriever rag node'unun ihtiyacıdır: sorguya en yakın belge parçalarının METNİ.
type Retriever interface {
	Passages(ctx context.Context, userID, query string, limit int) ([]string, error)
}

// MemoryMessage konuşma geçmişinden tek mesajdır.
type MemoryMessage struct {
	Role    string // "user" | "assistant"
	Content string
}

// History memory node'unun ihtiyacıdır: konuşmanın son mesajları (eskiden yeniye).
type History interface {
	Recent(ctx context.Context, userID, conversationID string, limit int) ([]MemoryMessage, error)
}

// SkillRun bir skill çalıştırmasının sonucudur.
type SkillRun struct {
	Skill  string
	Tool   string
	Output map[string]any
}

// ErrNoSkill sorguya uyan skill yoksa SkillRunner'ın döndürdüğü hatadır.
var ErrNoSkill = errors.New("workflow: sorguya uyan skill bulunamadı")

// SkillRunner skill node'unun ihtiyacıdır: sorguya en uygun skill'i bul ve bağlı tool'unu çalıştır.
type SkillRunner interface {
	RunSkill(ctx context.Context, userID, query string, args map[string]any) (SkillRun, error)
}

// Deps motorun yeteneklerinin toplamıdır. Nil olan yetenek, o türdeki node'ları ÇALIŞTIRILAMAZ yapar
// (kaydetmeyi engellemez): bkz. Deps.Missing.
type Deps struct {
	LLM       LLM
	Tools     ToolRunner
	Knowledge Retriever
	Memory    History
	Skills    SkillRunner
}

// Has node türünün çalışması için gereken yetenek bağlı mı?
func (d Deps) Has(t NodeType) bool {
	switch t {
	case NodeLLM:
		return d.LLM != nil
	case NodeTool:
		return d.Tools != nil
	case NodeRAG:
		return d.Knowledge != nil
	case NodeMemory:
		return d.Memory != nil
	case NodeSkill:
		return d.Skills != nil
	}
	return true
}

// Missing akışın kullandığı ama bağlı olmayan node türlerini döndürür (tekrarsız, sıralı).
func (d Deps) Missing(w *Workflow) []NodeType {
	var out []NodeType
	seen := map[NodeType]bool{}
	for _, n := range w.Nodes {
		if !d.Has(n.Type) && !seen[n.Type] {
			seen[n.Type] = true
			out = append(out, n.Type)
		}
	}
	return out
}

// RunEnv bir yürütmenin girdileridir.
type RunEnv struct {
	UserID         string
	ConversationID string // memory node'u için; boş olabilir
	Input          string
}

// MaxOutputRunes yürütmenin nihai çıktısının üst sınırıdır.
const MaxOutputRunes = 20000

// ---- Şablonlar ----
//
// Config metinlerinde {{değişken}} kullanılır. Değişkenler:
//   input           çalıştırma girdisi
//   prev            son çalışan node'un çıktısı (condition hariç)
//   <nodeId>        o node'un çıktısı; nesne çıktıları için <nodeId>.alan.alt
//   item, index     loop gövdesinde geçerli öğe ve 0 tabanlı sıra
//   conversationId  çalıştırmanın konuşması
// Bilinmeyen değişken (yazım hatası) HATADIR; henüz çalışmamış bir node'unki boş metindir.

var tokenRe = regexp.MustCompile(`\{\{\s*([A-Za-z0-9_.\-]+)\s*\}\}`)

var reservedVars = []string{"input", "prev", "item", "index", "conversationId"}

func reservedVar(name string) bool {
	for _, v := range reservedVars {
		if v == name {
			return true
		}
	}
	return false
}

// ---- Node açıklamaları (config alanları) ----
//
//	input       —
//	memory      limit (varsayılan 10, en çok 50)
//	rag         query* (şablon), limit (varsayılan 3, en çok 10)
//	skill       query* (şablon), arguments (nesne; değerler şablon)
//	llm         prompt* (şablon), system (şablon), model, temperature
//	tool        name* (şablon), input (nesne; değerler şablon)
//	condition   left (şablon), op*, right (şablon)   ops: bkz. validOp
//	loop        items* (şablon: JSON dizi ya da satır satır metin), gövde 'body' dalıdır
//	transform   template* (şablon), json (true ise sonuç JSON olarak ayrıştırılır)
//	output      template (varsayılan {{prev}})
// (* = zorunlu)

const (
	defaultMemoryLimit = 10
	maxMemoryLimit     = 50
	defaultRAGLimit    = 3
	maxRAGLimit        = 10
)

// Run akışı baştan sona yürütür. exec.Steps adım adım dolar; her değişiklikte notify çağrılır
// (nil olabilir). Dönüş değeri nil ise exec.Output doldurulmuştur. Durum/zaman alanlarını
// (Status, FinishedAt, LatencyMs, Error) ÇAĞIRAN belirler: Run yalnızca adımları, token ve
// tool sayaçlarını ve Output'u yönetir.
func Run(ctx context.Context, wf *Workflow, deps Deps, env RunEnv, exec *Execution, notify func(Execution)) error {
	if errs := Check(wf); len(errs) > 0 {
		return &ValidationError{Errors: errs, NotRunnable: true}
	}
	if missing := deps.Missing(wf); len(missing) > 0 {
		return &CapabilityError{Types: missing}
	}

	r := &runner{
		deps: deps, env: env, exec: exec, notify: notify,
		nodes: make(map[string]*Node, len(wf.Nodes)),
		edges: make(map[string]string, len(wf.Edges)),
		vars: map[string]any{
			"input": env.Input, "prev": env.Input, "conversationId": env.ConversationID,
		},
	}
	var inputID string
	for i := range wf.Nodes {
		n := wf.Nodes[i]
		n.Config = normalizeConfig(n.Config)
		r.nodes[n.ID] = &n
		if n.Type == NodeInput {
			inputID = n.ID
		}
	}
	for _, e := range wf.Edges {
		r.edges[e.From+"|"+e.Branch] = e.To
	}

	if err := r.walk(ctx, inputID, 0); err != nil {
		return err
	}
	out := stringify(r.vars["prev"])
	if r.hasResult {
		out = r.result
	}
	exec.Output = truncateRunes(out, MaxOutputRunes)
	return nil
}

// CapabilityError akış, bağlı olmayan bir yetenek gerektiriyor (ör. llm node'u ama LLM yok).
type CapabilityError struct{ Types []NodeType }

func (e *CapabilityError) Error() string {
	names := make([]string, 0, len(e.Types))
	for _, t := range e.Types {
		names = append(names, string(t))
	}
	return "workflow: bu sunucuda şu node türleri çalıştırılamıyor: " + strings.Join(names, ", ")
}

type runner struct {
	deps   Deps
	env    RunEnv
	exec   *Execution
	notify func(Execution)

	nodes map[string]*Node
	edges map[string]string // "from|branch" → to
	vars  map[string]any

	count     int // toplam çalıştırılan adım (MaxSteps)
	result    string
	hasResult bool
}

// nodeResult tek node çalıştırmasının sonucudur.
type nodeResult struct {
	out    any
	input  string // adım kaydında gösterilecek girdi (işlenmiş istem/sorgu)
	branch string // yalnızca condition
}

// walk start'tan başlayıp zincirin sonuna kadar yürür. Döngü gövdesi de bu fonksiyonla yürünür.
func (r *runner) walk(ctx context.Context, start string, iter int) error {
	cur := start
	for cur != "" {
		if err := ctx.Err(); err != nil {
			return err
		}
		if r.count >= MaxSteps {
			return fmt.Errorf("adım sınırı aşıldı (bir yürütmede en çok %d adım)", MaxSteps)
		}
		r.count++
		n := r.nodes[cur]
		idx := r.begin(n, iter)

		if n.Type == NodeLoop {
			next, err := r.runLoop(ctx, n, idx)
			if err != nil {
				return err
			}
			cur = next
			continue
		}

		res, err := r.execNode(ctx, n)
		if err != nil {
			r.end(idx, StepError, res.input, "", res.branch, err.Error())
			return fmt.Errorf("%s: %w", nodeName(n), err)
		}
		r.set(n, res.out)
		r.end(idx, StepOK, res.input, stringify(res.out), res.branch, "")

		switch n.Type {
		case NodeOutput:
			return nil // zincir biter (döngü gövdesindeyse yalnızca o yineleme)
		case NodeCondition:
			cur = r.edges[n.ID+"|"+res.branch]
		default:
			cur = r.edges[n.ID+"|"+BranchNext]
		}
	}
	return nil
}

// runLoop loop node'unu çalıştırır ve döngüden sonraki node'u döndürür.
func (r *runner) runLoop(ctx context.Context, n *Node, idx int) (string, error) {
	raw := cfgString(n.Config, "items")
	v, err := r.resolveValue(raw)
	if err != nil {
		r.end(idx, StepError, raw, "", "", err.Error())
		return "", fmt.Errorf("%s: %w", nodeName(n), err)
	}
	items := toList(v)
	if len(items) > MaxLoopIterations {
		err := fmt.Errorf("döngü en çok %d öğe işler (gelen: %d)", MaxLoopIterations, len(items))
		r.end(idx, StepError, raw, "", "", err.Error())
		return "", fmt.Errorf("%s: %w", nodeName(n), err)
	}

	prevItem, hadItem := r.vars["item"]
	prevIndex, hadIndex := r.vars["index"]
	body := r.edges[n.ID+"|"+BranchBody]
	results := make([]any, 0, len(items))
	for i, it := range items {
		r.vars["item"], r.vars["index"] = it, i
		if err := r.walk(ctx, body, i+1); err != nil {
			r.end(idx, StepError, fmt.Sprintf("%d öğe", len(items)), "", "", err.Error())
			return "", err
		}
		results = append(results, r.vars["prev"])
	}
	restore(r.vars, "item", prevItem, hadItem)
	restore(r.vars, "index", prevIndex, hadIndex)

	r.vars[n.ID] = results
	r.vars["prev"] = results
	r.end(idx, StepOK, fmt.Sprintf("%d öğe", len(items)), stringify(results), "", "")
	return r.edges[n.ID+"|"+BranchNext], nil
}

func restore(vars map[string]any, key string, v any, had bool) {
	if had {
		vars[key] = v
	} else {
		delete(vars, key)
	}
}

// execNode loop dışındaki node türlerini çalıştırır.
func (r *runner) execNode(ctx context.Context, n *Node) (nodeResult, error) {
	cfg := n.Config
	switch n.Type {
	case NodeInput:
		return nodeResult{out: r.env.Input, input: preview(r.env.Input)}, nil

	case NodeMemory:
		limit := clamp(cfgInt(cfg, "limit", defaultMemoryLimit), 1, maxMemoryLimit)
		if r.env.ConversationID == "" {
			return nodeResult{out: "", input: "konuşma yok"}, nil
		}
		msgs, err := r.deps.Memory.Recent(ctx, r.env.UserID, r.env.ConversationID, limit)
		if err != nil {
			return nodeResult{}, err
		}
		lines := make([]string, 0, len(msgs))
		for _, m := range msgs {
			who := "Kullanıcı"
			if m.Role == "assistant" {
				who = "Asistan"
			}
			lines = append(lines, who+": "+m.Content)
		}
		return nodeResult{out: strings.Join(lines, "\n"), input: fmt.Sprintf("son %d mesaj", limit)}, nil

	case NodeRAG:
		query, err := r.render(cfgString(cfg, "query"))
		if err != nil {
			return nodeResult{}, err
		}
		limit := clamp(cfgInt(cfg, "limit", defaultRAGLimit), 1, maxRAGLimit)
		passages, err := r.deps.Knowledge.Passages(ctx, r.env.UserID, query, limit)
		if err != nil {
			return nodeResult{input: preview(query)}, err
		}
		return nodeResult{out: strings.Join(passages, "\n\n"), input: preview(query)}, nil

	case NodeSkill:
		query, err := r.render(cfgString(cfg, "query"))
		if err != nil {
			return nodeResult{}, err
		}
		args, err := r.renderMap(cfg["arguments"])
		if err != nil {
			return nodeResult{input: preview(query)}, err
		}
		res, err := r.deps.Skills.RunSkill(ctx, r.env.UserID, query, args)
		if err != nil {
			return nodeResult{input: preview(query)}, err
		}
		if res.Tool != "" {
			r.exec.ToolCalls++
		}
		return nodeResult{
			out:   map[string]any{"skill": res.Skill, "tool": res.Tool, "output": res.Output},
			input: preview(query),
		}, nil

	case NodeLLM:
		prompt, err := r.render(cfgString(cfg, "prompt"))
		if err != nil {
			return nodeResult{}, err
		}
		system, err := r.render(cfgString(cfg, "system"))
		if err != nil {
			return nodeResult{input: preview(prompt)}, err
		}
		req := LLMRequest{
			UserID: r.env.UserID, System: system, Prompt: prompt,
			Model: strings.TrimSpace(cfgString(cfg, "model")),
		}
		if t, ok := cfgFloat(cfg, "temperature"); ok {
			req.Temperature = &t
		}
		res, err := r.deps.LLM.Complete(ctx, req)
		r.exec.PromptTokens += res.PromptTokens
		r.exec.CompletionTokens += res.CompletionTokens
		if err != nil {
			return nodeResult{input: preview(prompt)}, err
		}
		return nodeResult{out: res.Text, input: preview(prompt)}, nil

	case NodeTool:
		name, err := r.render(cfgString(cfg, "name"))
		if err != nil {
			return nodeResult{}, err
		}
		name = strings.TrimSpace(name)
		input, err := r.renderMap(cfg["input"])
		if err != nil {
			return nodeResult{input: name}, err
		}
		shown := name + " " + stringify(input)
		r.exec.ToolCalls++
		out, err := r.deps.Tools.RunTool(ctx, r.env.UserID, name, input)
		if err != nil {
			return nodeResult{input: preview(shown)}, err
		}
		return nodeResult{out: out, input: preview(shown)}, nil

	case NodeCondition:
		left, err := r.render(cfgString(cfg, "left"))
		if err != nil {
			return nodeResult{}, err
		}
		right, err := r.render(cfgString(cfg, "right"))
		if err != nil {
			return nodeResult{}, err
		}
		op := strings.ToLower(strings.TrimSpace(cfgString(cfg, "op")))
		ok, err := evalCondition(op, left, right)
		shown := preview(strings.TrimSpace(left + " " + op + " " + right))
		if err != nil {
			return nodeResult{input: shown}, err
		}
		branch := BranchFalse
		if ok {
			branch = BranchTrue
		}
		return nodeResult{out: ok, input: shown, branch: branch}, nil

	case NodeTransform:
		text, err := r.render(cfgString(cfg, "template"))
		if err != nil {
			return nodeResult{}, err
		}
		if cfgBool(cfg, "json") {
			var v any
			if err := json.Unmarshal([]byte(text), &v); err != nil {
				return nodeResult{input: preview(text)}, fmt.Errorf("sonuç geçerli JSON değil: %w", err)
			}
			return nodeResult{out: v, input: preview(text)}, nil
		}
		return nodeResult{out: text, input: preview(text)}, nil

	case NodeOutput:
		tmpl := cfgString(cfg, "template")
		if strings.TrimSpace(tmpl) == "" {
			tmpl = "{{prev}}"
		}
		text, err := r.render(tmpl)
		if err != nil {
			return nodeResult{}, err
		}
		r.result, r.hasResult = text, true
		return nodeResult{out: text, input: preview(tmpl)}, nil
	}
	return nodeResult{}, fmt.Errorf("bilinmeyen node türü: %s", n.Type)
}

// set node çıktısını değişkenlere yazar. condition `prev`i değiştirmez: araya girmiş bir
// koşul, sonraki node'un {{prev}} değerini "true"/"false" yapmamalı.
func (r *runner) set(n *Node, out any) {
	r.vars[n.ID] = out
	if n.Type != NodeCondition {
		r.vars["prev"] = out
	}
}

// ---- adım kaydı ----

func (r *runner) begin(n *Node, iter int) int {
	r.exec.Steps = append(r.exec.Steps, Step{
		Index: len(r.exec.Steps) + 1, NodeID: n.ID, NodeType: n.Type, Label: n.Label,
		Iteration: iter, Status: StepRunning, StartedAt: time.Now().UTC(),
	})
	r.emit()
	return len(r.exec.Steps) - 1
}

func (r *runner) end(idx int, status StepStatus, input, output, branch, errMsg string) {
	st := &r.exec.Steps[idx]
	now := time.Now().UTC()
	st.Status, st.Branch, st.Error = status, branch, errMsg
	st.Input, st.Output = preview(input), preview(output)
	st.FinishedAt = &now
	st.DurationMs = now.Sub(st.StartedAt).Milliseconds()
	r.emit()
}

func (r *runner) emit() {
	if r.notify != nil {
		r.notify(r.exec.Clone())
	}
}

func nodeName(n *Node) string {
	if n.Label != "" {
		return n.Label
	}
	return n.ID
}

// ---- şablon çözümleme ----

func (r *runner) known(name string) bool {
	if _, ok := r.nodes[name]; ok {
		return true
	}
	return reservedVar(name)
}

func (r *runner) lookup(path string) (any, error) {
	parts := strings.Split(path, ".")
	v, ok := r.vars[parts[0]]
	if !ok {
		if r.known(parts[0]) {
			return nil, nil // henüz çalışmamış node ya da döngü dışında item/index
		}
		return nil, fmt.Errorf("bilinmeyen değişken %q", parts[0])
	}
	for _, p := range parts[1:] {
		switch x := v.(type) {
		case map[string]any:
			v = x[p]
		case []any:
			i, err := strconv.Atoi(p)
			if err != nil || i < 0 || i >= len(x) {
				return nil, nil
			}
			v = x[i]
		default:
			return nil, nil
		}
	}
	return v, nil
}

func (r *runner) render(s string) (string, error) {
	var firstErr error
	out := tokenRe.ReplaceAllStringFunc(s, func(tok string) string {
		path := tokenRe.FindStringSubmatch(tok)[1]
		v, err := r.lookup(path)
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			return ""
		}
		return stringify(v)
	})
	return out, firstErr
}

// resolveValue metin tek bir {{...}} ise ham değeri (nesne/dizi/sayı), değilse işlenmiş metni döndürür.
func (r *runner) resolveValue(s string) (any, error) {
	t := strings.TrimSpace(s)
	if m := tokenRe.FindStringSubmatch(t); m != nil && m[0] == t {
		return r.lookup(m[1])
	}
	return r.render(s)
}

// renderAny config değerlerini özyinelemeli işler: metinler şablon, diğerleri olduğu gibi.
func (r *runner) renderAny(v any) (any, error) {
	switch x := v.(type) {
	case string:
		return r.resolveValue(x)
	case map[string]any:
		out := make(map[string]any, len(x))
		for k, val := range x {
			rv, err := r.renderAny(val)
			if err != nil {
				return nil, err
			}
			out[k] = rv
		}
		return out, nil
	case []any:
		out := make([]any, len(x))
		for i, val := range x {
			rv, err := r.renderAny(val)
			if err != nil {
				return nil, err
			}
			out[i] = rv
		}
		return out, nil
	}
	return v, nil
}

// renderMap config'teki nesneyi işler; nil/boş ise boş harita döner.
func (r *runner) renderMap(v any) (map[string]any, error) {
	m, ok := v.(map[string]any)
	if !ok || len(m) == 0 {
		return map[string]any{}, nil
	}
	out, err := r.renderAny(m)
	if err != nil {
		return nil, err
	}
	return out.(map[string]any), nil
}

// ---- yardımcılar ----

// stringify değeri metne çevirir: metin olduğu gibi, tam sayılar ondalıksız, nesne/dizi JSON.
func stringify(v any) string {
	switch x := v.(type) {
	case nil:
		return ""
	case string:
		return x
	case bool:
		return strconv.FormatBool(x)
	case float64:
		if x == math.Trunc(x) && math.Abs(x) < 1e15 {
			return strconv.FormatInt(int64(x), 10)
		}
		return strconv.FormatFloat(x, 'f', -1, 64)
	case int:
		return strconv.Itoa(x)
	case int64:
		return strconv.FormatInt(x, 10)
	}
	b, err := json.Marshal(v)
	if err != nil {
		return fmt.Sprint(v)
	}
	return string(b)
}

// toList döngü öğelerini çıkarır: dizi olduğu gibi; metin JSON dizi ise ayrıştırılır, değilse
// boş olmayan satırlara bölünür; nil boş liste; diğer değerler tek öğedir.
func toList(v any) []any {
	switch x := v.(type) {
	case nil:
		return nil
	case []any:
		return x
	case string:
		t := strings.TrimSpace(x)
		if t == "" {
			return nil
		}
		if strings.HasPrefix(t, "[") {
			var arr []any
			if json.Unmarshal([]byte(t), &arr) == nil {
				return arr
			}
		}
		var out []any
		for _, line := range strings.Split(t, "\n") {
			if line = strings.TrimSpace(line); line != "" {
				out = append(out, line)
			}
		}
		return out
	}
	return []any{v}
}

// normalizeConfig config'i JSON gidiş-dönüşüyle düz Go tiplerine çevirir: Mongo'dan gelen
// bson.A / int32 gibi tipler tip anahtarlarını (map[string]any, []any, float64) bozmasın.
func normalizeConfig(c map[string]any) map[string]any {
	if len(c) == 0 {
		return map[string]any{}
	}
	raw, err := json.Marshal(c)
	if err != nil {
		return c
	}
	var out map[string]any
	if json.Unmarshal(raw, &out) != nil || out == nil {
		return c
	}
	return out
}

func clamp(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}

func truncateRunes(s string, max int) string {
	if runes(s) <= max {
		return s
	}
	return string([]rune(s)[:max]) + "…"
}

// ---- koşullar ----

// conditionOps condition node'unun operatörleridir.
var conditionOps = []string{
	"eq", "ne", "contains", "not_contains", "starts_with", "ends_with",
	"gt", "gte", "lt", "lte", "empty", "not_empty",
}

func validOp(op string) bool {
	for _, o := range conditionOps {
		if o == op {
			return true
		}
	}
	return false
}

// evalCondition sol/sağ metinleri op ile karşılaştırır. eq/ne her iki taraf sayıysa sayısal,
// değilse kırpılmış metin karşılaştırır; contains/starts/ends büyük-küçük harf duyarsızdır;
// gt/gte/lt/lte sayısal olmayan değerde HATA verir (sessizce false demez).
func evalCondition(op, left, right string) (bool, error) {
	l, r := strings.TrimSpace(left), strings.TrimSpace(right)
	ln, lerr := strconv.ParseFloat(l, 64)
	rn, rerr := strconv.ParseFloat(r, 64)
	bothNum := lerr == nil && rerr == nil

	switch op {
	case "eq":
		if bothNum {
			return ln == rn, nil
		}
		return l == r, nil
	case "ne":
		if bothNum {
			return ln != rn, nil
		}
		return l != r, nil
	case "contains":
		return strings.Contains(strings.ToLower(l), strings.ToLower(r)), nil
	case "not_contains":
		return !strings.Contains(strings.ToLower(l), strings.ToLower(r)), nil
	case "starts_with":
		return strings.HasPrefix(strings.ToLower(l), strings.ToLower(r)), nil
	case "ends_with":
		return strings.HasSuffix(strings.ToLower(l), strings.ToLower(r)), nil
	case "empty":
		return l == "", nil
	case "not_empty":
		return l != "", nil
	case "gt", "gte", "lt", "lte":
		if !bothNum {
			return false, fmt.Errorf("%q sayısal karşılaştırma için sayı gerekli (sol=%q, sağ=%q)", op, l, r)
		}
		switch op {
		case "gt":
			return ln > rn, nil
		case "gte":
			return ln >= rn, nil
		case "lt":
			return ln < rn, nil
		default:
			return ln <= rn, nil
		}
	}
	return false, fmt.Errorf("geçersiz operatör: %q", op)
}
