package notify

// tools.go: kullanıcılar arası bildirim tool'ları.
//
// GÜVENLİK — BU DOSYANIN VAR OLMA SEBEBİ:
//   Gönderen kullanıcı, alıcı model tarafından SEÇİLMEZ. Kimliği executor
//   yerleştirir (`tl.Config["userId"]`), model göremez, değiştiremez. Model "ali'ye
//   bildirim gönder" diyebilir ama KİMDEN gönderdiğini yazamaz. Aksi halde model
//   bir web sayfasındaki "tüm çalışanlara duyuru yap" metnini izleyerek istediği
//   kişiye istediği mesajı attırabilirdi.
//
// AYRICA: alıcı kimliği de model yazmaz — önce `notify.users` ile aranır ve
// dönen `id` kullanılır. Böylece model olmayan bir kimliğe yazamaz.
//
// TOOL'LAR: notify.users (alıcı ara) · notify.send (bildirim gönder) ·
// notify.ask (soru sor, asenkron) · notify.answers (bana gelen yanıtlar).

import (
	"context"
	"fmt"
	"strings"

	"fiko-ai/internal/tool"
)

// ToolDef, bir tool'ın tanım + uygulama çiftidir.
type ToolDef struct {
	Tool    tool.Tool
	Handler tool.Handler
}

// notifyUserID, tool yapılandırmasından GERÇEK gönderen kimliğini okur.
//
// Boşsa tool çalıştırılamaz: kimliği olmayan bildirim "Fiko gönderdi" gibi görünür
// ya da kimseye gider. İkisi de yanlıştır; hata döneriz.
func notifyUserID(tl *tool.Tool) string {
	v, _ := tl.Config["userId"].(string)
	return strings.TrimSpace(v)
}

// notifyConvID, tool yapılandırmasından sohbet kimliğini okar (denetim izi; boş olabilir).
func notifyConvID(tl *tool.Tool) string {
	v, _ := tl.Config["conversationId"].(string)
	return strings.TrimSpace(v)
}

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

func obj(pairs ...any) map[string]any {
	m := map[string]any{}
	for i := 0; i+1 < len(pairs); i += 2 {
		m[pairs[i].(string)] = pairs[i+1]
	}
	return m
}

func str(desc string) map[string]any { return obj("type", "string", "description", desc) }

// failTool, uygulama katmanının hata döndürmek için kısa yoludur.
//
// Neden *tool.FailedError? (canlı hata 05.10.2026) Executor yalnızca FailedError
// içindeki Reason'ı modele iletir; diğer hatalarda "tool hata döndürdü" diye
// GENEL bir mesaj döner (bkz. executor.go runOnce, default dalı). Bu paket
// fmt.Errorf döndürdüğü için notify hatalarının SEBEBİP modele hiç ulaşmıyordu:
// kullanıcı "kendine bildirim gönderilemez" bilgisini göremiyor, model aynı
// hatayı tekrar tekrar deniyor ve "teknik bir hata oluştu" diye konuşuyordu.
//
// Aynı desen builtin.Fail'de de kullanılır — iki paket de tool.FailedError döner.
func failTool(name, format string, args ...any) error {
	return &tool.FailedError{Tool: name, Reason: fmt.Sprintf(format, args...)}
}

func inputString(in map[string]any, key string) (string, bool) {
	v, ok := in[key].(string)
	return v, ok && strings.TrimSpace(v) != ""
}

// inputStrings, alıcı listesini okur. Düz metin DİZİSİ de kabul edilir ("ali, veli")
// çünkü küçük modeller dizi kurmayı sık kaçırır.
func inputStrings(in map[string]any, key string) []string {
	switch v := in[key].(type) {
	case []any:
		out := make([]string, 0, len(v))
		for _, x := range v {
			if s, ok := x.(string); ok && strings.TrimSpace(s) != "" {
				out = append(out, strings.TrimSpace(s))
			}
		}
		return out
	case string:
		parts := strings.Split(v, ",")
		out := make([]string, 0, len(parts))
		for _, p := range parts {
			if p = strings.TrimSpace(p); p != "" {
				out = append(out, p)
			}
		}
		return out
	}
	return nil
}

// optionsOf, modelin verdiği seçenekleri okur. Düz metin seçenek de kabul edilir
// (Value metnin kendisi olur): seçenek vermek model için zor olduğundan
// zorlamak yanlış karara yol açar.
func optionsOf(v any) []Option {
	switch arr := v.(type) {
	case []any:
		out := make([]Option, 0, len(arr))
		for _, x := range arr {
			switch e := x.(type) {
			case string:
				if t := Normalize(e); t != "" {
					out = append(out, Option{Text: t})
				}
			case map[string]any:
				text, _ := e["text"].(string)
				if strings.TrimSpace(text) == "" {
					continue
				}
				val, _ := e["value"].(string)
				out = append(out, Option{Text: Normalize(text), Value: Normalize(val)})
			}
		}
		return out
	case string:
		parts := strings.Split(arr, "|")
		out := make([]Option, 0, len(parts))
		for _, p := range parts {
			if p = Normalize(p); p != "" {
				out = append(out, Option{Text: p})
			}
		}
		return out
	}
	return nil
}

// ToolSet, bildirim tool setidir.
type ToolSet struct {
	svc *Service
}

// NewToolSet, servisi alan tool setini kurar.
func NewToolSet(svc *Service) *ToolSet { return &ToolSet{svc: svc} }

// Definitions, kullanılabilen tool tanımlarını döndürür.
//
// KOŞULLA SUNUM: `notify.users` yalnızca alıcı arayüzü bağlıysa sunulur. Bağlı
// değilse model alıcı arayamaz; sunulması "bir şey bulamadım" hatası verirdi.
func (t *ToolSet) Definitions() []ToolDef {
	if t == nil || t.svc == nil {
		return nil
	}
	defs := []ToolDef{
		{Tool: t.sendTool(), Handler: t.send},
		{Tool: t.askTool(), Handler: t.ask},
		{Tool: t.answersTool(), Handler: t.answers},
	}
	if t.svc.rec != nil {
		defs = append(defs, ToolDef{Tool: t.usersTool(), Handler: t.users})
	}
	return defs
}

// ---- tool tanımları ----

func (t *ToolSet) usersTool() tool.Tool {
	return tool.Tool{
		Name:  "notify.users",
		Title: "Kullanıcı ara",
		Description: "Bildirim göndereceğin ya da soru soracağın kullanıcıları ARA. " +
			"Ad veya kullanıcı adı yaz; sonuçlar `id` ve `displayName` ile döner. " +
			"notify.send / notify.ask çağrılarında `to` alanına bu `id` yazılır. " +
			"Kimliği elle uydurma; önce bu aramayı çağır.",
		Version: "1.0.0",
		InputSchema: obj(
			"type", "object",
			"properties", obj(
				"query", str("Aranacak ad veya kullanıcı adı (en az 2 karakter)"),
				"limit", obj("type", "integer", "description", "En çok kaç sonuç",
					"default", 5, "minimum", 1, "maximum", 20),
			),
			"required", []any{"query"},
		),
		OutputSchema: obj("type", "object", "properties", obj(
			"users", obj("type", "array", "items", obj("type", "object")),
			"count", obj("type", "integer"),
		)),
		Permissions: []string{"execute"}, // bkz. not: yalnızca ARAMA
		Risk:        tool.RiskLow,
		TimeoutMs:   10_000,
		Tags:        []string{"notify", "bildirim"},
	}
}

func (t *ToolSet) sendTool() tool.Tool {
	return tool.Tool{
		Name:  "notify.send",
		Title: "Kullanıcıya bildirim gönder",
		Description: "BİR kullanıcıya kısa bir bildirim gönderir; yanıt BEKLENMEZ. " +
			"Mesajın göndereni KENDİSİN (kullanıcı adın görünür). " +
			"Yanıt gerekiyorsa bunu değil `notify.ask` kullan. " +
			"`to` alanına `notify.users` ile bulduğun kullanıcının id'sini yaz.",
		Version: "1.0.0",
		InputSchema: obj(
			"type", "object",
			"properties", obj(
				"to", str("Alıcının kullanıcı id'si (notify.users ile bulunur)"),
				"text", str("Gönderilecek bildirim metni (tek cümle, en fazla 1000 karakter)"),
				"toMany", obj("type", "array", "items", obj("type", "string"),
					"description", "Birden çok kullanıcıya gönderilecekse id listesi (en fazla 10)"),
			),
			"required", []any{"text"},
		),
		OutputSchema: obj("type", "object", "properties", obj(
			"status", str("sent"),
			"sent", obj("type", "array", "items", obj("type", "object")),
			"count", obj("type", "integer"),
			"failed", obj("type", "object", "description", "Kimlere gönderilemedi ve neden"),
		)),
		Permissions: []string{"execute"}, // bkz. not: kullanıcı adına yazma (write DEĞİL)
		Risk:        tool.RiskLow,
		TimeoutMs:   10_000,
		Tags:        []string{"notify", "bildirim"},
	}
}

func (t *ToolSet) askTool() tool.Tool {
	return tool.Tool{
		Name:  "notify.ask",
		Title: "Kullanıcıya soru sor",
		Description: "BİR kullanıcıya soru sorar ve YANITININ GELMESİNİ bekler. " +
			"Bu çağrı ASENKRONdur: soru gider, tur hemen biter, cevap SONRA gelir. " +
			"Cevabı almak için `notify.answers` çağır. Bilgi vermek istiyorsan " +
			"`notify.send` kullan (bekleyen soru biriktirmeyin).",
		Version: "1.0.0",
		InputSchema: obj(
			"type", "object",
			"properties", obj(
				"to", str("Sorulacak kullanıcının id'si (notify.users ile bulunur)"),
				"text", str("Sorulacak soru (tek cümle)"),
				"options", obj("type", "array", "items", obj("type", "string"),
					"description", "Seçenekler; kullanıcı birine tıklayarak yanıtlar (en fazla 4)"),
			),
			"required", []any{"to", "text"},
		),
		OutputSchema: obj("type", "object", "properties", obj(
			"status", str("asked"),
			"questionId", str("Soru kimliği; notify.answers çıktısında soruyla eşleşir"),
			"to", str("Kime soruldu"),
		)),
		Permissions: []string{"execute"},
		Risk:        tool.RiskLow,
		TimeoutMs:   10_000,
		Tags:        []string{"notify", "bildirim"},
	}
}

func (t *ToolSet) answersTool() tool.Tool {
	return tool.Tool{
		Name:  "notify.answers",
		Title: "Soru yanıtlarını oku",
		Description: "SORUDUĞUN sorulara gelen yanıtları okur. Yanıt henüz yoksa liste boş " +
			"döner; kullanıcının cevabı gelene kadar TEKRAR SORMA, bir sonraki turda yine bak. " +
			"Yanıtı okuduysan artık o soruyu tekrar sorma.",
		Version: "1.0.0",
		InputSchema: obj(
			"type", "object",
			"properties", obj(
				"pending", obj("type", "boolean",
					"description", "true ise yalnızca hâlâ yanıt bekleyen sorular",
					"default", false),
				"limit", obj("type", "integer", "description", "En çok kaç yanıt",
					"default", 5, "minimum", 1, "maximum", 20),
			),
		),
		OutputSchema: obj("type", "object", "properties", obj(
			"results", obj("type", "array", "items", obj("type", "object")),
			"count", obj("type", "integer"),
		)),
		Permissions: []string{"execute"}, // bkz. not: yalnızca KENDİ soruları
		Risk:        tool.RiskLow,
		TimeoutMs:   10_000,
		Tags:        []string{"notify", "bildirim"},
	}
}

// ---- uygulamalar ----

// identity, ortak kimlik kontrolüdür: kimlik yoksa HİÇBİR bildirim tool'u çalışmaz.
//
// Bu ayrı fonksiyon dört handler'da da kullanılır; birinde unutulursa model kimlik
// olmadan bildirim gönderebilir. Tek yerden kapatmak bunu kalıcı kılar.
func identity(tl *tool.Tool) (string, string, error) {
	uid := notifyUserID(tl)
	if uid == "" {
		return "", "", failTool(tl.Name,
			"gönderen kullanıcı bilinmiyor (tool kullanıcı bağlamı olmadan çalıştırıldı)")
	}
	return uid, notifyConvID(tl), nil
}

// users, `notify.users` uygulamasıdır: alıcı araması.
//
// GÜVENLİK: modele SADECE id ve görünen ad verilir; e-posta/rol/izin YOKTUR.
// Arama boş dönerse model uydurma kimlik göndermesin diye açık uyarı verilir.
func (t *ToolSet) users(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	if _, _, err := identity(tl); err != nil {
		return nil, err
	}
	query, ok := inputString(in, "query")
	if !ok {
		return nil, failTool(tl.Name, "query alanı zorunludur")
	}
	limit := 5
	if v, ok := in["limit"].(float64); ok && v >= 1 && v <= 20 {
		limit = int(v)
	}
	found, err := t.svc.rec.Search(ctx, query, limit)
	if err != nil {
		return nil, failTool(tl.Name, "kullanıcı araması başarısız: %v", err)
	}
	items := make([]map[string]any, 0, len(found))
	for _, u := range found {
		items = append(items, map[string]any{"id": u.ID, "displayName": u.DisplayName})
	}
	out := map[string]any{"users": items, "count": len(items)}
	if len(items) == 0 {
		out["message"] = "bu adla eşleşen etkin kullanıcı yok. Kimliği UYDURMA; " +
			"kullanıcıya adını sorup doğru aramayı yap."
	}
	return out, nil
}

// send, `notify.send` uygulamasıdır: tek veya çok alıcıya bildirim.
func (t *ToolSet) send(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	uid, convID, err := identity(tl)
	if err != nil {
		return nil, err
	}
	text, ok := inputString(in, "text")
	if !ok {
		return nil, failTool(tl.Name, "text alanı zorunludur")
	}
	single, hasSingle := inputString(in, "to")
	many := inputStrings(in, "toMany")
	if !hasSingle && len(many) == 0 {
		return nil, failTool(tl.Name,
			"alıcı yok: önce notify.users ile kullanıcıyı ara, sonra `to` (veya `toMany`) alanına id'sini yaz")
	}
	// Kendini listeden çıkar (TEK alıcı yolunda da): kullanıcı kendine bildirim
	// istemez; sunucu reddedince model hatayı görüp "gönderdim" DEMEZ, doğru
	// davranış sessizce elemektir (çoklu yolla aynı kural).
	if hasSingle && single == uid {
		hasSingle = false
	}
	// Tek alıcı da elendiyse ve toplu liste de yoksa gönderilecek kimse kalmadı.
	if !hasSingle && len(many) == 0 {
		// Mesaj MODELE de anlamlı olmalı (canlı hata 05.10.2026): tek kullanıcılı
		// bir sistemde kullanıcı kendine bildirim göndermeyi dener ve model
		// "teknik hata" deyip döner. Gerçek sebebi ve YAPILACAK İŞİ söyle.
		return nil, failTool(tl.Name,
			"kendine bildirim gönderilemez (gönderen ve alıcı aynı kullanıcı: %q). "+
				"Kullanıcıya bunu SÖYLE: bildirim göndermek için BAŞKA bir kullanıcı gerekir. "+
				"notify.users ile diğer kullanıcıları ara; sistemde başka kullanıcı yoksa "+
				"bu işlem yapılamaz, uydurma bir alıcı seçme ve gönderildi deme.",
			uid)
	}
	sendIn := SendInput{FromUserID: uid, Text: text, ConversationID: convID}

	// Tek alıcı hızlı yol.
	if hasSingle && len(many) == 0 {
		sendIn.ToUserID = single
		m, err := t.svc.Send(ctx, sendIn)
		if err != nil {
			return nil, failTool(tl.Name, "%v", err)
		}
		return map[string]any{
			"status": "sent",
			"sent":   []map[string]any{{"id": m.ID, "to": m.ToUserID, "kind": string(m.Kind)}},
			"count":  1,
		}, nil
	}

	// Çoklu: `to` varsa listeye ekle (model bazen ikisini birden doldurur).
	if hasSingle {
		many = append([]string{single}, many...)
	}
	// Kendini listeden çıkar: sunucu reddeder ama modele net hata yerine sessizce
	// elmek daha doğru (kullanıcı kendine bildirim istemez).
	filtered := many[:0]
	for _, to := range many {
		if to != uid {
			filtered = append(filtered, to)
		}
	}
	res, err := t.svc.SendToMany(ctx, sendIn, filtered)
	if err != nil {
		return nil, failTool(tl.Name, "%v", err)
	}
	sent := make([]map[string]any, 0, len(res.Sent))
	for _, m := range res.Sent {
		sent = append(sent, map[string]any{"id": m.ID, "to": m.ToUserID})
	}
	out := map[string]any{"status": "sent", "sent": sent, "count": len(sent)}
	if len(res.Failed) > 0 {
		out["failed"] = res.Failed
		out["message"] = "BİR KISMI GÖNDERİLEMEDİ (failed haritasına bak). " +
			"Kullanıcıya 'hepsine gitti' DEME."
	}
	return out, nil
}

// ask, `notify.ask` uygulamasıdır: asenkron soru.
func (t *ToolSet) ask(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	uid, convID, err := identity(tl)
	if err != nil {
		return nil, err
	}
	to, ok := inputString(in, "to")
	if !ok {
		return nil, failTool(tl.Name, "to alanı zorunludur (notify.users ile bulunur)")
	}
	text, ok := inputString(in, "text")
	if !ok {
		return nil, failTool(tl.Name, "text alanı zorunludur")
	}
	m, err := t.svc.Ask(ctx, SendInput{
		FromUserID: uid, ToUserID: to, Text: text,
		Options:        optionsOf(in["options"]),
		ConversationID: convID,
	})
	if err != nil {
		return nil, failTool(tl.Name, "%v", err)
	}
	// ASENKRON DAVRANIŞ AÇIKÇA BİLDİRİLİR: model "cevabı şimdi alacağım" deyip aynı
	// turda notify.answers çağırıp boş sonuç görüp "kimse cevap vermedi" demesin.
	return map[string]any{
		"status":     "asked",
		"questionId": m.ID,
		"to":         m.ToUserID,
		"message": "soru gönderildi; cevap GELENE KADAR cevaplanmadı. Tur bitiyor. " +
			"Kullanıcı bir sonraki konuşmada `notify.answers` ile cevabı okursun.",
	}, nil
}

// answers, `notify.answers` uygulamasıdır: bana gelen yanıtlar.
//
// KUTU `sent` DİR (inbox değil): yanıtın göndereni yanıtlayandır; soran olarak
// BEN onu okurum (bkz. matchesBox yorumu).
func (t *ToolSet) answers(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	uid, _, err := identity(tl)
	if err != nil {
		return nil, err
	}
	limit := 5
	if v, ok := in["limit"].(float64); ok && v >= 1 && v <= 20 {
		limit = int(v)
	}
	items, _, err := t.svc.List(ctx, uid, Filter{Box: BoxSent, Kind: KindAnswer, Limit: limit})
	if err != nil {
		return nil, failTool(tl.Name, "yanıtlar okunamadı: %v", err)
	}
	// Soru metni de döner: model "evet" cevabını görüp neye cevap verildiğini bilmeli.
	rows := make([]map[string]any, 0, len(items))
	for _, m := range items {
		rows = append(rows, map[string]any{
			"questionId": m.QuestionID,
			"question":   m.Text,
			"answer":     m.AnswerText,
			"from":       m.FromUserID,
			"at":         m.CreatedAt,
		})
	}
	out := map[string]any{"results": rows, "count": len(rows)}
	if len(rows) == 0 {
		out["message"] = "henüz cevap yok. SORUYU TEKRAR SORMA; bir sonraki turda yine bak."
	}
	return out, nil
}
