package agent

// textcall.go: modelin tool çağrısını YAPISAL tool_calls yerine DÜZ METİN olarak yazdığı
// durumu yakalar.
//
// Canlı bulgu (qwen2.5:7b): model `memory.ask {"question": …}` ya da `memory.remember {…}`
// satırını sohbet metni olarak üretiyordu. Ollama bunu tool_calls'a çevirmediği için
// çağrı HİÇ çalışmıyor, soru kartı çıkmıyor, bilgi kaydedilmiyor ve kullanıcı balonda ham
// `memory.ask {...}` metnini görüyordu. Geçmişe tool çağrısı düz metin olarak yazıldığı için
// (bkz. toolCallText) model bu biçimi taklit ediyor; ikisi birlikte sorunu tetikliyor.
//
// Çözüm iki parça:
//   - parseTextToolCall: metnin TAMAMI bilinen bir tool'a ait çağrıysa onu çağrıya çevirir.
//   - callSniffer: akıştaki ilk karaktere bakar; çağrıya benziyorsa metni kullanıcıya
//     AKITMAZ (tutar), akış bitince ya çağrıya çevirir ya da metin olarak bırakır.
//     Çağrıya benzemeyen yanıtlar gecikmeden akar.
//
// GÜVENLİK: yalnızca bu turda modele SUNULMUŞ tool adları kabul edilir. Metinde geçen
// bilinmeyen/izinsiz bir ad çağrıya dönüşmez; çalıştırmadaki yetki denetimi ayrıca durur.

import (
	"encoding/json"
	"regexp"
	"strings"

	"fiko-ai/internal/llm"
)

// callTextRe: `ad {json}`, `ad({json})` ve geçmiş biçimi `→ ad({json})`.
var callTextRe = regexp.MustCompile(`(?s)^(?:→\s*)?([A-Za-z_][A-Za-z0-9_.\-]*)\s*\(?\s*(\{.*\})\s*\)?$`)

// toolNameSet, modele sunulan tool adlarının kümesidir.
func toolNameSet(tools []llm.ToolSpec) map[string]bool {
	m := make(map[string]bool, len(tools))
	for _, t := range tools {
		m[t.Name] = true
	}
	return m
}

// stripCallWrappers kod çiti ve <tool_call> etiketini söker.
func stripCallWrappers(s string) string {
	s = strings.TrimSpace(s)
	if strings.HasPrefix(s, "```") {
		s = strings.TrimPrefix(s, "```")
		if i := strings.IndexByte(s, '\n'); i >= 0 && !strings.Contains(s[:i], "{") {
			s = s[i+1:] // dil etiketi satırı (```json)
		}
		s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), "```"))
	}
	if strings.HasPrefix(s, "<tool_call>") {
		s = strings.TrimPrefix(s, "<tool_call>")
		s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), "</tool_call>"))
	}
	return strings.TrimSpace(s)
}

// decodeCallArgs argüman nesnesini ya da JSON metnini nesneye çevirir.
func decodeCallArgs(raw json.RawMessage) (map[string]any, bool) {
	raw = json.RawMessage(strings.TrimSpace(string(raw)))
	if len(raw) == 0 || string(raw) == "null" {
		return map[string]any{}, true
	}
	var obj map[string]any
	if err := json.Unmarshal(raw, &obj); err == nil {
		if obj == nil {
			obj = map[string]any{}
		}
		return obj, true
	}
	var asText string
	if err := json.Unmarshal(raw, &asText); err != nil {
		return nil, false
	}
	if err := json.Unmarshal([]byte(asText), &obj); err != nil {
		return nil, false
	}
	if obj == nil {
		obj = map[string]any{}
	}
	return obj, true
}

// parseTextToolCall, içeriğin TAMAMI tek bir tool çağrısıysa onu döndürür.
// Araya serbest metin karışmışsa (açıklama + çağrı) çağrı sayılmaz: yanlış pozitif,
// kaçırılmış çağrıdan kötüdür (kullanıcının cümlesi bir komuta dönüşmemeli).
func parseTextToolCall(content string, names map[string]bool) (llm.ToolCall, bool) {
	s := stripCallWrappers(content)
	if s == "" || len(names) == 0 {
		return llm.ToolCall{}, false
	}
	// Biçim A: ad {json} / ad({json}) / → ad({json})
	if m := callTextRe.FindStringSubmatch(s); m != nil && names[m[1]] {
		if args, ok := decodeCallArgs(json.RawMessage(m[2])); ok {
			return llm.ToolCall{Name: m[1], Arguments: args}, true
		}
	}
	// Biçim B: {"name":"ad","arguments":{…}} (parameters da kabul)
	if strings.HasPrefix(s, "{") {
		var env struct {
			Name       string          `json:"name"`
			Arguments  json.RawMessage `json:"arguments"`
			Parameters json.RawMessage `json:"parameters"`
		}
		if err := json.Unmarshal([]byte(s), &env); err == nil && names[env.Name] {
			raw := env.Arguments
			if len(raw) == 0 {
				raw = env.Parameters
			}
			if args, ok := decodeCallArgs(raw); ok {
				return llm.ToolCall{Name: env.Name, Arguments: args}, true
			}
		}
	}
	return llm.ToolCall{}, false
}

type sniffMode int

const (
	sniffUndecided sniffMode = iota // henüz karar verecek kadar karakter yok
	sniffPass                       // sıradan metin: doğrudan akıt
	sniffHold                       // çağrıya benziyor: akıtma, sonda karar ver
)

func isNameChar(c byte) bool {
	return c == '_' || c == '.' || c == '-' ||
		(c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

// classifyStart, akışın şimdiye kadarki başlangıcına göre karar verir.
func classifyStart(p string, names map[string]bool) sniffMode {
	t := strings.TrimLeft(p, " \t\r\n")
	if t == "" {
		return sniffUndecided
	}
	if strings.HasPrefix(t, "```") {
		// Kod çiti: çitten sonrası çağrıya benziyorsa tut, değilse sıradan kod bloğudur.
		rest := strings.TrimPrefix(t, "```")
		i := strings.IndexByte(rest, '\n')
		if i < 0 {
			return sniffUndecided
		}
		return classifyStart(rest[i+1:], names)
	}
	if strings.HasPrefix("<tool_call>", t) && len(t) < len("<tool_call>") {
		return sniffUndecided // "<tool" gibi eksik etiket
	}
	if strings.HasPrefix(t, "<tool_call>") {
		return sniffHold
	}
	if strings.HasPrefix(t, "→") {
		t = strings.TrimLeft(strings.TrimPrefix(t, "→"), " \t")
		if t == "" {
			return sniffUndecided
		}
	}
	if t[0] == '{' {
		return sniffHold // {"name":…,"arguments":…} biçimi olabilir
	}
	end := 0
	for end < len(t) && isNameChar(t[end]) {
		end++
	}
	if end == 0 {
		return sniffPass
	}
	token := t[:end]
	if end == len(t) {
		// Ad henüz bitmedi: bir tool adının başlangıcıysa bekle.
		for n := range names {
			if strings.HasPrefix(n, token) {
				return sniffUndecided
			}
		}
		return sniffPass
	}
	if !names[token] {
		return sniffPass
	}
	next := strings.TrimLeft(t[end:], " \t\r\n")
	if next == "" {
		return sniffUndecided
	}
	if next[0] == '{' || next[0] == '(' {
		return sniffHold
	}
	return sniffPass
}

// callSniffer, model akışını çağrı-benzerliğine göre süzer.
type callSniffer struct {
	names map[string]bool
	out   llm.DeltaFunc
	buf   strings.Builder
	mode  sniffMode
}

func newCallSniffer(names map[string]bool, out llm.DeltaFunc) *callSniffer {
	return &callSniffer{names: names, out: out}
}

// Delta, sağlayıcının onDelta'sı olarak verilir.
func (s *callSniffer) Delta(d string) error {
	switch s.mode {
	case sniffPass:
		if s.out == nil {
			return nil
		}
		return s.out(d)
	case sniffHold:
		s.buf.WriteString(d)
		return nil
	}
	s.buf.WriteString(d)
	switch classifyStart(s.buf.String(), s.names) {
	case sniffPass:
		return s.flush()
	case sniffHold:
		s.mode = sniffHold
	}
	return nil
}

// flush tutulan metni kullanıcıya akıtır ve geçiş moduna döner.
func (s *callSniffer) flush() error {
	s.mode = sniffPass
	if s.buf.Len() == 0 {
		return nil
	}
	text := s.buf.String()
	s.buf.Reset()
	if s.out == nil {
		return nil
	}
	return s.out(text)
}

// Finish akış bittikten sonra çağrılır. Tutulan metin tam bir tool çağrısıysa sonucu
// (res) ona çevirir ve true döner; değilse tutulan metni olduğu gibi akıtır.
func (s *callSniffer) Finish(res *llm.Result) (bool, error) {
	if s.mode == sniffHold && len(res.ToolCalls) == 0 {
		if call, ok := parseTextToolCall(res.Content, s.names); ok {
			res.ToolCalls = []llm.ToolCall{call}
			res.Content = ""
			s.buf.Reset()
			s.mode = sniffPass
			return true, nil
		}
	}
	return false, s.flush()
}
