package builtin

// biz.go şirket yönetimi tool'ları: biz.add, biz.find, biz.update, biz.delete, biz.summary, biz.vat.
//
// Neden az sayıda genel tool? Küçük yerel modeller 30+ tool'u karıştırır ve istem şişer.
// Bunun yerine 6 tool + `collection` alanı kullanılır; alan adları ve kuralları tool
// açıklamasında ve sunucu tarafı doğrulamasında tutulur (model hatalı veri gönderse bile
// kayıt bozulmaz).
//
// Yetkiler: okuma (find/summary) read·low, yazma (add/update) write·medium, silme delete·high.
// Silme, varsayılan politikada kapalıdır; kullanıcı izin ekranından açıkça açmalıdır.

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"fiko-ai/internal/tool"
)

func bizToday(o Options) string { return o.clock().In(time.Local).Format("2006-01-02") }

func specList() string {
	names := make([]string, 0, len(bizSpecs))
	for _, sp := range bizSpecs {
		names = append(names, sp.Name)
	}
	return strings.Join(names, ", ")
}

// bizDefinitions şirket yönetimi tool'larını ve depolarını kurar. Bozuk veri dosyası hata verir.
// confirmedParam, onay akışının ortak girdisidir. Modele "true" yazmayı öğretmemek için
// AÇIKLAMAYA özellikle "bunu kendin gönderme, yalnızca kullanıcı onayladıktan sonra" notu
// düşülür. Ajan katmanı bu alanı model çağrısından ayırıp kendisi ekler (bkz. confirmFromUser).
var confirmedParam = obj(
	"type", "boolean",
	"description", "true yalnızca kullanıcı arayüzdeki onay formunda Onayla'ya bastıysa. Kendin gönderme; onay yoksa false/boş bırak.",
	"default", false,
)

func bizDefinitions(o Options) ([]Def, error) {
	st, err := newBizStore(o.BizPath)
	if err != nil {
		return nil, err
	}
	colProp := obj("type", "string", "description", "Koleksiyon", "enum", specNames())
	refProp := str("Kayıt kimliği (id) ya da benzersiz değer: SKU, fatura no, müşteri/personel adı, görev başlığı")
	docs := describeSpecs()

	return []Def{
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.add",
				Title: "Şirket kaydı ekle",
				Description: "Şirket verisine yeni kayıt ekler (müşteri, görev, fatura, gider, stok ürünü, personel, not, proje). " +
					"data içinde yalnızca aşağıdaki alanlar kullanılır; * zorunlu. Tarih YYYY-MM-DD. " +
					"Fatura: net ve vatRate verilir, KDV/brüt tutar ve numara otomatik hesaplanır. Alanlar — " + docs,
				Tags:        []string{"biz", "sirket"},
				Risk:        tool.RiskMedium,
				Permissions: []string{"write"},
				InputSchema: obj("type", "object",
					"properties", obj("collection", colProp, "data", obj("type", "object", "description", "Kayıt alanları"), "confirmed", confirmedParam),
					"required", []any{"collection", "data"},
				),
				OutputSchema: obj("type", "object", "properties", obj("record", obj("type", "object"))),
				TimeoutMs:    10_000,
				UI:           tool.UIForm, // onay formu → arayüzde doldurulabilir alanlar (bkz. confirm.go)
			},
			Handler: bizAddHandler(st, o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.find",
				Title: "Şirket kaydı bul",
				Description: "Şirket kayıtlarını listeler/arar. query tüm metin alanlarında aksan ve büyük/küçük harf duyarsız arar; " +
					"filter {alan: değer} ile tam eşleşme (örn. {\"status\":\"open\"}); from/to tarih aralığı (görev: termin, " +
					"fatura: düzenleme, gider: tarih). Güncelleme/silme öncesi kaydı bulmak için kullan.",
				Tags:        []string{"biz", "sirket"},
				Risk:        tool.RiskLow,
				Permissions: []string{"read"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"collection", colProp,
						"query", obj("type", "string", "description", "Aranacak metin", "maxLength", 200),
						"filter", obj("type", "object", "description", "Tam eşleşme süzgeci"),
						"from", str("Başlangıç tarihi (YYYY-MM-DD)"),
						"to", str("Bitiş tarihi (YYYY-MM-DD)"),
						"limit", obj("type", "integer", "default", 20, "minimum", 1, "maximum", 100),
					),
					"required", []any{"collection"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"records", obj("type", "array", "items", obj("type", "object")),
					"count", obj("type", "integer"), "total", obj("type", "integer"),
				)),
				TimeoutMs: 10_000,
			},
			Handler: bizFindHandler(st),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.update",
				Title: "Şirket kaydı güncelle",
				Description: "Var olan kaydı günceller. changes: değişecek alanlar (boş metin alanı temizler). " +
					"increment: sayısal alanlara ekle/çıkar, örn. stok girişi {\"stock\": 10}, çıkışı {\"stock\": -3}, " +
					"izin kullanımı {\"leaveBalance\": -2}. Sayı eksiye düşerse reddedilir. Görevi bitirmek: changes {\"status\":\"done\"}; " +
					"faturayı ödenmiş yapmak: {\"status\":\"paid\"}.",
				Tags:        []string{"biz", "sirket"},
				Risk:        tool.RiskMedium,
				Permissions: []string{"write"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"collection", colProp, "ref", refProp,
						"changes", obj("type", "object", "description", "Değişecek alanlar"),
						"increment", obj("type", "object", "description", "Sayısal alana eklenecek miktar (negatif düşer)"),
					),
					"required", []any{"collection", "ref"},
				),
				OutputSchema: obj("type", "object", "properties", obj("record", obj("type", "object"))),
				TimeoutMs:    10_000,
			},
			Handler: bizUpdateHandler(st, o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.delete",
				Title: "Şirket kaydı sil",
				Description: "Şirket kaydını (proje, müşteri, görev, fatura, gider, ürün, personel, not...) SİLER. " +
					"Önce biz.find ile kaydı bul, sonra collection + ref ile çağır. Çağırınca kullanıcıya ONAY FORMU çıkar; " +
					"kayıt ancak kullanıcı formda Onayla'ya basınca silinir — bu yüzden 'sildim' DEME, 'onay formunu bekliyorum' de. " +
					"GERİ ALINAMAZ. Proje silinirse projeye bağlı görev, kilometre taşı, risk ve sprint kayıtları da silinir. " +
					"ref: id, SKU, fatura no, proje kodu/adı, müşteri/personel adı ya da görev başlığı.",
				Tags: []string{"biz", "sirket"},
				// Risk MEDIUM: silmeyi güvenli yapan şey risk etiketi değil, araca gömülü ONAY FORMUDUR
				// (handler `confirmed` olmadan asla silmez; bayrağı modelden değil yalnızca ajan verir).
				// HIGH olduğunda araç modele hiç sunulmuyordu → "proje silinemiyor" (canlı hata).
				// `delete` izni KALIR: kullanıcı izin ekranından silmeyi tümden kapatabilir.
				Risk:        tool.RiskMedium,
				Permissions: []string{"delete"},
				InputSchema: obj("type", "object",
					"properties", obj("collection", colProp, "ref", refProp, "confirmed", confirmedParam),
					"required", []any{"collection", "ref"},
				),
				OutputSchema: obj("type", "object", "properties", obj("deleted", obj("type", "boolean"))),
				TimeoutMs:    10_000,
				UI:           tool.UIForm,
			},
			Handler: bizDeleteHandler(st, o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.summary",
				Title: "Şirket özeti",
				Description: "Şirketin anlık durum özetini verir: açık/geciken görevler, tahsil edilmemiş ve vadesi geçmiş faturalar, " +
					"bu ay fatura/gider, kritik stok, müşteri ve personel sayısı. 'Durum ne?', 'bugün ne var?' gibi sorularda kullan. " +
					"ÇIKTI 'collections' ve 'nonEmptyCollections' alanlarını içerir: hangi koleksiyonda kaç kayıt GERÇEKTEN var. " +
					"ŞİMDİ veri olmayan bir koleksiyondan (count 0) içerik uydurma; kullanıcı bilgi verdiyse önce biz.add ile kaydet. " +
					"Bu tool GEÇMİŞİ gösterir; yeni bilgi vermeden 'özetle' demek, mevcut veriyi tekrarlamaktır.",
				Tags:         []string{"biz", "sirket", "rapor"},
				Risk:         tool.RiskLow,
				Permissions:  []string{"read"},
				InputSchema:  obj("type", "object", "properties", obj()),
				OutputSchema: obj("type", "object"),
				UI:           tool.UIMetrics, // sayısal sayaçlar → metrik kartları
				TimeoutMs:    10_000,
			},
			Handler: bizSummaryHandler(st, o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.vat",
				Title: "KDV hesapla",
				Description: "KDV hesaplar. from=net: tutar KDV hariç (varsayılan); from=gross: tutar KDV dahil. " +
					"rate yüzde olarak (varsayılan 20). Kayıt tutmaz; saf hesaplamadır.",
				Tags:        []string{"biz", "hesap"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"amount", obj("type", "number", "description", "Tutar", "minimum", 0),
						"rate", obj("type", "number", "description", "KDV oranı (%)", "default", 20, "minimum", 0, "maximum", 100),
						"from", obj("type", "string", "enum", []any{"net", "gross"}, "default", "net"),
					),
					"required", []any{"amount"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"net", obj("type", "number"), "vat", obj("type", "number"), "gross", obj("type", "number"),
				)),
				UI:        tool.UIMetrics, // net/KDV/brüt → üç metrik kartı
				TimeoutMs: 5_000,
			},
			Handler: bizVatHandler,
		},
		// ---- PROJE YÖNETİMİ ----
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.project",
				Title: "Proje durumu",
				Description: "Bir projenin yönetim panosunu verir: ilerleme yüzdesi ve kalan gün, faza göre görev " +
					"dağılımı (backlog/todo/doing/done/blocked), geciken ve bu hafta bitecek işler, kilometre taşları, " +
					"açık riskler (etki×olasılık), sprint ve bütçe. 'Proje nasıl gidiyor?', 'ne kaldı?' sorularında kullan.",
				Tags:         []string{"biz", "proje", "rapor"},
				Risk:         tool.RiskLow,
				Permissions:  []string{"read"},
				InputSchema:  obj("type", "object", "properties", obj("ref", str("Proje referansı: id, code veya ad"))),
				OutputSchema: obj("type", "object"),
				TimeoutMs:    10_000,
			},
			Handler: bizProjectHandler(st, o),
		},
		{
			Enabled: true,
			Tool: tool.Tool{
				Name:  "biz.plan",
				Title: "Proje planla",
				Description: "Proje planlaması yapar. Verilen hedef tarih (deadline) ve mevcut duruma göre " +
					"gerekli gün sayısını, haftalık kapasiteye göre dağıtımı ve geç kalınacak noktaları hesaplar. " +
					"capacity günlük kişi-gün; records sonuç KAYDETMEZ, plan çıktısıdır. " +
					"Gerçek planı biz.add (collection=projects, startDate/plannedEnd) ile kaydet.",
				Tags:        []string{"biz", "proje", "planlama"},
				Risk:        tool.RiskLow,
				Permissions: []string{"execute"},
				InputSchema: obj("type", "object",
					"properties", obj(
						"ref", str("Proje referansı: id, code veya ad (boşsa yeni proje planı)"),
						"tasks", obj("type", "array", "description", "Görev tahminleri (gün)",
							"items", obj("type", "object", "properties", obj(
								"title", str("Görev"), "estimate", obj("type", "number"), "assignee", str("Sorumlu"),
							))),
						"capacity", obj("type", "number", "description", "Günlük kapasite (kişi-gün)", "default", 1, "minimum", 0),
						"startDate", str("Başlangıç (YYYY-MM-DD); boşsa bugün"),
						"deadline", str("Hedef bitiş tarihi (YYYY-MM-DD)"),
					),
				),
				OutputSchema: obj("type", "object"),
				TimeoutMs:    5_000,
			},
			Handler: bizPlanHandler(st, o),
		},
	}, nil
}

func bizAddHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		sp, ok := specFor(in["collection"])
		if !ok {
			return nil, Fail(t.Name, "bilinmeyen collection; geçerli: %s", specList())
		}
		data, _ := in["data"].(map[string]any)
		if len(data) == 0 {
			return nil, Fail(t.Name, "data boş olamaz")
		}
		today := bizToday(o)

		// Onay akışı: kullanıcı politika gereği istiyorsa form gösterilir.
		// Konuşmadan gelen alanlar forma DOLDURULUR; eksik zorunlu alanlar
		// kullanıcıya form üstünde sorulur (metin cevabı beklenmez).
		if !isConfirmed(in) {
			fields := addConfirmFields(sp, data, today)
			// risky=false: ekleme geri alınabilir (kayıt silinebilir/düzeltilebilir).
			// Politika kararı AJANINDADIR (bkz. agent.confirmRequired); burada form
			// her zaman üretilir, gerekip gerekmediğine ajan bakar.
			return onayFormu(
				t.Name, "add",
				fmt.Sprintf("%s kaydı eklensin mi?", sp.Label),
				"Aşağıdaki alanları kontrol edip onaylayın. Eksik alanları doldurabilirsiniz.",
				fields,
				map[string]any{"collection": sp.Name, "data": data},
				false, "Kaydet", "Vazgeç"), nil
		}

		fields, errs := sp.normalize(data, false, today)
		if len(errs) > 0 {
			return nil, Fail(t.Name, "geçersiz veri → %s", strings.Join(errs, "; "))
		}
		var created map[string]any
		err := st.mutate(sp.Name, func(recs []map[string]any) ([]map[string]any, error) {
			rec := fields
			if sp.derive != nil {
				sp.derive(rec, recs, true, today)
			}
			id := newBizID(sp.Prefix)
			if err := sp.checkUnique(rec, recs, id); err != nil {
				return nil, err
			}
			now := o.clock().UTC().Format(time.RFC3339)
			rec["id"], rec["createdAt"], rec["updatedAt"] = id, now, now
			created = cloneRec(rec)
			next := make([]map[string]any, len(recs), len(recs)+1)
			copy(next, recs)
			return append(next, rec), nil
		})
		if err != nil {
			return nil, Fail(t.Name, "%s", err.Error())
		}
		return map[string]any{"collection": sp.Name, "created": true, "record": created}, nil
	}
}

func recordMatches(sp *bizSpec, r map[string]any, q string) bool {
	if id, _ := r["id"].(string); fold(id) == q {
		return true
	}
	for _, f := range sp.Fields {
		if s, ok := r[f.Name].(string); ok && strings.Contains(fold(s), q) {
			return true
		}
	}
	return false
}

func bizFindHandler(st *bizStore) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		sp, ok := specFor(in["collection"])
		if !ok {
			return nil, Fail(t.Name, "bilinmeyen collection; geçerli: %s", specList())
		}
		q := ""
		if s, ok := in["query"].(string); ok {
			q = fold(s)
		}
		filter, _ := in["filter"].(map[string]any)
		want := map[string]any{}
		for k, v := range filter {
			f := sp.field(k)
			if f == nil {
				return nil, Fail(t.Name, "filter: %s bilinmeyen alan", k)
			}
			val, cleared, problem := f.coerce(v)
			if problem != "" {
				return nil, Fail(t.Name, "filter %s: %s", k, problem)
			}
			if !cleared {
				want[k] = val
			}
		}
		from, to := "", ""
		for _, p := range []struct {
			key string
			dst *string
		}{{"from", &from}, {"to", &to}} {
			s, _ := in[p.key].(string)
			if strings.TrimSpace(s) == "" {
				continue
			}
			if sp.DateField == "" {
				return nil, Fail(t.Name, "%s koleksiyonunda tarih aralığı süzgeci yok", sp.Name)
			}
			d, ok := parseBizDate(s)
			if !ok {
				return nil, Fail(t.Name, "%s tarihi YYYY-MM-DD olmalı", p.key)
			}
			*p.dst = d
		}
		limit := intInput(in, "limit", 20, 1, 100)

		var matched []map[string]any
	rows:
		for _, r := range st.read(sp.Name) {
			if q != "" && !recordMatches(sp, r, q) {
				continue
			}
			for k, v := range want {
				switch x := v.(type) {
				case string:
					if s, _ := r[k].(string); fold(s) != fold(x) {
						continue rows
					}
				default:
					if r[k] != v {
						continue rows
					}
				}
			}
			if from != "" || to != "" {
				d, _ := r[sp.DateField].(string)
				if d == "" || (from != "" && d < from) || (to != "" && d > to) {
					continue
				}
			}
			matched = append(matched, r)
		}

		key := "updatedAt"
		if sp.DateField != "" {
			key = sp.DateField
		}
		sort.SliceStable(matched, func(i, j int) bool {
			a, _ := matched[i][key].(string)
			b, _ := matched[j][key].(string)
			if sp.SortAsc && sp.DateField != "" {
				// Tarihsizler sona.
				if a == "" || b == "" {
					return a != "" && b == ""
				}
				return a < b
			}
			return a > b
		})
		total := len(matched)
		if len(matched) > limit {
			matched = matched[:limit]
		}
		out := make([]any, 0, len(matched))
		for _, r := range matched {
			out = append(out, cloneRec(r))
		}
		res := map[string]any{"collection": sp.Name, "records": out, "count": len(out), "total": total}

		// Sonuç boşsa model NEDEN boş olduğunu bilmelidir. Aksi halde "kayıt yok"
		// ile "yanlış koleksiyon/süzgeç" karışır ve model veri uydurur
		// (canlı hata: hiç kaydı olmayan koleksiyonlardan sınıf/müşteri listeleme).
		if len(out) == 0 {
			res["empty"] = true
			res["collectionTotal"] = len(st.read(sp.Name))
			var reason string
			switch {
			case res["collectionTotal"] == 0:
				reason = fmt.Sprintf("'%s' koleksiyonunda HİÇ KAYIT YOK. Kayıt yoksa bu bir hata değil; "+
					"kullanıcı bilgi verdiyse önce biz.add ile kaydet. Olmayan veriyi uydurma.", sp.Name)
			case q != "":
				reason = fmt.Sprintf("'%s' koleksiyonunda %d kayıt var ama '%s' araması eşleşmedi. "+
					"Benzer kayıt için query'yi kısaltmayı dene ya da filtreyi gevşet.", sp.Name, res["collectionTotal"], strings.TrimSpace(q))
			default:
				reason = fmt.Sprintf("'%s' koleksiyonunda %d kayıt var ama verilen filtreye uyan kayıt yok. "+
					"Filtre alanlarını gözden geçir.", sp.Name, res["collectionTotal"])
			}
			res["note"] = reason
		}
		return res, nil
	}
}

func numberDelta(v any) (float64, bool) {
	switch x := v.(type) {
	case float64:
		return x, true
	case int:
		return float64(x), true
	case int64:
		return float64(x), true
	case string:
		n, err := strconv.ParseFloat(strings.ReplaceAll(strings.TrimSpace(x), ",", "."), 64)
		return n, err == nil
	}
	return 0, false
}

func bizUpdateHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		sp, ok := specFor(in["collection"])
		if !ok {
			return nil, Fail(t.Name, "bilinmeyen collection; geçerli: %s", specList())
		}
		ref, _ := in["ref"].(string)
		changes, _ := in["changes"].(map[string]any)
		incRaw, _ := in["increment"].(map[string]any)
		if len(changes) == 0 && len(incRaw) == 0 {
			return nil, Fail(t.Name, "changes ya da increment verilmeli")
		}
		today := bizToday(o)
		norm, errs := sp.normalize(changes, true, today)
		incs := map[string]float64{}
		for k, v := range incRaw {
			f := sp.field(k)
			if f == nil || f.Kind != "number" {
				errs = append(errs, k+": increment yalnızca sayısal alanlar için")
				continue
			}
			d, ok := numberDelta(v)
			if !ok {
				errs = append(errs, k+": increment sayı olmalı")
				continue
			}
			incs[k] = d
		}
		if len(errs) > 0 {
			return nil, Fail(t.Name, "geçersiz veri → %s", strings.Join(errs, "; "))
		}

		var updated map[string]any
		err := st.mutate(sp.Name, func(recs []map[string]any) ([]map[string]any, error) {
			idx, err := sp.resolve(recs, ref)
			if err != nil {
				return nil, err
			}
			rec := cloneRec(recs[idx])
			for k, v := range norm {
				if v == nil {
					delete(rec, k)
				} else {
					rec[k] = v
				}
			}
			for k, d := range incs {
				cur, _ := rec[k].(float64)
				nv := round(cur+d, 4)
				if nv < 0 {
					return nil, fmt.Errorf("%s eksiye düşemez (mevcut %g, değişim %g)", k, cur, d)
				}
				if f := sp.field(k); f.Max > 0 && nv > f.Max {
					return nil, fmt.Errorf("%s en çok %g olabilir", k, f.Max)
				}
				rec[k] = nv
			}
			if sp.derive != nil {
				sp.derive(rec, recs, false, today)
			}
			id, _ := rec["id"].(string)
			if err := sp.checkUnique(rec, recs, id); err != nil {
				return nil, err
			}
			rec["updatedAt"] = o.clock().UTC().Format(time.RFC3339)
			next := make([]map[string]any, len(recs))
			copy(next, recs)
			next[idx] = rec
			updated = cloneRec(rec)
			return next, nil
		})
		if err != nil {
			return nil, Fail(t.Name, "%s", err.Error())
		}
		return map[string]any{"collection": sp.Name, "updated": true, "record": updated}, nil
	}
}

// addConfirmFields, yeni kayıt için onay formu alanlarını üretir.
//
// Konuşmadan gelen `data` değerleri forma DOLDURULUR; gelmeyenler boş ve required
// olarak işaretlenir — kullanıcı eksikleri form üstünde doldurur. Enum alanları seçim
// kutusuna, tarihler YYYY-MM-DD olarak gösterilir (modelin sık yaptığı biçim hatası).
func addConfirmFields(sp *bizSpec, data map[string]any, today string) []ConfirmField {
	fields := make([]ConfirmField, 0, len(sp.Fields)+1)
	fields = append(fields, fSabit("collection", "Koleksiyon", sp.Name+" ("+sp.Label+")"))
	for _, f := range sp.Fields {
		val := ""
		if v, ok := data[f.Name]; ok && v != nil {
			switch x := v.(type) {
			case string:
				val = x
			case float64:
				val = fmt.Sprint(x)
			default:
				val = fmt.Sprint(x)
			}
		}
		// Varsayılan değerleri forma YAZ: kullanıcı ne kaydedileceğini görsün.
		if strings.TrimSpace(val) == "" && f.Default != nil && f.Default != todayDefault {
			val = fmt.Sprint(f.Default)
		}
		switch {
		case f.Kind == "enum":
			fields = append(fields, fSecim(f.Name, f.Name+enumLabel(f), val, f.Enum))
		case f.Kind == "date":
			if val == "" && f.Default == todayDefault {
				val = today
			}
			fields = append(fields, fTarih(f.Name, f.Name, val))
		case f.Kind == "number":
			if f.Required {
				fields = append(fields, fZorunluSayi(f.Name, f.Name, val))
			} else {
				fields = append(fields, fSayi(f.Name, f.Name, val))
			}
		case f.Required:
			fields = append(fields, fZorunlu(f.Name, f.Name+enumLabel(f), val))
		default:
			fields = append(fields, fMetin(f.Name, f.Name, val))
		}
	}
	return fields
}

// enumLabel, enum alanının geçerli değerlerini parantez içinde gösterir.
// Modelin sık yaptığı hata: "active" yerine "Aktif"/"true" yazmak. Seçenekler
// formda görünce model de kullanıcı da doğru değeri seçer.
func enumLabel(f bizField) string {
	if len(f.Enum) == 0 {
		return ""
	}
	return " (" + strings.Join(f.Enum, "|") + ")"
}

// bizDeleteHandler KAYIT SİLER — geri alınamaz. Bu yüzden çalışmadan önce kullanıcı
// onayı ister (bkz. confirm.go): model tek başına veri kaybettiremez.
//
// Akış: model çağırır → kayıt ÇÖZÜLÜR (yoksa hata) → ONAY FORMU döner → kullanıcı
// Onayla'ya basınca araç confirmed:true ile TEKRAR çağrılır ve bu kez siler.
func bizDeleteHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		sp, ok := specFor(in["collection"])
		if !ok {
			return nil, Fail(t.Name, "bilinmeyen collection; geçerli: %s", specList())
		}
		ref, _ := in["ref"].(string)

		// Kayıt önce ÇÖZÜMLENMELİ: onay formu kullanıcıya ne silineceğini göstermeli.
		// Çözülemezse form üretmek anlamsız; anlaşılır hata döner.
		recs := st.read(sp.Name)
		idx, err := sp.resolve(recs, ref)
		if err != nil {
			return nil, Fail(t.Name, "%v", err)
		}
		record := recs[idx]
		id, _ := record["id"].(string)

		// Onay gelmediyse form döndür. confirmed=true ise kullanıcı Onayla'ya basmıştır.
		if !isConfirmed(in) {
			fields := deleteConfirmFields(sp, record, id)
			msg := "Bu işlem GERİ ALINAMAZ."
			if sp.Name == "projects" {
				if c := projectChildCounts(st, record); c.total() > 0 {
					msg += " Projeye bağlı " + c.describe() + " de silinecek."
				}
			}
			// destructive=true: geri alınamaz. Politika kararı AJANINDADIR.
			return onayFormu(
				t.Name, "delete",
				fmt.Sprintf("%s silinsin mi?", sp.Label),
				msg,
				fields,
				map[string]any{"collection": sp.Name, "ref": ref},
				true, "Sil", "Vazgeç"), nil
		}

		var removed map[string]any
		err = st.mutate(sp.Name, func(cur []map[string]any) ([]map[string]any, error) {
			idx, err := sp.resolve(cur, ref)
			if err != nil {
				return nil, err
			}
			removed = cloneRec(cur[idx])
			next := make([]map[string]any, 0, len(cur)-1)
			next = append(next, cur[:idx]...)
			return append(next, cur[idx+1:]...), nil
		})
		if err != nil {
			return nil, Fail(t.Name, "%s", err.Error())
		}
		out := map[string]any{"collection": sp.Name, "deleted": true, "record": removed}
		// Proje silindiyse sahipsiz kalacak alt kayıtları da temizle (görev/kilometre taşı/risk/sprint).
		if sp.Name == "projects" {
			cascade := map[string]any{}
			for _, col := range projectChildCols {
				n := 0
				cerr := st.mutate(col, func(cur []map[string]any) ([]map[string]any, error) {
					next := make([]map[string]any, 0, len(cur))
					for _, r := range cur {
						if sameProject(r, removed) {
							n++
							continue
						}
						next = append(next, r)
					}
					return next, nil
				})
				if cerr != nil {
					return nil, Fail(t.Name, "proje silindi ama bağlı %s kayıtları silinemedi: %s", col, cerr.Error())
				}
				if n > 0 {
					cascade[col] = n
				}
			}
			if len(cascade) > 0 {
				out["cascade"] = cascade
			}
		}
		return out, nil
	}
}

// projectChildCols, proje silinince birlikte silinen alt koleksiyonlardır.
var projectChildCols = []string{"projectTasks", "milestones", "risks", "sprints"}

type childCounts map[string]int

func (c childCounts) total() int {
	n := 0
	for _, v := range c {
		n += v
	}
	return n
}

// describe "3 görev, 1 kilometre taşı" biçiminde okunur özet üretir.
func (c childCounts) describe() string {
	labels := []struct{ col, label string }{
		{"projectTasks", "görev"}, {"milestones", "kilometre taşı"}, {"risks", "risk"}, {"sprints", "sprint"},
	}
	parts := make([]string, 0, len(labels))
	for _, l := range labels {
		if n := c[l.col]; n > 0 {
			parts = append(parts, fmt.Sprintf("%d %s", n, l.label))
		}
	}
	return strings.Join(parts, ", ")
}

// projectChildCounts projeye bağlı alt kayıtları sayar (silme formunda kullanıcıya gösterilir).
func projectChildCounts(st *bizStore, project map[string]any) childCounts {
	out := childCounts{}
	for _, col := range projectChildCols {
		for _, r := range st.read(col) {
			if sameProject(r, project) {
				out[col]++
			}
		}
	}
	return out
}

// isConfirmed, aracın kullanıcı onayıyla çağrıldığını söyler.
//
// Neden ayrı alan? Model onayı kendi uydurup `confirmed:true` gönderebilirdi; onu
// güvenilmez kılmanın tek yolu, alanı MODEL DEĞİL İSTEK KATMANINDAN doldurmaktır
// (bkz. agent.confirmFromUser). Ajan, onay alınmadan bu alanı modele koymaz.
func isConfirmed(in map[string]any) bool {
	b, _ := in["confirmed"].(bool)
	return b
}

// deleteConfirmFields, silinecek kaydın onay formu alanlarını üretir.
// Kayıt kimliği değiştirilemez (fSabit) ve kaydın TÜM alanları gösterilir: kullanıcı
// "bunu mu sileceğim?" sorusunu cevaplayabilmelidir.
func deleteConfirmFields(sp *bizSpec, record map[string]any, id string) []ConfirmField {
	fields := make([]ConfirmField, 0, len(sp.Fields)+2)
	fields = append(fields, fSabit("collection", "Koleksiyon", sp.Name+" ("+sp.Label+")"))
	for _, f := range sp.Fields {
		val := ""
		switch v := record[f.Name].(type) {
		case nil:
			val = ""
		case string:
			val = v
		case float64:
			val = fmt.Sprint(v)
		default:
			val = fmt.Sprint(v)
		}
		label := f.Name
		if f.Required {
			label += " *"
		}
		fields = append(fields, ConfirmField{
			Key: f.Name, Label: label, Value: val, Editable: false, Type: "text",
		})
	}
	fields = append(fields, fSabit("id", "Kayıt no", id))
	return fields
}

func nval(r map[string]any, k string) float64 { v, _ := r[k].(float64); return v }
func sval(r map[string]any, k string) string  { v, _ := r[k].(string); return v }

func bizSummaryHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, _ *tool.Tool, _ map[string]any) (map[string]any, error) {
		today := bizToday(o)
		month := today[:7]
		base, _ := time.Parse("2006-01-02", today)
		prevMonth := time.Date(base.Year(), base.Month()-1, 1, 0, 0, 0, 0, time.UTC).Format("2006-01")

		// müşteriler
		custStatus := map[string]int{}
		customers := st.read("customers")
		for _, c := range customers {
			custStatus[sval(c, "status")]++
		}

		// görevler
		open, overdue, dueToday := 0, 0, 0
		var attention []map[string]any
		for _, tk := range st.read("tasks") {
			if sval(tk, "status") == "done" {
				continue
			}
			open++
			due := sval(tk, "due")
			late := due != "" && due < today
			if late {
				overdue++
			}
			if due == today {
				dueToday++
			}
			if due != "" {
				attention = append(attention, map[string]any{
					"id": sval(tk, "id"), "title": sval(tk, "title"), "due": due,
					"assignee": sval(tk, "assignee"), "priority": sval(tk, "priority"), "overdue": late,
				})
			}
		}
		sort.SliceStable(attention, func(i, j int) bool { return attention[i]["due"].(string) < attention[j]["due"].(string) })
		if len(attention) > 5 {
			attention = attention[:5]
		}

		// faturalar
		invCount := map[string]int{}
		var outstanding, overdueAmt, billedNet, paidTotal float64
		for _, iv := range st.read("invoices") {
			status := sval(iv, "status")
			invCount[status]++
			gross := nval(iv, "gross")
			switch status {
			case "sent":
				outstanding += gross
				if due := sval(iv, "dueDate"); due != "" && due < today {
					overdueAmt += gross
				}
			case "paid":
				paidTotal += gross
			}
			if status != "cancelled" && strings.HasPrefix(sval(iv, "issueDate"), month) {
				billedNet += nval(iv, "net")
			}
		}

		// giderler
		var expThis, expPrev float64
		byCat := map[string]float64{}
		for _, e := range st.read("expenses") {
			d, amt := sval(e, "date"), nval(e, "amount")
			switch {
			case strings.HasPrefix(d, month):
				expThis += amt
				cat := sval(e, "category")
				if cat == "" {
					cat = "kategorisiz"
				}
				byCat[cat] += amt
			case strings.HasPrefix(d, prevMonth):
				expPrev += amt
			}
		}
		type catAmt struct {
			Cat string
			Amt float64
		}
		cats := make([]catAmt, 0, len(byCat))
		for c, a := range byCat {
			cats = append(cats, catAmt{c, a})
		}
		sort.Slice(cats, func(i, j int) bool {
			if cats[i].Amt != cats[j].Amt {
				return cats[i].Amt > cats[j].Amt
			}
			return cats[i].Cat < cats[j].Cat
		})
		topCats := make([]any, 0, 5)
		for i, c := range cats {
			if i == 5 {
				break
			}
			topCats = append(topCats, map[string]any{"category": c.Cat, "amount": round(c.Amt, 2)})
		}

		// stok
		products := st.read("products")
		var low []any
		for _, p := range products {
			if min := nval(p, "minStock"); min > 0 && nval(p, "stock") <= min {
				if len(low) < 10 {
					low = append(low, map[string]any{
						"sku": sval(p, "sku"), "name": sval(p, "name"), "stock": nval(p, "stock"), "minStock": min,
					})
				}
			}
		}
		if low == nil {
			low = []any{}
		}

		attentionOut := make([]any, 0, len(attention))
		for _, a := range attention {
			attentionOut = append(attentionOut, a)
		}
		// ---- proje yönetimi özeti ----
		// Aktif projeler + geciken proje görevleri. Kullanıcı "şirket nasıl gidiyor?"
		// dediğinde hem mali hem operasyonel görünür.
		activeProjects := make([]any, 0, 8)
		projOverdue := []any{}
		projCounts := map[string]int{}
		projTasks := st.read("projectTasks")
		for _, p := range st.read("projects") {
			status := sval(p, "status")
			projCounts[status]++
			if status != "active" && status != "planning" {
				continue
			}
			pid, _ := p["id"].(string)
			openT, doneT, est, doneEst := 0, 0, 0.0, 0.0
			for _, k := range projTasks {
				if sval(k, "project") != pid {
					continue
				}
				if sval(k, "status") == "done" {
					doneT++
					doneEst += nval(k, "estimate")
				} else {
					openT++
					est += nval(k, "estimate")
					if due := sval(k, "due"); due != "" && due < today && sval(k, "status") != "blocked" {
						projOverdue = append(projOverdue, taskBrief(k, "proje görevi gecikti"))
					}
				}
			}
			pct := 0.0
			if doneT+openT > 0 {
				if doneEst+est > 0 {
					pct = doneEst / (doneEst + est) * 100
				} else {
					pct = float64(doneT) / float64(doneT+openT) * 100
				}
			}
			row := map[string]any{
				"id": pid, "name": p["name"], "code": p["code"], "status": status,
				"percent": round(pct, 1), "tasksOpen": openT, "tasksDone": doneT,
			}
			if end := sval(p, "plannedEnd"); end != "" {
				row["daysLeft"] = daysBetween(today, end)
				row["overdue"] = end < today
			}
			activeProjects = append(activeProjects, row)
		}
		if len(projOverdue) > 15 {
			projOverdue = projOverdue[:15]
		}

		// ---- GERÇEK VERİ ENVANTERİ ----
		// Neden? Canlı hata: model "customers, tasks, invoices, expenses, products,
		// employees, notes koleksiyonlarından sınıf yapılabiliyor" dedi; oysa bunların
		// çoğunda HİÇ KAYIT YOKTU. Sebep: summary boş koleksiyonları sessizce 0'a
		// düşürüyordu, model de hangi koleksiyonda veri olduğunu bilemediği için
		// tahmin yürütüyordu. Burada liste AÇIKÇA verilir: dolu koleksiyonlar kayıt
		// sayısıyla, boş olanlar "count: 0" ile. Model artık varmayan şeyi uyduramaz.
		inventory := make([]any, 0, len(bizSpecs))
		nonEmpty := make([]string, 0, len(bizSpecs))
		for _, sp := range bizSpecs {
			n := len(st.read(sp.Name))
			inventory = append(inventory, map[string]any{
				"collection": sp.Name,
				"label":      sp.Label,
				"count":      n,
			})
			if n > 0 {
				// Düz ve ÇIPLAK ad: model bu listeyi doğrudan biz.find/biz.add
				// `collection` değeri olarak kullanır. "customers (2)" gibi
				// süslü yazılsaydı model adı kopyalarken hataya düşerdi.
				nonEmpty = append(nonEmpty, sp.Name)
			}
		}

		out := map[string]any{
			"date":      today,
			"customers": map[string]any{"total": len(customers), "byStatus": custStatus},
			"tasks": map[string]any{
				"open": open, "overdue": overdue, "dueToday": dueToday, "nextUp": attentionOut,
			},
			"projects": map[string]any{
				"byStatus": projCounts, "active": activeProjects,
				"overdueTasks": projOverdue,
				"note":         "Ayrıntı için biz.project (ref) kullan.",
			},
			"invoices": map[string]any{
				"byStatus": invCount, "outstanding": round(outstanding, 2), "overdue": round(overdueAmt, 2),
				"paidTotal": round(paidTotal, 2), "billedNetThisMonth": round(billedNet, 2),
			},
			"expenses": map[string]any{
				"thisMonth": round(expThis, 2), "lastMonth": round(expPrev, 2), "topCategoriesThisMonth": topCats,
			},
			"stock":     map[string]any{"products": len(products), "lowStock": low},
			"employees": map[string]any{"headcount": len(st.read("employees"))},
			"estimates": map[string]any{
				"profitThisMonth": round(billedNet-expThis, 2),
				"note":            "Yaklaşık: bu ay kesilen faturaların KDV hariç toplamı − bu ayın giderleri; tahsilat zamanı ve gider KDV'si dikkate alınmaz.",
			},
			// Envanter: model hangi koleksiyonda veri olduğunu buradan görür.
			"collections": inventory,
		}
		// Kullanılabilir koleksiyonların düz listesi. Boşsa model bunu "hiç kayıt yok"
		// olarak anlar; sıfır değer döndürmek, sıfır kayda rağmen veri varmış izlenimi
		// yaratıyordu (canlı hatanın kaynağı).
		if len(nonEmpty) > 0 {
			out["nonEmptyCollections"] = nonEmpty
		} else {
			out["nonEmptyCollections"] = []string{}
			out["note"] = "Şu anda hiçbir koleksiyonda kayıt yok. Kullanıcı bilgi verdiyse " +
				"önce biz.add ile kaydet, sonra özetle. Olmayan koleksiyon/veri uydurma."
		}
		return out, nil
	}
}

func bizVatHandler(_ context.Context, _ *tool.Tool, in map[string]any) (map[string]any, error) {
	amount, _ := numberDelta(in["amount"])
	rate := 20.0
	if r, ok := numberDelta(in["rate"]); ok {
		rate = r
	}
	from, _ := in["from"].(string)
	var net, gross float64
	if from == "gross" {
		gross = amount
		net = amount / (1 + rate/100)
	} else {
		net = amount
		gross = amount * (1 + rate/100)
	}
	return map[string]any{
		"net": round(net, 2), "vat": round(gross-net, 2), "gross": round(gross, 2), "rate": rate,
	}, nil
}

// ---- PROJE YÖNETİMİ HANDLER'LARI ----

// bizProjectHandler bir projenin yönetim panosunu üretir. Okuma yetkisi ister ve
// KAYIT YAZMAZ: pano bir görünümdür, kaynak her zaman görev/kilometre taşı/risk
// koleksiyonlarıdır (tek doğruluk ilkesi — pano kaydedilip gerçeklikten kopmaz).
func bizProjectHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		today := bizToday(o)
		ref, _ := in["ref"].(string)
		if strings.TrimSpace(ref) == "" {
			return nil, Fail(t.Name, "ref (proje referansı) gerekli")
		}
		projects := st.read("projects")
		idx, err := specByName("projects").resolve(projects, ref)
		if err != nil {
			return nil, Fail(t.Name, "%v", err)
		}
		project := projects[idx]

		// Yalnızca bu projeye ait alt kayıtlar (project alanı id ya da ad/kod ile eşleşir).
		match := func(r map[string]any) bool { return sameProject(r, project) }
		tasks := filterBy(st.read("projectTasks"), match)
		milestones := filterBy(st.read("milestones"), match)
		risks := filterBy(st.read("risks"), match)
		sprints := filterBy(st.read("sprints"), match)

		// Görev dağılımı (faza göre) ve ilerleme.
		byStatus := map[string]int{}
		total, doneEst, todoEst := 0, 0.0, 0.0
		overdue, dueThisWeek := []any{}, []any{}
		for _, k := range tasks {
			s := sval(k, "status")
			byStatus[s]++
			total++
			est := nval(k, "estimate")
			if s == "done" {
				doneEst += est
			} else {
				todoEst += est
			}
			due := sval(k, "due")
			if s == "done" || due == "" {
				continue
			}
			if due < today {
				overdue = append(overdue, taskBrief(k, "gecikti"))
			} else if due <= todayPlus(today, 7) {
				dueThisWeek = append(dueThisWeek, taskBrief(k, "bu hafta"))
			}
		}
		// İlerleme tahmin ağırlıklıdır; hiç tahmin yoksa görev sayısına düşer.
		progress := 0.0
		if total > 0 {
			if doneEst+todoEst > 0 {
				progress = doneEst / (doneEst + todoEst) * 100
			} else {
				progress = float64(byStatus["done"]) / float64(total) * 100
			}
		}

		// Risk skoru = etki × olasılık; yüksek olanlar öne çıkar.
		type riskRow struct {
			rec   map[string]any
			score int
		}
		risksOut := make([]riskRow, 0, len(risks))
		for _, r := range risks {
			if sval(r, "status") == "closed" {
				continue
			}
			risksOut = append(risksOut, riskRow{rec: r, score: riskScore(r)})
		}
		sort.SliceStable(risksOut, func(i, j int) bool {
			if risksOut[i].score != risksOut[j].score {
				return risksOut[i].score > risksOut[j].score
			}
			return sval(risksOut[i].rec, "title") < sval(risksOut[j].rec, "title")
		})
		if len(risksOut) > 10 {
			risksOut = risksOut[:10]
		}
		riskList := make([]any, 0, len(risksOut))
		for _, r := range risksOut {
			row := cloneRec(r.rec)
			row["score"] = float64(r.score)
			riskList = append(riskList, row)
		}

		// Kilometre taşları: açık olanlar tarihe göre sıralı.
		openMs := make([]map[string]any, 0, len(milestones))
		for _, m := range milestones {
			if sval(m, "status") != "done" {
				openMs = append(openMs, m)
			}
		}
		sortByDate(openMs, "due")

		// Harcanan süre: kişi bazında.
		hours := 0.0
		personHours := map[string]float64{}
		for _, e := range st.read("timeEntries") {
			h := nval(e, "hours")
			hours += h
			if p := sval(e, "person"); p != "" {
				personHours[p] += h
			}
		}

		activeSprint := ""
		for _, s := range sprints {
			if sval(s, "status") == "active" {
				activeSprint = sval(s, "name")
				break
			}
		}

		// Kalan gün: plan bitişine kalan süre (yoksa null — tarih uydurulmaz).
		var remaining any
		if end := sval(project, "plannedEnd"); end != "" {
			remaining = map[string]any{"plannedEnd": end, "daysLeft": daysBetween(today, end)}
		}

		return map[string]any{
			"project": project,
			"progress": map[string]any{
				"percent":      round(progress, 1),
				"tasksTotal":   total,
				"tasksDone":    byStatus["done"],
				"estimateDone": round(doneEst, 2),
				"estimateOpen": round(todoEst, 2),
				"byStatus":     byStatus,
				"remaining":    remaining,
			},
			"overdue":      overdue,
			"dueThisWeek":  dueThisWeek,
			"milestones":   openMs,
			"risks":        riskList,
			"budget":       map[string]any{"planned": nval(project, "budget"), "currency": sval(project, "currency")},
			"time":         map[string]any{"totalHours": round(hours, 2), "byPerson": roundMap(personHours)},
			"activeSprint": activeSprint,
			"note":         "Bu bir GÖRÜNÜMDÜR; görev/kilometre taşı/risk değiştirmek için biz.update kullan.",
		}, nil
	}
}

// bizPlanHandler hedef tarihe göre plan çıkarır. KAYDETMEZ: plan bir öneridir;
// kaydedilmesi kullanıcının kararıdır (biz.add collection=projects).
func bizPlanHandler(st *bizStore, o Options) tool.Handler {
	return func(_ context.Context, t *tool.Tool, in map[string]any) (map[string]any, error) {
		today := bizToday(o)
		capacity := 1.0
		if c, ok := numberDelta(in["capacity"]); ok && c > 0 {
			capacity = c
		}
		start, _ := in["startDate"].(string)
		if strings.TrimSpace(start) == "" {
			start = today
		}

		// Proje: ref verildiyse kayıttan, yoksa sıfırdan planlanır.
		var project map[string]any
		if ref, _ := in["ref"].(string); strings.TrimSpace(ref) != "" {
			projects := st.read("projects")
			idx, err := specByName("projects").resolve(projects, ref)
			if err != nil {
				return nil, Fail(t.Name, "%v", err)
			}
			project = cloneRec(projects[idx])
			if strings.TrimSpace(start) == today {
				if d := sval(project, "startDate"); d != "" {
					start = d
				}
			}
		} else {
			project = map[string]any{"name": "Yeni proje", "startDate": start}
		}

		// İş yükü: verilen tahminler + projeye bağlı AÇIK görevler (bitenler sayılmaz).
		var items []planWork
		for _, raw := range asSlice(in["tasks"]) {
			est := 1.0
			if v, ok := numberDelta(raw["estimate"]); ok && v > 0 {
				est = v
			}
			title, _ := raw["title"].(string)
			if strings.TrimSpace(title) == "" {
				title = "(başlıksız görev)"
			}
			as, _ := raw["assignee"].(string)
			items = append(items, planWork{title: title, assignee: as, estimate: est, status: "todo"})
		}
		for _, k := range st.read("projectTasks") {
			if !sameProject(k, project) || sval(k, "status") == "done" {
				continue
			}
			est := nval(k, "estimate")
			if est <= 0 {
				est = 1.0
			}
			items = append(items, planWork{
				title: sval(k, "title"), assignee: sval(k, "assignee"),
				estimate: est, status: sval(k, "status"), due: sval(k, "due"),
			})
		}
		// Son tarihi olan iş önce (yoksa en sona).
		sort.SliceStable(items, func(i, j int) bool {
			di, dj := items[i].due, items[j].due
			if di == "" {
				di = "9999-99-99"
			}
			if dj == "" {
				dj = "9999-99-99"
			}
			return di < dj
		})

		total := 0.0
		for _, it := range items {
			total += it.estimate
		}
		neededDays := 0
		if total > 0 {
			neededDays = int(total/capacity + 0.5)
		}
		finish := todayPlus(start, neededDays)

		deadline, _ := in["deadline"].(string)
		if strings.TrimSpace(deadline) == "" {
			deadline = sval(project, "plannedEnd")
		}

		out := map[string]any{
			"startDate": start, "totalEstimate": round(total, 2),
			"dailyCapacity": capacity, "neededDays": neededDays,
			"finishDate": finish, "taskCount": len(items),
		}
		// Hedef tarih verilmişse yeterlilik analizi: yetişecek mi?
		if strings.TrimSpace(deadline) != "" {
			avail := daysBetween(start, deadline)
			if avail < 0 {
				avail = 0
			}
			required := 0.0
			if avail > 0 {
				required = total / float64(avail)
			} else if total > 0 {
				required = capacity // süresi yok: en azından mevcut kapasite gerekir
			}
			out["deadline"] = deadline
			out["availableDays"] = avail
			out["requiredCapacity"] = round(required, 2)
			out["feasible"] = avail >= neededDays
			if avail < neededDays {
				out["shortfallDays"] = neededDays - avail
				out["advice"] = fmt.Sprintf(
					"Bu kapasiteyle %d gün gerekiyor, hedefe %d gün var: %d gün gecikme ya da günde %.2f kişi-gün gerekir.",
					neededDays, avail, neededDays-avail, required)
			}
		}
		// Görev bazlı takvim (en fazla 60 görev): "kim ne zaman yapacak" tek bakışta.
		out["schedule"] = scheduleRows(items, start, capacity)
		out["note"] = "Bu bir PLAN ÖNERİSİDİR, kaydedilmedi. Kaydetmek için biz.add collection=projects startDate/plannedEnd."
		return out, nil
	}
}

// ---- proje yönetimi yardımcıları ----

// sameProject bir alt kaydın (görev/kilometre taşı/risk/sprint) projeye ait olup
// olmadığını söyler. Eşleşme proje kimliğiyle ya da ad/kod ile yapılır: kullanıcı
// görev eklerken proje referansını adla da yazabilmeli (kimlik bilmesi zorunlu değil).
func sameProject(rec, project map[string]any) bool {
	if rec == nil || project == nil {
		return false
	}
	pv := sval(rec, "project")
	if pv == "" {
		return false
	}
	if id, _ := project["id"].(string); id != "" && pv == id {
		return true
	}
	f := fold(pv)
	for _, key := range []string{"id", "code", "name"} {
		if s, _ := project[key].(string); s != "" && fold(s) == f {
			return true
		}
	}
	return false
}

// filterBy süzgeci sağlayan kayıtları sırayı bozmadan süzer.
func filterBy(recs []map[string]any, keep func(map[string]any) bool) []map[string]any {
	out := make([]map[string]any, 0, len(recs))
	for _, r := range recs {
		if keep(r) {
			out = append(out, r)
		}
	}
	return out
}

// riskScore riski etki × olasılık ile derecelendirir (1..9). Sıralama ve "en yüksek
// risk" seçimi tek kural olsun diye türetilmiş alanda hesaplanır.
func riskScore(rec map[string]any) int {
	return levelScore(sval(rec, "impact")) * levelScore(sval(rec, "likelihood"))
}

// levelScore düşük=1, orta=2, yüksek=3. Bilinmeyen değer orta kabul edilir
// (az bilgi, düşük risk demek değildir).
func levelScore(v string) int {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "low":
		return 1
	case "high":
		return 3
	default:
		return 2
	}
}

// taskBrief geciken/haftalık görev listesi için kısa kayıt üretir (tam kayıt şişmesin).
func taskBrief(rec map[string]any, flag string) map[string]any {
	return map[string]any{
		"id": rec["id"], "title": rec["title"], "assignee": rec["assignee"],
		"due": rec["due"], "status": rec["status"], "flag": flag,
		"project": rec["project"],
	}
}

// sortByDate tarih alanına göre sıralar; tarihi olmayanlar sona gider (belirsizlik gizlenmez).
func sortByDate(recs []map[string]any, field string) {
	sort.SliceStable(recs, func(i, j int) bool {
		a, b := sval(recs[i], field), sval(recs[j], field)
		if a == "" {
			return false
		}
		if b == "" {
			return true
		}
		return a < b
	})
}

// todayPlus "YYYY-MM-DD" tarihine gün ekler ve aynı biçimde döndürür.
func todayPlus(date string, days int) string {
	d, ok := bizTime(date)
	if !ok {
		return date
	}
	return d.AddDate(0, 0, days).Format("2006-01-02")
}

// daysBetween iki tarih arasındaki gün farkı (to - from). Tarih çözülemezse 0.
func daysBetween(from, to string) int {
	f, okF := bizTime(from)
	t, okT := bizTime(to)
	if !okF || !okT {
		return 0
	}
	return int(t.Sub(f).Hours() / 24)
}

// bizTime "YYYY-MM-DD" metnini time.Time'a çevirir; çözülemezse false.
func bizTime(s string) (time.Time, bool) {
	t, err := time.Parse("2006-01-02", strings.TrimSpace(s))
	return t, err == nil
}

// planWork planlama için taşınan görev satırıdır (biz.plan içinde doldurulur).
type planWork struct {
	title, assignee, status, due string
	estimate                     float64
}

// scheduleRows görevleri kapasiteye göre günlere dağıtır: "kim, ne zaman yapacak".
// Sonuç GÖRÜNÜMDÜR; kaydedilmez.
func scheduleRows(items []planWork, start string, capacity float64) []any {
	if capacity <= 0 {
		capacity = 1
	}
	out := make([]any, 0, len(items))
	byPerson := map[string]float64{} // kişi başına kalan iş yükü → kişiye göre sıralama
	order := make([]string, 0, len(items))
	for _, it := range items {
		if it.assignee != "" {
			if _, ok := byPerson[it.assignee]; !ok {
				order = append(order, it.assignee)
			}
			byPerson[it.assignee] += it.estimate
		}
	}
	// İş yükü az olandan başlayarak dağıt: en yüklü kişi en sona düşer.
	person := ""
	used := 0.0
	day := start
	for i, it := range items {
		if it.assignee != "" {
			if person == "" || used >= byPerson[it.assignee] {
				for _, p := range order {
					if p == person || byPerson[p] == 0 {
						continue
					}
					person, used = p, 0
					break
				}
			}
		} else {
			person, used = "", 0
		}
		if used+it.estimate > capacity {
			used = 0
			day = todayPlus(day, 1)
		}
		used += it.estimate
		late := it.due != "" && day > it.due
		out = append(out, map[string]any{
			"seq": i + 1, "title": it.title, "assignee": it.assignee,
			"date": day, "estimate": round(it.estimate, 2), "status": it.status,
			"late": late,
		})
	}
	return out
}

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

// specByName koleksiyon adından şema döndürür. Bilinmeyen ad yeni bir şema üretir
// (böylece tool "collection: xyz" gönderdiğinde anlamlı "bilinmeyen alan" hatası
// gelir, çökme değil).
func specByName(name string) *bizSpec {
	for _, sp := range bizSpecs {
		if sp.Name == name {
			return sp
		}
	}
	return &bizSpec{Name: name, Label: name, Prefix: "gen"}
}

// asSlice "any" değeri []map[string]any'e çevirir; liste olmayan tek nesne tek elemanlı
// liste olur (LLM bazen tasks'ı dizi yerine tek nesne gönderebilir).
func asSlice(v any) []map[string]any {
	switch x := v.(type) {
	case []any:
		out := make([]map[string]any, 0, len(x))
		for _, e := range x {
			if m, ok := e.(map[string]any); ok {
				out = append(out, m)
			}
		}
		return out
	case map[string]any:
		return []map[string]any{x}
	}
	return nil
}

// roundMap sayı haritasını iki ondalığa yuvarlar (LLM'in okuması kolay olsun).
func roundMap(in map[string]float64) map[string]float64 {
	out := make(map[string]float64, len(in))
	for k, v := range in {
		out[k] = round(v, 2)
	}
	return out
}

// svalDefault girdiden metin okur; boşsa verilen varsayılanı döndürür.
func svalDefault(in map[string]any, key, def string) string {
	if s, _ := in[key].(string); strings.TrimSpace(s) != "" {
		return s
	}
	return def
}
