package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"html"
	"regexp"
	"strings"

	"go-micro.dev/v6/ai"
)

var fencedJSONBlock = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```")
var taggedToolCallBlock = regexp.MustCompile(`(?s)<[^<>]*(?:tool_call|tool_calls|function=)[^<>]*>(.*?)</[^<>]*>`)
var singleTaggedToolCall = regexp.MustCompile(`(?s)<(tool_call\b[^<>]*|[^<>]*function\s*=[^<>]*)>(.*?)</[^<>]*>`)
var taggedToolNameAttr = regexp.MustCompile(`(?i)(?:function|name|tool)\s*=\s*["\']?([^"\'\s>]+)`)
var openingTaggedToolCall = regexp.MustCompile(`(?i)<(tool_call\b[^<>]*|[^<>]*function\s*=[^<>]*)>`)

type textToolCall struct {
	ID        string         `json:"id"`
	Name      string         `json:"name"`
	Tool      string         `json:"tool"`
	Input     map[string]any `json:"input"`
	Arguments any            `json:"arguments"`
	Function  *textToolCall  `json:"function"`
}

// executeTextToolCalls is a compatibility fallback for providers that return a
// tool call as text JSON instead of a structured tool_calls field. It only runs
// calls whose names match the tools offered to the model, so ordinary JSON
// answers are left untouched.
func (a *agentImpl) executeTextToolCalls(ctx context.Context, reply string, tools []ai.Tool) ([]ai.ToolCall, string, bool) {
	calls := parseTextToolCalls(reply, tools)
	if len(calls) == 0 {
		return nil, "", false
	}

	handler := a.toolHandler()
	results := make([]string, 0, len(calls))
	for i := range calls {
		result := handler(ctx, calls[i])
		calls[i].Result = result.Content
		if result.Refused != "" {
			calls[i].Error = result.Refused
		}
		if result.Content != "" {
			results = append(results, result.Content)
		}
	}
	return calls, strings.Join(results, "\n"), true
}

// executeAdditionalTextToolCalls runs text-encoded tool calls that accompany a
// structured tool_calls response. Some OpenAI-compatible providers can mix the
// two forms in a single assistant turn: for example, emitting a native
// conformance_echo call while rendering a follow-up guarded delegate call as
// <tool_call name="delegate">...</tool_call> text. Keep this fallback additive
// and de-duplicate calls already represented in the structured tool_calls list.
func (a *agentImpl) executeAdditionalTextToolCalls(ctx context.Context, reply string, tools []ai.Tool, existing []ai.ToolCall) ([]ai.ToolCall, string, bool) {
	calls := parseTextToolCalls(reply, tools)
	if len(calls) == 0 {
		return nil, "", false
	}

	seen := map[string]bool{}
	for _, call := range existing {
		seen[textToolCallKey(call)] = true
	}

	handler := a.toolHandler()
	out := make([]ai.ToolCall, 0, len(calls))
	results := make([]string, 0, len(calls))
	for i := range calls {
		if seen[textToolCallKey(calls[i])] {
			continue
		}
		result := handler(ctx, calls[i])
		calls[i].Result = result.Content
		if result.Refused != "" {
			calls[i].Error = result.Refused
		}
		if result.Content != "" {
			results = append(results, result.Content)
		}
		out = append(out, calls[i])
	}
	return out, strings.Join(results, "\n"), len(out) > 0
}

func textToolCallKey(call ai.ToolCall) string {
	b, _ := json.Marshal(call.Input)
	return call.Name + "\x00" + string(b)
}

func parseTextToolCalls(text string, tools []ai.Tool) []ai.ToolCall {
	text = html.UnescapeString(text)
	allowed := textToolNames(tools)
	if len(allowed) == 0 {
		return nil
	}

	if calls := decodeTaggedTextToolCalls(text, allowed); len(calls) > 0 {
		return calls
	}
	if calls := decodeFunctionTextToolCalls(text, allowed); len(calls) > 0 {
		return calls
	}
	for _, candidate := range jsonCandidates(text) {
		if calls := decodeTextToolCalls(candidate, allowed); len(calls) > 0 {
			return calls
		}
	}
	return nil
}

func partialTextToolCallName(text string, tools []ai.Tool) string {
	text = html.UnescapeString(text)
	allowed := textToolNames(tools)
	if len(allowed) == 0 {
		return ""
	}
	openMatches := openingTaggedToolCall.FindAllStringSubmatchIndex(text, -1)
	if len(openMatches) == 0 {
		return ""
	}
	closedMatches := singleTaggedToolCall.FindAllStringSubmatchIndex(text, -1)
	for _, open := range openMatches {
		closed := false
		for _, match := range closedMatches {
			if match[0] == open[0] {
				closed = true
				break
			}
		}
		if closed {
			continue
		}
		tag := text[open[2]:open[3]]
		name := taggedToolName(tag)
		if canonical := allowed[name]; canonical != "" {
			return canonical
		}
	}
	return ""
}

func textToolNames(tools []ai.Tool) map[string]string {
	allowed := map[string]string{}
	for _, tool := range tools {
		addTextToolName(allowed, tool.Name, tool.Name)
		if tool.OriginalName != "" {
			addTextToolName(allowed, tool.OriginalName, tool.Name)
		}
	}
	return allowed
}

func addTextToolName(allowed map[string]string, name, canonical string) {
	if name == "" || canonical == "" {
		return
	}
	allowed[name] = canonical
	// Some OpenAI-compatible models describe an idempotent Add endpoint as a
	// creation action and emit the otherwise-correct service tool with a Create
	// suffix in text-only tool-call markup. Keep the fallback bounded by the
	// offered service tool prefix so ordinary unknown tools remain ignored.
	for _, suffix := range []string{"_Add", ".Add"} {
		if strings.HasSuffix(name, suffix) {
			allowed[strings.TrimSuffix(name, suffix)+strings.Replace(suffix, "Add", "Create", 1)] = canonical
		}
	}
}

func jsonCandidates(text string) []string {
	trimmed := strings.TrimSpace(text)
	var out []string
	if trimmed != "" {
		out = append(out, trimmed)
	}
	for _, match := range fencedJSONBlock.FindAllStringSubmatch(text, -1) {
		if len(match) > 1 {
			out = append(out, strings.TrimSpace(match[1]))
		}
	}
	for _, match := range taggedToolCallBlock.FindAllStringSubmatch(text, -1) {
		if len(match) > 1 {
			out = append(out, strings.TrimSpace(match[1]))
		}
	}
	if start, end := strings.IndexAny(text, "[{"), strings.LastIndexAny(text, "]}"); start >= 0 && end > start {
		out = append(out, strings.TrimSpace(text[start:end+1]))
	}
	return out
}

func decodeTextToolCalls(candidate string, allowed map[string]string) []ai.ToolCall {
	var root any
	if err := json.Unmarshal([]byte(candidate), &root); err != nil {
		return nil
	}
	return collectTextToolCalls(root, allowed)
}

func collectTextToolCalls(v any, allowed map[string]string) []ai.ToolCall {
	switch x := v.(type) {
	case []any:
		var out []ai.ToolCall
		for _, item := range x {
			out = append(out, collectTextToolCalls(item, allowed)...)
		}
		return out
	case map[string]any:
		if nested, ok := firstNestedToolCalls(x); ok {
			return collectTextToolCalls(nested, allowed)
		}
		call := mapToTextToolCall(x)
		name, input := textToolCallNameAndInput(call)
		if name == "" || allowed[name] == "" || input == nil {
			return nil
		}
		id := call.ID
		if id == "" {
			id = fmt.Sprintf("text-call-%s", strings.ReplaceAll(name, ".", "_"))
		}
		return []ai.ToolCall{{ID: id, Name: allowed[name], Input: input}}
	default:
		return nil
	}
}

func textToolCallNameAndInput(call textToolCall) (string, map[string]any) {
	name := call.Name
	if name == "" {
		name = call.Tool
	}
	input := call.Input
	if input == nil {
		input = textToolArguments(call.Arguments)
	}
	if call.Function != nil {
		fnName, fnInput := textToolCallNameAndInput(*call.Function)
		if name == "" {
			name = fnName
		}
		if input == nil {
			input = fnInput
		}
	}
	return name, input
}

func textToolArguments(raw any) map[string]any {
	switch args := raw.(type) {
	case map[string]any:
		return args
	case string:
		var input map[string]any
		if err := json.Unmarshal([]byte(args), &input); err == nil {
			return input
		}
	}
	return nil
}

func containsNestedTextToolCall(v any) bool {
	switch x := v.(type) {
	case string:
		text := html.UnescapeString(x)
		return openingTaggedToolCall.MatchString(text) || singleTaggedToolCall.MatchString(text)
	case map[string]any:
		for _, item := range x {
			if containsNestedTextToolCall(item) {
				return true
			}
		}
	case []any:
		for _, item := range x {
			if containsNestedTextToolCall(item) {
				return true
			}
		}
	}
	return false
}

func decodeTaggedTextToolCalls(text string, allowed map[string]string) []ai.ToolCall {
	var out []ai.ToolCall
	for _, match := range singleTaggedToolCall.FindAllStringSubmatch(text, -1) {
		if len(match) < 3 {
			continue
		}
		tag, body := match[1], strings.TrimSpace(match[2])
		if calls := decodeTextToolCalls(body, allowed); len(calls) > 0 {
			out = append(out, calls...)
			continue
		}
		if calls := decodeTaggedTextToolCalls(body, allowed); len(calls) > 0 {
			out = append(out, calls...)
			continue
		}
		name := taggedToolName(tag)
		if name == "" || allowed[name] == "" {
			continue
		}
		var input map[string]any
		if err := json.Unmarshal([]byte(body), &input); err != nil || input == nil {
			continue
		}
		out = append(out, ai.ToolCall{
			ID:    fmt.Sprintf("text-call-%s", strings.ReplaceAll(name, ".", "_")),
			Name:  allowed[name],
			Input: input,
		})
	}
	return out
}

func taggedToolName(tag string) string {
	match := taggedToolNameAttr.FindStringSubmatch(tag)
	if len(match) < 2 {
		return ""
	}
	return strings.Trim(match[1], `"'`)
}

func decodeFunctionTextToolCalls(text string, allowed map[string]string) []ai.ToolCall {
	var out []ai.ToolCall
	for alias, canonical := range allowed {
		for _, body := range functionCallBodies(text, alias) {
			var input map[string]any
			if err := json.Unmarshal([]byte(body), &input); err != nil || input == nil {
				continue
			}
			out = append(out, ai.ToolCall{
				ID:    fmt.Sprintf("text-call-%s", strings.ReplaceAll(alias, ".", "_")),
				Name:  canonical,
				Input: input,
			})
		}
	}
	return out
}

func functionCallBodies(text, name string) []string {
	if name == "" {
		return nil
	}
	var bodies []string
	for searchFrom := 0; searchFrom < len(text); {
		idx := strings.Index(text[searchFrom:], name)
		if idx < 0 {
			break
		}
		start := searchFrom + idx
		open := start + len(name)
		if !isFunctionCallBoundary(text, start, open) {
			searchFrom = start + len(name)
			continue
		}
		bodyStart := open + 1
		bodyEnd, ok := balancedJSONObjectEnd(text, bodyStart)
		if !ok {
			searchFrom = bodyStart
			continue
		}
		bodies = append(bodies, strings.TrimSpace(text[bodyStart:bodyEnd]))
		searchFrom = bodyEnd + 1
	}
	return bodies
}

func isFunctionCallBoundary(text string, start, open int) bool {
	if open >= len(text) || text[open] != '(' {
		return false
	}
	if start > 0 {
		prev := text[start-1]
		if prev == '_' || prev == '.' || prev == '-' || prev == '$' || ('0' <= prev && prev <= '9') || ('A' <= prev && prev <= 'Z') || ('a' <= prev && prev <= 'z') {
			return false
		}
	}
	for i := open + 1; i < len(text); i++ {
		switch text[i] {
		case ' ', '\n', '\r', '\t':
			continue
		case '{':
			return true
		default:
			return false
		}
	}
	return false
}

func balancedJSONObjectEnd(text string, start int) (int, bool) {
	for start < len(text) {
		switch text[start] {
		case ' ', '\n', '\r', '\t':
			start++
		case '{':
			depth := 0
			inString := false
			escaped := false
			for i := start; i < len(text); i++ {
				c := text[i]
				if inString {
					if escaped {
						escaped = false
					} else if c == '\\' {
						escaped = true
					} else if c == '"' {
						inString = false
					}
					continue
				}
				switch c {
				case '"':
					inString = true
				case '{':
					depth++
				case '}':
					depth--
					if depth == 0 {
						for j := i + 1; j < len(text); j++ {
							switch text[j] {
							case ' ', '\n', '\r', '\t':
								continue
							case ')':
								return i + 1, true
							default:
								return 0, false
							}
						}
					}
				}
			}
			return 0, false
		default:
			return 0, false
		}
	}
	return 0, false
}

func firstNestedToolCalls(m map[string]any) (any, bool) {
	for _, key := range []string{"tool_calls", "toolCalls", "calls"} {
		if v, ok := m[key]; ok {
			return v, true
		}
	}
	return nil, false
}

func mapToTextToolCall(m map[string]any) textToolCall {
	b, _ := json.Marshal(m)
	var call textToolCall
	_ = json.Unmarshal(b, &call)
	return call
}
