package openai

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

var (
	textToolCallOpeningHint      = "<tool_call"
	textMustacheToolCallOpenHint = "{{"
)

type xmlTextToolCallBlock struct {
	start  int
	end    int
	name   string
	params map[string]string
	err    error
}

type textToolSpec struct {
	name       string
	parameters map[string]any
}

type textToolSynthesisResult struct {
	cleaned        string
	toolCalls      []any
	candidateFound bool
	validationErr  error
}

type candidateMatch struct {
	start         int
	end           int
	name          string
	arguments     string
	validationErr error
}

func collectXMLCandidateMatches(content string, specs map[string]textToolSpec, names map[string]struct{}) ([]candidateMatch, bool) {
	blocks, foundAny := scanXMLTextToolCallBlocks(content)
	if !foundAny {
		return nil, false
	}
	out := make([]candidateMatch, 0)
	for _, block := range blocks {
		if block.err != nil {
			out = append(out, candidateMatch{
				start:         block.start,
				end:           block.end,
				validationErr: block.err,
			})
			continue
		}
		if _, ok := names[block.name]; !ok {
			out = append(out, candidateMatch{
				start:         block.start,
				end:           block.end,
				name:          block.name,
				validationErr: fmt.Errorf("tool %q is not in request tools", block.name),
			})
			continue
		}
		spec := specs[block.name]
		args := map[string]any{}
		for key, value := range block.params {
			if key == "" {
				continue
			}
			args[key] = parseTextToolParameterValue(value)
		}
		args = normalizeTextToolCallArguments(block.name, args, spec)
		encoded, err := json.Marshal(args)
		if err != nil {
			out = append(out, candidateMatch{
				start:         block.start,
				end:           block.end,
				name:          block.name,
				validationErr: fmt.Errorf("failed to marshal arguments for tool %q: %w", block.name, err),
			})
			continue
		}
		out = append(out, candidateMatch{
			start:     block.start,
			end:       block.end,
			name:      block.name,
			arguments: string(encoded),
		})
	}
	return out, foundAny
}

func collectMustacheCandidateMatches(content string, specs map[string]textToolSpec, names map[string]struct{}) ([]candidateMatch, bool) {
	if !strings.Contains(content, textMustacheToolCallOpenHint) {
		return nil, false
	}
	out := make([]candidateMatch, 0)
	foundAny := false
	for start := 0; start < len(content); {
		open := strings.Index(content[start:], textMustacheToolCallOpenHint)
		if open < 0 {
			break
		}
		blockStart := start + open
		nameStart := skipASCIISpaces(content, blockStart+len(textMustacheToolCallOpenHint))
		nameEnd := nameStart
		for nameEnd < len(content) && isTextToolIdentifierChar(content[nameEnd], nameEnd == nameStart) {
			nameEnd++
		}
		name := content[nameStart:nameEnd]
		if name == "" {
			start = blockStart + len(textMustacheToolCallOpenHint)
			continue
		}
		callOpen := skipASCIISpaces(content, nameEnd)
		if callOpen >= len(content) || content[callOpen] != '(' {
			start = blockStart + len(textMustacheToolCallOpenHint)
			continue
		}
		foundAny = true
		argsStart := callOpen + 1
		argsEnd, blockEnd, ok := findMustacheToolCallEnd(content, argsStart)
		if !ok {
			out = append(out, candidateMatch{
				start:         blockStart,
				end:           len(content),
				name:          name,
				validationErr: fmt.Errorf("malformed tool call: unclosed mustache tool call %q", name),
			})
			break
		}
		if _, ok := names[name]; !ok {
			out = append(out, candidateMatch{
				start:         blockStart,
				end:           blockEnd,
				name:          name,
				validationErr: fmt.Errorf("tool %q is not in request tools", name),
			})
			start = blockEnd
			continue
		}
		spec := specs[name]
		args, ok := parseMustacheToolCallArguments(content[argsStart:argsEnd])
		if !ok {
			out = append(out, candidateMatch{
				start:         blockStart,
				end:           blockEnd,
				name:          name,
				validationErr: fmt.Errorf("failed to parse arguments for tool %q", name),
			})
			start = blockEnd
			continue
		}
		args = normalizeTextToolCallArguments(name, args, spec)
		encoded, err := json.Marshal(args)
		if err != nil {
			out = append(out, candidateMatch{
				start:         blockStart,
				end:           blockEnd,
				name:          name,
				validationErr: fmt.Errorf("failed to marshal arguments for tool %q: %w", name, err),
			})
			start = blockEnd
			continue
		}
		out = append(out, candidateMatch{
			start:     blockStart,
			end:       blockEnd,
			name:      name,
			arguments: string(encoded),
		})
		start = blockEnd
	}
	return out, foundAny
}

func synthesizeToolCallsFromTextResult(content string, tools []any, runID string) textToolSynthesisResult {
	specs := requestedToolSpecs(tools)
	names := requestedToolNames(tools)

	xmlMatches, xmlFound := collectXMLCandidateMatches(content, specs, names)
	mustacheMatches, mustacheFound := collectMustacheCandidateMatches(content, specs, names)
	candidateFound := xmlFound || mustacheFound

	var matches []candidateMatch
	matches = append(matches, xmlMatches...)
	matches = append(matches, mustacheMatches...)

	sort.SliceStable(matches, func(i, j int) bool {
		if matches[i].start == matches[j].start {
			return matches[i].end > matches[j].end
		}
		return matches[i].start < matches[j].start
	})

	var cleaned strings.Builder
	cursor := 0
	toolCalls := make([]any, 0)
	var firstErr error

	for _, match := range matches {
		if match.start < cursor {
			continue
		}
		if match.validationErr != nil {
			if firstErr == nil {
				firstErr = match.validationErr
			}
			continue
		}
		cleaned.WriteString(content[cursor:match.start])
		cursor = match.end
		toolCalls = append(toolCalls, map[string]any{
			"id":   textToolCallID(runID, len(toolCalls)+1),
			"type": "function",
			"function": map[string]any{
				"name":      match.name,
				"arguments": match.arguments,
			},
		})
	}

	if firstErr != nil {
		return textToolSynthesisResult{
			cleaned:        content,
			toolCalls:      nil,
			candidateFound: candidateFound,
			validationErr:  firstErr,
		}
	}

	if len(toolCalls) == 0 {
		return textToolSynthesisResult{
			cleaned:        content,
			toolCalls:      nil,
			candidateFound: candidateFound,
			validationErr:  nil,
		}
	}

	cleaned.WriteString(content[cursor:])
	return textToolSynthesisResult{
		cleaned:        strings.TrimSpace(cleaned.String()),
		toolCalls:      toolCalls,
		candidateFound: candidateFound,
		validationErr:  nil,
	}
}

func synthesizeToolCallsFromText(content string, tools []any, runID string) (string, []any) {
	res := synthesizeToolCallsFromTextResult(content, tools, runID)
	if res.validationErr != nil {
		return content, nil
	}
	return res.cleaned, res.toolCalls
}

func textToolCallID(runID string, ordinal int) string {
	normalized := normalizeToolCallIDPart(runID)
	if normalized == "" {
		return fmt.Sprintf("call_iop_%d", ordinal)
	}
	return fmt.Sprintf("call_iop_%s_%d", normalized, ordinal)
}

func normalizeToolCallIDPart(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch {
		case r >= 'a' && r <= 'z':
			b.WriteRune(r)
		case r >= 'A' && r <= 'Z':
			b.WriteRune(r)
		case r >= '0' && r <= '9':
			b.WriteRune(r)
		case r == '_' || r == '-':
			b.WriteRune(r)
		default:
			b.WriteByte('_')
		}
	}
	return strings.Trim(b.String(), "_-")
}

func requestedToolNames(tools []any) map[string]struct{} {
	names := make(map[string]struct{}, len(tools))
	for _, tool := range tools {
		m, ok := tool.(map[string]any)
		if !ok {
			continue
		}
		if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" {
			names[strings.TrimSpace(name)] = struct{}{}
		}
		fn, ok := m["function"].(map[string]any)
		if !ok {
			continue
		}
		if name, ok := fn["name"].(string); ok && strings.TrimSpace(name) != "" {
			names[strings.TrimSpace(name)] = struct{}{}
		}
	}
	return names
}

func requestedToolSpecs(tools []any) map[string]textToolSpec {
	specs := make(map[string]textToolSpec, len(tools))
	for _, tool := range tools {
		m, ok := tool.(map[string]any)
		if !ok {
			continue
		}
		if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" {
			name = strings.TrimSpace(name)
			specs[name] = textToolSpec{name: name, parameters: mapValue(m["parameters"])}
		}
		fn, ok := m["function"].(map[string]any)
		if !ok {
			continue
		}
		if name, ok := fn["name"].(string); ok && strings.TrimSpace(name) != "" {
			name = strings.TrimSpace(name)
			specs[name] = textToolSpec{name: name, parameters: mapValue(fn["parameters"])}
		}
	}
	return specs
}

func mapValue(value any) map[string]any {
	m, _ := value.(map[string]any)
	return m
}

func parseTextToolParameterValue(raw string) any {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return ""
	}
	var decoded any
	if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil {
		return decoded
	}
	return trimmed
}
