package memory

// tools.go kalıcı belleğin TOOL tanımlarıdır.
//
// Neden tool? (Kullanıcı kararı: "soru sormayı tool olarak ayarlayalım, hafıza içinde
// tool'lar yapalım".) Önceden karar JSON çıktısında ("action":"ask") geliyordu; bu, soruyu
// modelin ÜRETİMİ sırasında pasif veri olarak ifade ediyordu ve o yol artık YOKTUR
// (bkz. extract.go Action). Tool modelinde ise model
// kararı AÇIKÇA bir çağrı olarak yapar: `memory.ask(...)`. Böylece
//   - karar, diğer tool kararlarıyla AYNI yerde ve AYNI mekanizmayla yaşar (denetim kaydı,
//     izin, zaman aşımı, ToolStart/ToolResult arayüz olayları),
//   - "emin değilim" durumu modele ÖZEL bir eylem olarak görünür, JSON alanı değil,
//   - model çağrıyı UNUTURSA soru sorulmaz: yani yanlışlıkla kayıt da yapılmaz.
//
// GÜVENLİK: tool'lar yalnızca KENDİ belleğine erişir; kullanıcı kimliğini executor
// yerleştirir, model değiştiremez. Hassas veri (Sensitive) hiçbir yolda kaydedilmez.

import (
	"context"
	"fmt"
	"strings"

	"fiko-ai/internal/tool"
	"fiko-ai/pkg/id"
)

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

// AskSink, sorunun istemciye (dolayısıyla kullanıcıya) ulaştırılmasını sağlar.
//
// Tool katmanı WebSocket'i BİLMEZ (agent ile aynı ilke); yalnızca bu arayüzü bilir.
// cmd/server bu arayüzü hub yayınına bağlar.
type AskSink interface {
	// Ask soruyu depolar ve kullanıcıya gönderir. Soru kimliğini döner.
	Ask(userID, conversationID string, q Question) string
}

// ToolSet, bellek tool'larının tanım ve çalıştırma noktasıdır.
type ToolSet struct {
	svc       *Service
	questions *QuestionStore
	// ask, soruyu kullanıcıya ulaştırır; nil ise `memory.ask` HİÇ SUNULMAZ, yani model
	// onu görmez ve çağıramaz. Sorulabilir ama cevaplanamayan bir tool yanıltıcı olurdu.
	ask AskSink
}

// NewToolSet bellek tool setini kurar. ask nil olabilir (soru akışı kapalı).
func NewToolSet(svc *Service, questions *QuestionStore, ask AskSink) *ToolSet {
	return &ToolSet{svc: svc, questions: questions, ask: ask}
}

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

// obj, JSON Schema nesnesi kurar (okunabilirlik için kısa yol).
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
}

// str alan kısayolu.
func str(desc string) map[string]any { return obj("type", "string", "description", desc) }

// failTool, tool uygulamasının anlaşılır bir hata döndürmesi için kısa yoldur.
//
// Neden *tool.FailedError? (canlı hata 05.10.2026) Executor yalnızca FailedError
// içindeki Reason'ı modele iletir; düz hatalarda "tool hata döndürdü" diye GENEL
// bir mesaj döner (executor.go runOnce, default dalı) ve gerçek sebep kaybolur.
// memory ve notify paketleri aynı hatayı yapıyordu; ikisi de builtin.Fail ile
// aynı desende *tool.FailedError döndürür.
func failTool(name, format string, args ...any) error {
	return &tool.FailedError{Tool: name, Reason: fmt.Sprintf(format, args...)}
}

// inputString, girdiden zorunlu metin alanını okur.
func inputString(in map[string]any, key string) (string, bool) {
	v, ok := in[key].(string)
	return v, ok && strings.TrimSpace(v) != ""
}

// maxQuestionOptions bir sorudaki en çok seçenek sayısıdır (buton satırı taşmasın).
const maxQuestionOptions = 4

// optionsOf, modelin verdiği seçenekleri KARARLARIYLA birlikte okur.
//
// Her seçenek {"text":"...","confirmed":true|false} nesnesi olmalıdır. `confirmed` açıkça
// bildirilmemiş (ya da düz metin olan) seçenek ATLANIR: karar ASLA metinden tahmin
// edilmez. Eskiden "hayır/kaydetme/vazgeç" geçen metinler ret sayılıyordu; model
// "Hayır, farklı bir yıl (2014 değil)" yazınca kullanıcı onaylamak istediği halde ret
// uygulanıyordu. Tahminle yanlış kayıt ya da yanlış ret, seçeneği hiç göstermemekten
// kötüdür. Geçerli seçenek kalmazsa çağıran sabit varsayılanı kullanır (defaultOptions).
func optionsOf(v any) []QuestionOption {
	arr, ok := v.([]any)
	if !ok {
		return nil
	}
	out := make([]QuestionOption, 0, len(arr))
	for _, x := range arr {
		e, ok := x.(map[string]any)
		if !ok {
			continue // düz metin: kararı yok
		}
		text, _ := e["text"].(string)
		if strings.TrimSpace(text) == "" {
			continue
		}
		conf, ok := e["confirmed"].(bool)
		if !ok {
			continue // karar açıkça bildirilmemiş: tahmin edilmez
		}
		save, _ := e["saveAsNew"].(bool)
		out = append(out, QuestionOption{Text: Normalize(text), Confirmed: conf, SaveAsNew: save})
		if len(out) >= maxQuestionOptions {
			break
		}
	}
	return out
}

// defaultOptions, model kullanılabilir seçenek vermediğinde sunulan iki sabit seçenektir.
//
// Kararlar SUNUCUDA verilir, arayüzde değil: "Evet" uygular, "Hayır" hiçbir şey yazmaz.
// (Arayüz bu varsayılanı kendisi üretiyordu; o zaman kararın nereden geldiği belli
// olmuyordu ve eski `optionTexts` alanındaki düz metinler körü körüne onay sayılıyordu.)
func defaultOptions() []QuestionOption {
	return []QuestionOption{
		{Text: "Evet, kaydet", Confirmed: true},
		{Text: "Hayır, kaydetme", Confirmed: false},
	}
}

// usableOptions, seçenekleri doğrular: onaylayabilen en az bir seçenek yoksa kullanıcı
// bu soruyla hiçbir şey kaydedemez, o yüzden sabit varsayılana düşülür.
func usableOptions(opts []QuestionOption) []QuestionOption {
	for _, o := range opts {
		if o.Confirmed {
			return opts
		}
	}
	return defaultOptions()
}

// memoryUserID, tool yapılandırmasından gerçek kullanıcı kimliğini okur.
// Executor kimliği kendisi yerleştirir; model bu değeri DEĞİŞTİREMEZ (güvenlik sınırı).
func memoryUserID(tl *tool.Tool) string {
	if tl == nil {
		return ""
	}
	v, _ := tl.Config["userId"].(string)
	return v
}

// memoryConvID, tool yapılandırmasından konuşma kimliğini okur (varsa).
func memoryConvID(tl *tool.Tool) string {
	if tl == nil {
		return ""
	}
	v, _ := tl.Config["conversationId"].(string)
	return v
}

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

// Definitions, kaydedilecek tool tanımlarını döndürür.
//
// `memory.ask` yalnızca soru akışı AÇIKKEN sunulur: kapalıyken görünmez olması, modelin
// "sordum" sanıp boşa dönmesini engeller.
func (t *ToolSet) Definitions() []ToolDef {
	if t == nil || t.svc == nil {
		return nil
	}
	defs := []ToolDef{
		{
			Tool: tool.Tool{
				Name:  "memory.remember",
				Title: "Bilgiyi kalıcı olarak hatırla",
				Description: "Kullanıcı hakkında kalıcı bir bilgiyi kalıcı belleğe KAYDET. " +
					"Ad, meslek, tercih, alışkanlık, aile, kullanılan araçlar gibi ileride işe yarayacak " +
					"gerçekler için kullan. Geçici istekler (bugün, şimdi) veya ZATEN BİLİNEN " +
					"bilgiler için KULLANMA; şüpheliyse memory.ask ile sor.",
				Version: "1.0.0",
				InputSchema: obj(
					"type", "object",
					"properties", obj(
						"content", obj("type", "string",
							"description", "Kendinde tam anlaşılan tek cümle. Örn: 'Kullanıcı Angular kullanıyor.'",
							"minLength", 8, "maxLength", 300),
					),
					"required", []any{"content"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"status", str("created | unchanged | updated"),
					"id", str("Anının kimliği"),
					"content", str("Kaydedilen metin"),
				)),
				Permissions: []string{"execute"}, // bkz. not: kullanıcının KENDİ anıları
				Risk:        tool.RiskLow,
				TimeoutMs:   10_000,
				Tags:        []string{"memory", "hafiza"},
			},
			Handler: t.remember,
		},
		{
			Tool: tool.Tool{
				Name:  "memory.update",
				Title: "Eski bilgiyi düzelt",
				Description: "Bellekte ZATEN KAYITLI bir bilgiyi düzeltmek için kullan. targetId, " +
					"değiştirilecek kaydın kimliğidir (memory.recall ile öğrenilir). " +
					"YENİ bir bilgi için bunu değil memory.remember'ı çağır.",
				Version: "1.0.0",
				InputSchema: obj(
					"type", "object",
					"properties", obj(
						"targetId", obj("type", "string", "description", "Güncellenecek kaydın id'si",
							"minLength", 4, "maxLength", 64),
						"content", obj("type", "string",
							"description", "Düzeltilmiş TAM cümle. Önceki cümlenin öznesini koru.",
							"minLength", 8, "maxLength", 300),
					),
					"required", []any{"targetId", "content"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"status", str("updated | unchanged"),
					"id", str("Güncellenen kaydın kimliği"),
					"content", str("Yeni metin"),
				)),
				Permissions: []string{"execute"}, // bkz. not: kullanıcının KENDİ anıları
				Risk:        tool.RiskLow,
				TimeoutMs:   10_000,
				Tags:        []string{"memory", "hafiza"},
			},
			Handler: t.update,
		},
		{
			Tool: tool.Tool{
				Name:  "memory.recall",
				Title: "Bellekte ara",
				Description: "Kullanıcının hatırlanan bilgileri içinde ara. Bir konuda neyin " +
					"saklandığını öğrenmek için kullan; sonuçlar id'siyle döner ve " +
					"memory.update gerektiğinde o id kullanılır.",
				Version: "1.0.0",
				InputSchema: obj(
					"type", "object",
					"properties", obj(
						"query", obj("type", "string", "description", "Arama ifadesi",
							"minLength", 2, "maxLength", 200),
						"limit", obj("type", "integer", "description", "En çok kaç sonuç",
							"default", 5, "minimum", 1, "maximum", 20),
					),
					"required", []any{"query"},
				),
				OutputSchema: obj("type", "object", "properties", obj(
					"results", obj("type", "array", "items", obj("type", "object")),
					"count", obj("type", "integer"),
				)),
				Permissions: []string{"execute"}, // bkz. not: kullanıcının KENDİ anıları
				Risk:        tool.RiskLow,
				TimeoutMs:   10_000,
				Tags:        []string{"memory", "hafiza"},
			},
			Handler: t.recall,
		},
	}
	if t.questions != nil && t.ask != nil {
		defs = append(defs, ToolDef{Tool: t.askTool(), Handler: t.askUser})
	}
	return defs
}

// askTool, `memory.ask` tanımıdır (soru akışı açıkken sunulur).
func (t *ToolSet) askTool() tool.Tool {
	return tool.Tool{
		Name:  "memory.ask",
		Title: "Kullanıcıya sor",
		Description: "Bir bilgiyi kaydetmek ya da güncellemek konusunda EMİN DEĞİLSEN " +
			"kullanıcıya SOR: kararı o verir. Önce memory.recall ile bak; gerçekten emin " +
			"değilsen BURAYI çağır ve tahmin etme. Soruyu YAZILI metin olarak sormak " +
			"YANLIŞTIR: seçenekli soru kartı yalnızca bu tool ile oluşur. Soru " +
			"gönderildikten sonra turu bitir, yanıt sonraki turda gelir.",
		Version: "1.0.0",
		InputSchema: obj(
			"type", "object",
			"properties", obj(
				"question", obj("type", "string",
					"description", "Kullanıcıya sorulacak doğal Türkçe soru cümlesi.",
					"minLength", 8, "maxLength", 240),
				"options", obj("type", "array", "items", obj("type", "object",
					"properties", obj(
						"text", obj("type", "string", "description", "Butonda görünecek kısa metin."),
						"confirmed", obj("type", "boolean", "description",
							"true ise bu seçenek seçilince kayıt/güncelleme UYGULANIR. "+
								"'Emin değilim, kaydetme' gibi bir seçenek false OLMALIDIR."),
						"saveAsNew", obj("type", "boolean",
							"description", "true ise uygulamak yerine ayrı kayıt açılır."),
					),
					"required", []any{"text", "confirmed"},
				),
					"description", "2-4 seçenek. HER SEÇENEK confirmed taşır; kararı metinden TAHMİN ETME."),
				"content", obj("type", "string",
					"description", "Kullanıcı onaylarsa KAYDEDİLECEK tam cümle. ZORUNLUDUR: "+
						"içeriği olmayan soru onaylansa bile hiçbir şey yazamaz.",
					"minLength", 4, "maxLength", 300),
				"targetId", obj("type", "string",
					"description", "Güncellenecek kaydın id'si; yoksa yeni kayıt açılır.",
					"maxLength", 64),
			),
			"required", []any{"question", "content"},
		),
		OutputSchema: obj("type", "object", "properties", obj(
			"status", str("asked"),
			"questionId", str("Sorunun kimliği"),
			"message", str("Ne olduğu (kullanıcıya söylenecek açıklama)"),
		)),
		Permissions: []string{"execute"}, // bkz. not: kullanıcının KENDİ anıları
		Risk:        tool.RiskLow,
		TimeoutMs:   5_000,
		Tags:        []string{"memory", "hafiza"},
	}
}

// ---- uygulamalar (handler) ----

// remember, `memory.remember` uygulamasıdır: kalıcı bilgiyi ekler.
func (t *ToolSet) remember(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	content, ok := inputString(in, "content")
	if !ok {
		return nil, failTool(tl.Name, "content alanı zorunludur")
	}
	content = Normalize(content)
	m, outcome, err := t.svc.Add(ctx, AddInput{
		UserID: memoryUserID(tl), Kind: KindLongTerm, Content: content, Origin: OriginAuto,
	})
	if err != nil {
		return nil, err
	}
	return t.finishWrite(ctx, tl, m, outcome, content), nil
}

// update, `memory.update` uygulamasıdır: hedeflenen kaydın içeriğini değiştirir.
func (t *ToolSet) update(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	targetID, ok := inputString(in, "targetId")
	if !ok {
		return nil, failTool(tl.Name, "targetId alanı zorunludur")
	}
	content, ok := inputString(in, "content")
	if !ok {
		return nil, failTool(tl.Name, "content alanı zorunludur")
	}
	content = Normalize(content)
	m, outcome, err := t.svc.Add(ctx, AddInput{
		UserID: memoryUserID(tl), Kind: KindLongTerm, Content: content, Origin: OriginAuto,
		TargetID: targetID,
	})
	if err != nil {
		return nil, err
	}
	return t.finishWrite(ctx, tl, m, outcome, content), nil
}

// finishWrite, yazma sonucunu kurar; kayıt KORUMA yüzünden değişmediyse onay kartını
// SUNUCU açar. Eskiden bunu modelin memory.ask çağırmasına bırakmıştık; küçük modeller
// çağırmıyor, kullanıcı ne güncelleme ne kart görüyordu. Karar yine kullanıcıdadır:
// kart onaylanmadıkça kayıt değişmez (ApplyAnswer kullanıcı onaylı yazar).
func (t *ToolSet) finishWrite(ctx context.Context, tl *tool.Tool, m *Memory, outcome Outcome, requested string) map[string]any {
	out := writeResult(m, outcome, requested)
	if out["status"] != "blocked" || m == nil || t.questions == nil || t.ask == nil {
		return out
	}
	userID := memoryUserID(tl)
	if userID == "" {
		return out
	}
	q := Question{
		ID: id.New("mq"), UserID: userID, ConversationID: memoryConvID(tl), Kind: KindLongTerm,
		Content: requested, TargetID: m.ID, Previous: m.Content,
		Text: Normalize("Kayıtlı bilgi şu: \"" + m.Content + "\". Bunu \"" + requested + "\" olarak güncelleyeyim mi?"),
		Options: []QuestionOption{
			{Text: "Evet, güncelle", Confirmed: true},
			{Text: "Hayır, kalsın", Confirmed: false},
		},
	}
	qid := t.ask.Ask(userID, q.ConversationID, q)
	if qid == "" {
		return out
	}
	return map[string]any{
		"status": "asked", "questionId": qid, "id": m.ID, "content": m.Content,
		"message": "DEĞİŞMEDİ: kayıt kullanıcının kendi yazdığı bir kayıttır. Kullanıcıya onay kartı gönderildi; " +
			"onaylarsa güncellenir. Kullanıcıya 'güncelledim' DEME, kartı onaylamasını söyle ve turu bitir.",
	}
}

// writeResult, yazma tool'larının sonucunu GERÇEĞE göre kurar.
//
// Canlı hata: kullanıcının elle yazdığı (OriginUser) kayıt otomatik yazmayla ezilmez ve
// Service.Add bu durumda ESKİ kaydı "unchanged" ile döner. Ama tool çıktısına istenen YENİ
// metni yazıyordu; model "content: yeni metin" gördüğü için kullanıcıya "güncelledim"
// diyordu, oysa kayıt hiç değişmemişti. Artık çıktıdaki content DEPODAKİ metindir ve
// kayıt istenenden farklı kaldıysa durum "blocked" olur, model ne yapacağını bilir.
func writeResult(m *Memory, outcome Outcome, requested string) map[string]any {
	out := map[string]any{"status": string(outcome), "content": requested}
	if m == nil {
		return out
	}
	out["id"] = m.ID
	out["content"] = m.Content
	if outcome == Unchanged && m.Content != requested {
		out["status"] = "blocked"
		out["message"] = "DEĞİŞMEDİ: bu kayıt kullanıcının kendi yazdığı bir kayıt ve otomatik değiştirilemez; " +
			"depodaki metin yukarıdaki content'tir. Kullanıcıya 'güncelledim' DEME. " +
			"Değişikliği uygulamak için memory.ask çağır (targetId=" + m.ID + ", content=yeni cümle); " +
			"kullanıcı kartta onaylarsa güncellenir."
	}
	return out
}

// recall, `memory.recall` uygulamasıdır: ilgili anıları kimlikleriyle döner.
// memory.update'in targetId değeri buradan öğrenilir.
func (t *ToolSet) recall(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	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)
	}
	hits := t.svc.Recall(ctx, memoryUserID(tl), query, limit, 0)
	items := make([]map[string]any, 0, len(hits))
	for _, h := range hits {
		items = append(items, map[string]any{
			"id": h.Memory.ID, "content": h.Memory.Content, "kind": string(h.Memory.Kind),
		})
	}
	return map[string]any{"results": items, "count": len(items)}, nil
}

// askUser, `memory.ask` uygulamasıdır: soruyu kullanıcıya sorar ve ANINDA döner.
//
// ASENKRON (kullanıcı kararı "A"): cevap BURADA BEKLENMEZ. Soru depoya yazılır ve
// istemciye gider; tool "asked" diyerek turu bitirir. Kullanıcı cevapladığında kayıt
// uygulanır ve sonuç bir sonraki turun istemine girer. Böylece sohbet donmaz.
func (t *ToolSet) askUser(ctx context.Context, tl *tool.Tool, in map[string]any) (map[string]any, error) {
	if t.questions == nil || t.ask == nil {
		return nil, failTool(tl.Name, "soru akışı kapalı; emin değilsen kaydetme")
	}
	question, ok := inputString(in, "question")
	if !ok {
		return nil, failTool(tl.Name, "question alanı zorunludur")
	}
	content, _ := in["content"].(string)
	content = Normalize(strings.TrimSpace(content))
	// İçerik ZORUNLUDUR. Boş içerikli soru kullanıcıya gösterilirdi ama "Evet" dendiğinde
	// doğrulama hatası verirdi ve soru da harcanmış olurdu. Hatayı modele şimdi, tool
	// sonucu olarak dönmek, kullanıcıya ölü bir kart göstermekten iyidir.
	if _, ferr := validateContent(content); ferr != nil {
		return nil, failTool(tl.Name, "content geçersiz (%s): onaylanırsa kaydedilecek cümleyi ver", ferr.Message)
	}
	targetID, _ := in["targetId"].(string)
	userID := memoryUserID(tl)
	if userID == "" {
		// Sahipsiz soru hiçbir kullanıcıya gösterilemez ve yanıtlanamaz (ApplyAnswer sahibi
		// arar); depoyu boşuna doldurmasın. Diğer tool'lar da kimliksizse reddediyor.
		return nil, failTool(tl.Name, "kullanıcı kimliği yok; soru sorulamaz")
	}

	q := Question{
		ID: id.New("mq"), UserID: userID, ConversationID: memoryConvID(tl), Kind: KindLongTerm,
		Content: content, TargetID: strings.TrimSpace(targetID),
		Text: Normalize(question), Options: usableOptions(optionsOf(in["options"])),
	}
	// Önceki metin arayüzde "şundan → şuna" gösterilir.
	if q.TargetID != "" {
		if prev, err := t.svc.Get(ctx, userID, q.TargetID); err == nil && prev != nil {
			q.Previous = prev.Content
		}
	}
	qid := t.ask.Ask(userID, q.ConversationID, q)
	return map[string]any{
		"status":     "asked",
		"questionId": qid,
		"message":    "Soru kullanıcıya iletildi; bu turu bitir, yanıt sonraki mesajda gelecek.",
	}, nil
}
