package agent

// confirm.go, modelin yazma/silme istegini KULLANICI ONAYINA baglar.
//
// CANLI HATA (04.10.2026): kullanici "proje silme araci yok" dedi. Arac kodda vardi
// (biz.delete) ama HIGH risk + delete izni istedigi icin modele hic gosterilmiyordu.
// Arac'lari acmak geri alinamaz islemleri MODELE emanet etmek demektir; o yuzden karari
// insana verdik: arac calismadan once ONAY FORMU doner, kullanici Onayla'ya basinca
// gercekten calisir.
//
// GUVENLIK - model confirmed:true deyimini kendisi uydurabilirdi. Bunu engellemek icin
// alan model cagrisindan AYRILIR: kullanicinin onayi gelmeden modelin girdisi asla
// degistirilmez (bkz. applyConfirmValues). Yani onay olmadan islem calismaz.

import (
	"context"
	"encoding/json"
	"errors"
	"strconv"
	"strings"
	"sync"
	"time"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/protocol"
	"fiko-ai/pkg/id"
)

// pendingForm, kullanic onayini bekleyen bir formdur.
type pendingForm struct {
	payload protocol.ToolConfirmPayload
	// toolInput, kullanic onaylayinca calistirilacak GIRDIDIR. Kullanic formu
	// duzenlediyse applyConfirmValues ile guncellenir.
	toolInput map[string]any
	toolName  string
	callID    string
	createdAt time.Time
}

// PendingConfirm, bekleyen onay formlarini tutar.
//
// Zaman asimi neden var? Kullanic formu acik unutup gidebilir; bellekte sonsuza dek
// kalmamasi icin formlar 30 dakika sonra duser (bkz. confirmTTL).
type PendingConfirm struct {
	mu   sync.Mutex
	byID map[string]*pendingForm
	// early: tur HENÜZ asistan mesajını kaydetmeden cevaplanan formların sonucu.
	// Kalıcı kart ancak tur bitince oluşur; erken gelen cevap burada bekler ve tur
	// mesajı kaydettikten sonra uygulanır (bkz. applyEarlyResolutions).
	early map[string]earlyResolution
}

type earlyResolution struct {
	res conversation.ToolConfirmResolution
	at  time.Time
}

// NewPendingConfirm bos depo uretir.
func NewPendingConfirm() PendingConfirm {
	return PendingConfirm{byID: make(map[string]*pendingForm), early: make(map[string]earlyResolution)}
}

// Payload bekleyen formun istemciye giden halini dondurur (formu depodan CIKARMAZ).
func (p *PendingConfirm) Payload(confirmID string) (protocol.ToolConfirmPayload, bool) {
	p.mu.Lock()
	defer p.mu.Unlock()
	f, ok := p.byID[confirmID]
	if !ok {
		return protocol.ToolConfirmPayload{}, false
	}
	return f.payload, true
}

// NoteResolved erken gelen bir cevabin sonucunu saklar.
func (p *PendingConfirm) NoteResolved(confirmID string, r conversation.ToolConfirmResolution) {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.early == nil {
		p.early = make(map[string]earlyResolution)
	}
	p.early[confirmID] = earlyResolution{res: r, at: time.Now()}
}

// TakeResolved saklanan sonucu alir ve siler.
func (p *PendingConfirm) TakeResolved(confirmID string) (conversation.ToolConfirmResolution, bool) {
	p.mu.Lock()
	defer p.mu.Unlock()
	e, ok := p.early[confirmID]
	if ok {
		delete(p.early, confirmID)
	}
	return e.res, ok
}

// confirmTTL, bir onay formunun beklenecegi en uzun suredir.
const confirmTTL = 30 * time.Minute

// ErrNoConfirm, verilen kimlikle bekleyen form yoksa doner.
var ErrNoConfirm = errors.New("agent: onay formu bulunamadi (suresi dolmus olabilir)")

// Add yeni bir bekleyen form kaydeder ve kimligini dondurur.
func (p *PendingConfirm) Add(f protocol.ToolConfirmPayload, in map[string]any, callID string) string {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.byID == nil {
		p.byID = make(map[string]*pendingForm)
	}
	if f.ConfirmID == "" {
		f.ConfirmID = id.New("cfm")
	}
	p.byID[f.ConfirmID] = &pendingForm{
		payload:   f,
		toolInput: cloneArgs(in),
		toolName:  f.Tool,
		callID:    callID,
		createdAt: time.Now(),
	}
	return f.ConfirmID
}

// Take formu alir ve depodan CIKARIR (tek kullanimlidir: onay iki kez isletilemez).
func (p *PendingConfirm) Take(confirmID string) (*pendingForm, error) {
	p.mu.Lock()
	defer p.mu.Unlock()
	f, ok := p.byID[strings.TrimSpace(confirmID)]
	if !ok {
		return nil, ErrNoConfirm
	}
	delete(p.byID, confirmID)
	if time.Since(f.createdAt) > confirmTTL {
		return nil, ErrNoConfirm
	}
	return f, nil
}

// Len bekleyen form sayisidir (testler ve teshis icin).
func (p *PendingConfirm) Len() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return len(p.byID)
}

// Expire suresi dolmus formlari siler; silinen sayiyi dondurur.
func (p *PendingConfirm) Expire() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	n := 0
	for id, f := range p.byID {
		if time.Since(f.createdAt) > confirmTTL {
			delete(p.byID, id)
			n++
		}
	}
	// Tur hic kaydetmediyse (konusma silindi, cokme) erken sonuclar da sonsuza dek kalmasin.
	for id, e := range p.early {
		if time.Since(e.at) > confirmTTL {
			delete(p.early, id)
		}
	}
	return n
}

// cloneArgs, girdi haritasinin kopyasini uretir (paylasilan harita mutasyona ugramasin).
func cloneArgs(in map[string]any) map[string]any {
	out := make(map[string]any, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

// topLevelArgs, form alanlarindan dogrudan tool girdisinin ust duzeyine yazilanlar.
var topLevelArgs = map[string]bool{"collection": true, "ref": true, "query": true}

func isTopLevelArg(k string) bool { return topLevelArgs[k] }

// applyConfirmValues, kullanicinin forma yazdigi alanlari tool girdisine uygular.
//
// GUVENLIK: yalnizca formda ACIKCA duzenlenebilir isaretlenmis alanlar yazilir. Sabit
// alanlar (kayit kimligi, koleksiyon adi) kullanic tarafindan degistirilemez; aksi
// halde "customers sil" istegi "projects sil"e donusurdu.
func applyConfirmValues(toolInput map[string]any, fields []protocol.ToolConfirmField, values map[string]string) {
	if toolInput == nil || len(values) == 0 {
		return
	}
	editable := make(map[string]bool, len(fields))
	for _, f := range fields {
		if f.Editable {
			editable[f.Key] = true
		}
	}
	// data icindeki alanlar (biz.add/update) gomuludur: data altinda guncellenir.
	data, _ := toolInput["data"].(map[string]any)
	if data == nil {
		data = make(map[string]any)
	}
	touched := false
	for k, v := range values {
		if !editable[k] {
			continue
		}
		if isTopLevelArg(k) {
			toolInput[k] = v
			touched = true
			continue
		}
		data[k] = v
		touched = true
	}
	if touched {
		toolInput["data"] = data
	}
}

// ConfirmAnswer, kullanicinin onayini isler: formu dogrular, araci calistirir ve sonucu
// istemciye bildirir.
//
// confirmed=false ise HICBIR SEY calistirilmaz. Boylece "vazgec" bir hata degil,
// normal bir karardir.
func (a *Agent) ConfirmAnswer(ctx context.Context, convID, userID string, ans protocol.ToolConfirmAnswerPayload) {
	payload := a.ResolveConfirm(ctx, convID, userID, ans)
	a.send(Input{ConversationID: convID, UserID: userID}, protocol.ServerToolConfirmResolved, payload)
}

// ResolveConfirm onay cevabini isler (gerekirse araci calistirir), KALICI karti gunceller ve
// istemciye gidecek sonucu DONDURUR; yayin yapmaz.
//
// CANLI HATA (04.10.2026): kart "isleniyor"da kaliyordu. Sonuc a.send ile turu baslatan
// oturumun emit'ine gidiyordu; ama onay formu tur BITTIKTEN sonra cevaplanir, aktif tur yoktur,
// emitFor nil doner ve tool.confirm.resolved sessizce dusuyordu (silme calisiyor, kart haber
// alamiyordu). Cevabi veren WS oturumu sonucu kendisi gonderir (bkz. ws.handleConfirmAnswer).
func (a *Agent) ResolveConfirm(ctx context.Context, convID, userID string, ans protocol.ToolConfirmAnswerPayload) protocol.ToolConfirmResolvedPayload {
	res := a.resolveConfirm(ctx, convID, userID, ans)
	a.persistResolution(convID, userID, ans.ConfirmID, res)
	return protocol.ToolConfirmResolvedPayload{
		ConfirmID: ans.ConfirmID, Confirmed: res.confirmed, Summary: res.summary,
		Action: res.action, Values: res.values, Expired: res.expired,
	}
}

// resolutionOf, sonucu kalici kartin durumuna cevirir.
func resolutionOf(res confirmResolution) conversation.ToolConfirmResolution {
	state := conversation.ConfirmCancelled
	switch {
	case res.expired:
		state = conversation.ConfirmExpired
	case res.confirmed:
		state = conversation.ConfirmAnswered
	}
	return conversation.ToolConfirmResolution{State: state, Summary: res.summary, Values: res.values}
}

// persistResolution, cevabi kalici karta yazar.
//
// SIRA ONEMLI (yaris onleme): once "erken sonuc" haritasina not edilir, sonra kayda yazilmaya
// calisilir. Tur mesaji henuz kaydetmediyse yazma ErrNotFound alir ama not kalir ve tur,
// mesaji kaydettikten SONRA haritayi okuyup uygular (applyEarlyResolutions). Tur once
// kaydederse yazma basarili olur. Hangi sirayla olursa olsun sonuc kaybolmaz.
func (a *Agent) persistResolution(convID, userID, confirmID string, res confirmResolution) {
	if strings.TrimSpace(confirmID) == "" {
		return
	}
	r := resolutionOf(res)
	a.confirm.NoteResolved(confirmID, r)
	if a.svc == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.WithoutCancel(a.bgOrBackground()), 10*time.Second)
	defer cancel()
	err := a.svc.ResolveToolConfirm(ctx, userID, convID, confirmID, r)
	switch {
	case err == nil:
		a.confirm.TakeResolved(confirmID) // kayit guncellendi: not gereksiz
	case errors.Is(err, conversation.ErrNotFound):
		// Ya tur henuz kaydetmedi (not tur tarafindan uygulanacak) ya da kart zaten kapali.
	default:
		if a.log != nil {
			a.log.Warn("onay karti guncellenemedi", "err", err, "conversation", convID, "confirm", confirmID)
		}
	}
}

// bgOrBackground, arka plan baglamini (yoksa Background) dondurur.
func (a *Agent) bgOrBackground() context.Context {
	if a.bg != nil {
		return a.bg
	}
	return context.Background()
}

// applyEarlyResolutions, tur mesaji kaydettikten SONRA, kayit beklerken cevaplanan kartlari kapatir.
func (a *Agent) applyEarlyResolutions(ctx context.Context, in Input, confirms []conversation.ToolConfirm) {
	if a.svc == nil {
		return
	}
	for _, c := range confirms {
		r, ok := a.confirm.TakeResolved(c.ConfirmID)
		if !ok {
			continue
		}
		err := a.svc.ResolveToolConfirm(ctx, in.UserID, in.ConversationID, c.ConfirmID, r)
		if err != nil && !errors.Is(err, conversation.ErrNotFound) && a.log != nil {
			a.log.Warn("erken onay karti guncellenemedi", "err", err, "conversation", in.ConversationID, "confirm", c.ConfirmID)
		}
	}
}

// toolConfirmRecord, istemciye giden formu kalici kayda cevirir (arac girdisi saklanmaz).
func toolConfirmRecord(p protocol.ToolConfirmPayload, now time.Time) conversation.ToolConfirm {
	fields := make([]conversation.ToolConfirmField, 0, len(p.Fields))
	for _, f := range p.Fields {
		fields = append(fields, conversation.ToolConfirmField{
			Key: f.Key, Label: f.Label, Value: f.Value, Editable: f.Editable, Required: f.Required,
			Missing: f.Missing, Hint: f.Hint, Options: f.Options, Type: f.Type,
		})
	}
	return conversation.ToolConfirm{
		ConfirmID: p.ConfirmID, CallID: p.CallID, Tool: p.Tool, Action: p.Action, Title: p.Title,
		Message: p.Message, Fields: fields, MissingFields: p.MissingFields,
		ConfirmLabel: p.ConfirmLabel, CancelLabel: p.CancelLabel, Destructive: p.Destructive,
		State: conversation.ConfirmPending, CreatedAt: now.UTC(),
	}
}

// askAction, ui.form (bilgi formu) eylemidir: arac CALISTIRILMAZ, kullanicinin
// cevaplari istemciye doner ve sohbete kullanici mesaji olarak gider.
const askAction = "ask"

// askMaxValueRunes, bilgi formundaki tek bir cevabin ust siniridir (sohbete mesaj olur).
const askMaxValueRunes = 2000

// confirmResolution, onay cevabinin sonucudur.
type confirmResolution struct {
	summary   string
	confirmed bool
	// action formun turu (ask | add | delete ...). ask ise values dolu gelir.
	action string
	values map[string]string
	// expired: form artik gecerli degil (suresi doldu / sunucu yeniden basladi / zaten kullanildi).
	expired bool
}

// askValues, bilgi formunun nihai cevaplarini uretir: her alan icin varsayilan deger,
// kullanici duzenlediyse onun degeri. Yalnizca formda tanimli ve duzenlenebilir alanlar
// kabul edilir (formda olmayan anahtar sessizce atilir); enum disi secenek bosaltilir,
// onay kutusu "true"/"false"a indirgenir ve uzun metin kirpilir.
func askValues(fields []protocol.ToolConfirmField, given map[string]string) map[string]string {
	out := make(map[string]string, len(fields))
	for _, f := range fields {
		v := f.Value
		if g, ok := given[f.Key]; ok && f.Editable {
			v = g
		}
		v = strings.TrimSpace(v)
		if r := []rune(v); len(r) > askMaxValueRunes {
			v = strings.TrimSpace(string(r[:askMaxValueRunes]))
		}
		switch f.Type {
		case "bool":
			if strings.EqualFold(v, "true") {
				v = "true"
			} else {
				v = "false"
			}
		case "enum":
			ok := false
			for _, o := range f.Options {
				if o == v {
					ok = true
					break
				}
			}
			if !ok {
				v = ""
			}
		}
		out[f.Key] = v
	}
	return out
}

// resolveConfirm, onayi gercekten yurutur ve sonucu dondurur (yayindan ayri tutuldu ki
// davranis test edilebilsin).
func (a *Agent) resolveConfirm(ctx context.Context, convID, userID string, ans protocol.ToolConfirmAnswerPayload) confirmResolution {
	f, err := a.confirm.Take(ans.ConfirmID)
	if err != nil {
		return confirmResolution{summary: "Onay formu artik gecerli degil; islem yapilmadi.", expired: true}
	}
	if ans.Cancelled || !ans.Confirmed {
		a.publish(ctx, event.ToolExecuted, Input{ConversationID: convID, UserID: userID},
			map[string]any{"tool": f.toolName, "result": "cancelled"})
		return confirmResolution{summary: "Kullanici vazgec; hicbir islem yapilmadi.", action: f.payload.Action}
	}

	// Bilgi formu: arac calistirilmaz, cevaplar istemciye doner.
	if f.payload.Action == askAction {
		a.publish(ctx, event.ToolExecuted, Input{ConversationID: convID, UserID: userID},
			map[string]any{"tool": f.toolName, "result": "answered", "via": "ask"})
		return confirmResolution{
			summary: "Cevaplar gönderildi.", confirmed: true, action: askAction,
			values: askValues(f.payload.Fields, ans.Values),
		}
	}

	// Kullanicinin duzenlemeleri girdiye uygulanir (yalnizca duzenlenebilir alanlar).
	applyConfirmValues(f.toolInput, f.payload.Fields, ans.Values)
	// Onay bayragi AJAN tarafindan eklenir: model confirmed:true uydurabilirdi,
	// buraya yalnizca gercek kullanic onayi ulasir.
	f.toolInput["confirmed"] = true

	run, err := a.runner.Run(ctx, f.toolName, f.toolInput, userID, nil)
	if err != nil {
		return confirmResolution{summary: "Islem yapilamadi: " + err.Error(), action: f.payload.Action}
	}
	a.publish(ctx, event.ToolExecuted, Input{ConversationID: convID, UserID: userID},
		map[string]any{"tool": f.toolName, "result": "success", "via": "confirm"})
	return confirmResolution{summary: confirmSummary(run.Output), confirmed: true, action: f.payload.Action}
}

// confirmSummary, tool ciktisindan kullaniciya gosterilecek tek satirlik ozet uretir.
func confirmSummary(out map[string]any) string {
	if out == nil {
		return "Islem tamamlandi."
	}
	coll, _ := out["collection"].(string)
	switch {
	case out["deleted"] == true:
		return humanColl(coll) + " kaydi silindi."
	case out["created"] == true:
		return humanColl(coll) + " kaydi eklendi."
	case out["updated"] == true:
		return humanColl(coll) + " kaydi guncellendi."
	}
	if n, ok := out["count"].(int); ok && n > 0 {
		if n == 1 {
			return "1 kayit islendi."
		}
		return strconv.Itoa(n) + " kayit islendi."
	}
	return "Islem tamamlandi."
}

// humanColl koleksiyon adini okunur hale getirir ("customers" -> "Customers").
func humanColl(s string) string {
	if s == "" {
		return "Kayit"
	}
	return strings.ToUpper(s[:1]) + s[1:]
}

// Onay modlari (settings paketiyle ayni degerler; tek dogruluk yolu ayar ekranidir).
const (
	confirmAlways  = "always"
	confirmRisky   = "risky"
	confirmMissing = "missing"
	confirmNever   = "never"
)

// confirmPolicy, kullanici tarafindan secilen anlik onay politikasidir.
// ConfirmFunc verilmezse guvenli varsayilan (risky) kullanilir.
func (a *Agent) confirmPolicy() string {
	if a.cfg.ConfirmFunc == nil {
		return confirmRisky
	}
	switch strings.TrimSpace(a.cfg.ConfirmFunc()) {
	case confirmAlways, confirmRisky, confirmMissing, confirmNever:
		return strings.TrimSpace(a.cfg.ConfirmFunc())
	default:
		return confirmRisky
	}
}

// handleConfirmForm, tool ciktisinda onay formu varsa onu istemciye yollar ve
// formun istemciye GONDERILIP GONDERILMEYECEGE karar verir.
//
// Politika KATMANI burada uygulanir (tool katmaninda degil): tool yalnizca formu
// URETIR, karari ajan verir. Boylece ayar ekranindan degisiklik aninda gecerli olur;
// tool tanimlari yeniden kurulmaz.
//
// Arac GERCEKTE calismadi; modele de oyle bildirilir. Bu olmadan model formu gorup
// "tamam, sildim" derdi ama veri hala duruyordu (canli hata sinifi).
func (a *Agent) handleConfirmForm(in Input, callID, toolName string, out map[string]any) (string, bool) {
	cf, ok := formOf(out)
	if !ok {
		return "", false
	}

	// Politika kapisi. Politika "never" ise ya da islem riskli degilse ve mod "risky"
	// ise form GONDERILMEZ; isim direk calisir.
	if !a.confirmRequired(cf, strsOf(cf["missingFields"])) {
		return "", false
	}

	payload := protocol.ToolConfirmPayload{
		CallID:       callID,
		Tool:         toolName,
		Action:       strOf(cf["action"]),
		Title:        strOf(cf["title"]),
		Message:      strOf(cf["message"]),
		ConfirmLabel: strOf(cf["confirmLabel"]),
		CancelLabel:  strOf(cf["cancelLabel"]),
		Destructive:  boolOf(cf["destructive"]),
		ToolInput:    mapOf(cf["payload"]),
	}
	if payload.ConfirmLabel == "" {
		payload.ConfirmLabel = "Onayla"
	}
	if payload.CancelLabel == "" {
		payload.CancelLabel = "Vazgec"
	}
	for _, f := range sliceOfMaps(cf["fields"]) {
		payload.Fields = append(payload.Fields, protocol.ToolConfirmField{
			Key: strOf(f["key"]), Label: strOf(f["label"]), Value: strOf(f["value"]),
			Editable: boolOf(f["editable"]), Required: boolOf(f["required"]),
			Missing: boolOf(f["missing"]), Hint: strOf(f["hint"]), Type: strOf(f["type"]),
			Options: strsOf(f["options"]),
		})
	}
	payload.MissingFields = strsOf(cf["missingFields"])

	payload.ConfirmID = a.confirm.Add(payload, payload.ToolInput, callID)
	a.send(in, protocol.ServerToolConfirm, payload)
	return payload.ConfirmID, true
}

// confirmRequired, verilen islem icin onay formunun gosterilip gosterilmeyecegini belirler.
//
//   - never   : hicbiri
//   - always  : her sey
//   - risky   : yalnizca geri alinamazlar (silme, arsiv)
//   - missing : yalnizca eksik zorunlu alan varsa
func (a *Agent) confirmRequired(cf map[string]any, missing []string) bool {
	// Bilgi formu (ui.form) bir ONAY degil, istenen CIKTIDIR: politika (never dahil)
	// formu gizleyemez. Gizlense arac yeniden calistirilir ve kullanici hic soru gormezdi.
	if strOf(cf["action"]) == askAction {
		return true
	}
	risky := boolOf(cf["destructive"])
	switch a.confirmPolicy() {
	case confirmNever:
		return false
	case confirmAlways:
		return true
	case confirmMissing:
		return len(missing) > 0
	default: // confirmRisky (varsayilan)
		return risky
	}
}

// ---- kucuk donusturuculer (cikti JSON tipleri any geligi icin gerekli) ----

// asMap, tool ciktisindaki bir degeri duz haritaya cevirir.
//
// CANLI HATA (04.10.2026): builtin tool'lar onay formunu YAPI (builtin.ConfirmForm) olarak
// dondurur; ajan ise yalnizca map[string]any kabul ediyordu. Testler hep map verdigi icin
// hata gorunmedi, gercekte form ASLA kullaniciya gitmiyordu (silme/ekleme onayi calismiyordu).
// Simdi her tur kabul edilir: harita oldugu gibi, digerleri JSON uzerinden cevrilir.
func asMap(v any) (map[string]any, bool) {
	switch x := v.(type) {
	case nil:
		return nil, false
	case map[string]any:
		return x, true
	}
	raw, err := json.Marshal(v)
	if err != nil {
		return nil, false
	}
	var m map[string]any
	if err := json.Unmarshal(raw, &m); err != nil || m == nil {
		return nil, false
	}
	return m, true
}

// formOf, tool ciktisindaki onay formunu duz harita olarak dondurur.
func formOf(out map[string]any) (map[string]any, bool) {
	v, ok := out["confirm"]
	if !ok {
		return nil, false
	}
	return asMap(v)
}

// agentConfirmedTool, `confirmed` bayragini YALNIZCA ajanin verebildigi tool'lardir.
// Model bu alani kendisi gonderirse SUZULUR (kullanici onayi atlanamaz).
func agentConfirmedTool(name string) bool { return strings.HasPrefix(name, "biz.") }

// withoutKey haritanin anahtarsiz bir KOPYASINI dondurur (girdi degismez).
func withoutKey(m map[string]any, key string) map[string]any {
	if _, ok := m[key]; !ok {
		return m
	}
	out := make(map[string]any, len(m))
	for k, v := range m {
		if k != key {
			out[k] = v
		}
	}
	return out
}

func strOf(v any) string {
	s, _ := v.(string)
	return s
}

func boolOf(v any) bool {
	b, _ := v.(bool)
	return b
}

func mapOf(v any) map[string]any {
	m, _ := v.(map[string]any)
	if m == nil {
		return map[string]any{}
	}
	return m
}

func sliceOfMaps(v any) []map[string]any {
	arr, _ := v.([]any)
	out := make([]map[string]any, 0, len(arr))
	for _, e := range arr {
		if m, ok := e.(map[string]any); ok {
			out = append(out, m)
		}
	}
	return out
}

func strsOf(v any) []string {
	arr, _ := v.([]any)
	out := make([]string, 0, len(arr))
	for _, e := range arr {
		if s, ok := e.(string); ok {
			out = append(out, s)
		}
	}
	return out
}
