package openai

import (
	"encoding/json"
	"fmt"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

type chatCompletionRequest struct {
	Model               string          `json:"model"`
	Messages            []chatMessage   `json:"messages"`
	Stream              bool            `json:"stream"`
	Metadata            json.RawMessage `json:"metadata,omitempty"`
	MaxTokens           *int            `json:"max_tokens,omitempty"`
	MaxCompletionTokens *int            `json:"max_completion_tokens,omitempty"`
	Temperature         *float64        `json:"temperature,omitempty"`
	TopP                *float64        `json:"top_p,omitempty"`
	PresencePenalty     *float64        `json:"presence_penalty,omitempty"`
	FrequencyPenalty    *float64        `json:"frequency_penalty,omitempty"`
	Seed                *int            `json:"seed,omitempty"`
	Stop                any             `json:"stop,omitempty"`
	ResponseFormat      any             `json:"response_format,omitempty"`
	Tools               []any           `json:"tools,omitempty"`
	ToolChoice          any             `json:"tool_choice,omitempty"`
	ParallelToolCalls   *bool           `json:"parallel_tool_calls,omitempty"`
	StreamOptions       any             `json:"stream_options,omitempty"`
	Store               any             `json:"store,omitempty"`
}

type chatMessage struct {
	Role             string `json:"role"`
	Content          string `json:"content"`
	ReasoningContent string `json:"reasoning_content,omitempty"`
	Images           []any  `json:"images,omitempty"`
	ToolCalls        []any  `json:"tool_calls,omitempty"`
	ToolCallID       string `json:"tool_call_id,omitempty"`
	ToolName         string `json:"tool_name,omitempty"`
}

func (m *chatMessage) UnmarshalJSON(b []byte) error {
	var raw struct {
		Role             string `json:"role"`
		Content          any    `json:"content"`
		ReasoningContent string `json:"reasoning_content,omitempty"`
		Images           []any  `json:"images,omitempty"`
		ToolCalls        []any  `json:"tool_calls,omitempty"`
		ToolCallID       string `json:"tool_call_id,omitempty"`
		ToolName         string `json:"tool_name,omitempty"`
	}
	if err := json.Unmarshal(b, &raw); err != nil {
		return err
	}
	m.Role = raw.Role
	m.Content = contentToString(raw.Content)
	m.ReasoningContent = raw.ReasoningContent
	m.Images = raw.Images
	m.ToolCalls = raw.ToolCalls
	m.ToolCallID = raw.ToolCallID
	m.ToolName = raw.ToolName
	return nil
}

func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage, strictOutput, nativeToolCalls bool) map[string]any {
	input := map[string]any{
		"prompt":   prompt,
		"messages": chatMessagesInput(messages),
	}
	options := map[string]any{}
	if req.MaxTokens != nil {
		options["max_tokens"] = *req.MaxTokens
	} else {
		setOptionInt(options, "max_tokens", req.MaxCompletionTokens)
	}
	setOptionFloat(options, "temperature", req.Temperature)
	setOptionFloat(options, "top_p", req.TopP)
	setOptionFloat(options, "presence_penalty", req.PresencePenalty)
	setOptionFloat(options, "frequency_penalty", req.FrequencyPenalty)
	setOptionInt(options, "seed", req.Seed)
	if req.Stop != nil {
		options["stop"] = req.Stop
	}
	if req.ResponseFormat != nil {
		options["response_format"] = req.ResponseFormat
	}
	if len(options) > 0 {
		input["options"] = options
	}
	if strictOutput {
		input["think"] = false
	}
	if len(req.Tools) > 0 {
		input["tools"] = req.Tools
		if nativeToolCalls {
			if shouldForwardProviderToolChoice(req.ToolChoice) {
				input["tool_choice"] = req.ToolChoice
			}
		} else {
			input["tool_choice"] = "none"
		}
	}
	return input
}

func shouldForwardProviderToolChoice(toolChoice any) bool {
	if toolChoice == nil {
		return false
	}
	if s, ok := toolChoice.(string); ok {
		return strings.TrimSpace(s) != "auto"
	}
	return true
}

func setOptionInt(options map[string]any, key string, val *int) {
	if val != nil {
		options[key] = *val
	}
}

func setOptionFloat(options map[string]any, key string, val *float64) {
	if val != nil {
		options[key] = *val
	}
}

type strictOutputPolicy struct {
	Strict              bool
	StreamBuffer        bool
	XMLCompletionTool   string
	XMLResultTag        string
	ContractInstruction bool
}

func chatMessagesInput(messages []chatMessage) []any {
	out := make([]any, 0, len(messages))
	for _, msg := range messages {
		m := map[string]any{
			"role":    msg.Role,
			"content": msg.Content,
		}
		if msg.ReasoningContent != "" {
			m["thinking"] = msg.ReasoningContent
		}
		if len(msg.Images) > 0 {
			m["images"] = msg.Images
		}
		if len(msg.ToolCalls) > 0 {
			m["tool_calls"] = msg.ToolCalls
		}
		if msg.ToolCallID != "" {
			m["tool_call_id"] = msg.ToolCallID
		}
		if msg.ToolName != "" {
			m["tool_name"] = msg.ToolName
		}
		out = append(out, m)
	}
	return out
}

func contentToString(v any) string {
	switch t := v.(type) {
	case nil:
		return ""
	case string:
		return t
	case []any:
		parts := make([]string, 0, len(t))
		for _, item := range t {
			if m, ok := item.(map[string]any); ok {
				if text, ok := m["text"].(string); ok {
					parts = append(parts, text)
				}
			}
		}
		return strings.Join(parts, "\n")
	default:
		b, _ := json.Marshal(t)
		return string(b)
	}
}

var (
	textToolCallBlockRE          = regexp.MustCompile(`(?is)<\s*tool_call\s*>(.*?)<\s*/\s*tool_call\s*>`)
	textToolFunctionRE           = regexp.MustCompile(`(?is)<\s*function\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*function(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`)
	textToolParameterRE          = regexp.MustCompile(`(?is)<\s*parameter\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*parameter(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`)
	textToolCallOpeningHint      = "<tool_call"
	textMustacheToolCallOpenHint = "{{"
)

type textToolCallMatch struct {
	start     int
	end       int
	name      string
	arguments string
}

type mustacheTextToolCallBlock struct {
	start int
	end   int
	name  string
	args  string
}

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

func synthesizeToolCallsFromText(content string, tools []any, runID string) (string, []any) {
	specs := requestedToolSpecs(tools)
	if len(specs) == 0 {
		return content, nil
	}
	matches := collectTextToolCallMatches(content, specs)
	if len(matches) == 0 {
		return content, nil
	}

	var cleaned strings.Builder
	cursor := 0
	toolCalls := make([]any, 0, len(matches))
	for _, match := range matches {
		if match.start < cursor {
			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 len(toolCalls) == 0 {
		return content, nil
	}
	cleaned.WriteString(content[cursor:])
	return strings.TrimSpace(cleaned.String()), toolCalls
}

func collectTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
	var matches []textToolCallMatch
	matches = append(matches, collectXMLTextToolCallMatches(content, specs)...)
	matches = append(matches, collectMustacheTextToolCallMatches(content, specs)...)
	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
	})
	return matches
}

func collectXMLTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
	if !strings.Contains(strings.ToLower(content), textToolCallOpeningHint) {
		return nil
	}
	rawMatches := textToolCallBlockRE.FindAllStringSubmatchIndex(content, -1)
	if len(rawMatches) == 0 {
		return nil
	}
	out := make([]textToolCallMatch, 0, len(rawMatches))
	for _, rawMatch := range rawMatches {
		block := content[rawMatch[0]:rawMatch[1]]
		name, arguments, ok := parseTextToolCallBlock(block, specs)
		if !ok {
			continue
		}
		out = append(out, textToolCallMatch{
			start:     rawMatch[0],
			end:       rawMatch[1],
			name:      name,
			arguments: arguments,
		})
	}
	return out
}

func collectMustacheTextToolCallMatches(content string, specs map[string]textToolSpec) []textToolCallMatch {
	if !strings.Contains(content, textMustacheToolCallOpenHint) {
		return nil
	}
	blocks := scanMustacheTextToolCallBlocks(content)
	if len(blocks) == 0 {
		return nil
	}
	out := make([]textToolCallMatch, 0, len(blocks))
	for _, block := range blocks {
		spec, ok := specs[block.name]
		if !ok {
			continue
		}
		args, ok := parseMustacheToolCallArguments(block.args)
		if !ok {
			continue
		}
		args = normalizeTextToolCallArguments(block.name, args, spec)
		encoded, err := json.Marshal(args)
		if err != nil {
			continue
		}
		out = append(out, textToolCallMatch{
			start:     block.start,
			end:       block.end,
			name:      block.name,
			arguments: string(encoded),
		})
	}
	return out
}

func scanMustacheTextToolCallBlocks(content string) []mustacheTextToolCallBlock {
	var blocks []mustacheTextToolCallBlock
	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
		}
		argsStart := callOpen + 1
		argsEnd, blockEnd, ok := findMustacheToolCallEnd(content, argsStart)
		if !ok {
			start = blockStart + len(textMustacheToolCallOpenHint)
			continue
		}
		blocks = append(blocks, mustacheTextToolCallBlock{
			start: blockStart,
			end:   blockEnd,
			name:  name,
			args:  content[argsStart:argsEnd],
		})
		start = blockEnd
	}
	return blocks
}

func findMustacheToolCallEnd(content string, argsStart int) (int, int, bool) {
	depth := 0
	var quote byte
	escaped := false
	for i := argsStart; i < len(content); i++ {
		c := content[i]
		if quote != 0 {
			if escaped {
				escaped = false
				continue
			}
			if c == '\\' {
				escaped = true
				continue
			}
			if c == quote {
				quote = 0
			}
			continue
		}
		switch c {
		case '\'', '"':
			quote = c
		case '[', '{', '(':
			depth++
		case ']', '}':
			if depth > 0 {
				depth--
			}
		case ')':
			if depth > 0 {
				depth--
				continue
			}
			closeStart := skipASCIISpaces(content, i+1)
			if closeStart+1 < len(content) && content[closeStart] == '}' && content[closeStart+1] == '}' {
				return i, closeStart + 2, true
			}
		}
	}
	return 0, 0, false
}

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 toolCallsForStreamDelta(toolCalls []any) []any {
	out := make([]any, 0, len(toolCalls))
	for i, item := range toolCalls {
		call, ok := item.(map[string]any)
		if !ok {
			out = append(out, item)
			continue
		}
		delta := make(map[string]any, len(call)+1)
		for k, v := range call {
			delta[k] = v
		}
		delta["index"] = i
		out = append(out, delta)
	}
	return out
}

func normalizeNativeToolCallsForResponse(toolCalls []any, tools []any) []any {
	specs := requestedToolSpecs(tools)
	if len(toolCalls) == 0 || len(specs) == 0 {
		return toolCalls
	}
	var out []any
	for i, item := range toolCalls {
		normalized, changed := normalizeNativeToolCallForResponse(item, specs)
		if !changed {
			if out != nil {
				out[i] = item
			}
			continue
		}
		if out == nil {
			out = make([]any, len(toolCalls))
			copy(out, toolCalls)
		}
		out[i] = normalized
	}
	if out == nil {
		return toolCalls
	}
	return out
}

func normalizeNativeToolCallForResponse(item any, specs map[string]textToolSpec) (any, bool) {
	call, ok := item.(map[string]any)
	if !ok {
		return item, false
	}
	fn, ok := call["function"].(map[string]any)
	if !ok {
		return item, false
	}
	name, _ := fn["name"].(string)
	spec, ok := specs[strings.TrimSpace(name)]
	if !ok {
		return item, false
	}
	arguments, ok := normalizeNativeToolCallArguments(fn["arguments"], spec)
	if !ok {
		return item, false
	}
	nextFn := make(map[string]any, len(fn))
	for key, value := range fn {
		nextFn[key] = value
	}
	nextFn["arguments"] = arguments
	nextCall := make(map[string]any, len(call))
	for key, value := range call {
		nextCall[key] = value
	}
	nextCall["function"] = nextFn
	return nextCall, true
}

func normalizeNativeToolCallArguments(value any, spec textToolSpec) (string, bool) {
	var args map[string]any
	original := ""
	switch v := value.(type) {
	case string:
		original = v
		if err := json.Unmarshal([]byte(v), &args); err != nil {
			return "", false
		}
	case map[string]any:
		args = v
	default:
		return "", false
	}
	if args == nil {
		return "", false
	}
	decoded := decodeStringifiedSchemaValues(args, spec.parameters)
	normalized := normalizeTextToolCallArguments(spec.name, args, spec)
	changed := decoded || !reflect.DeepEqual(normalized, args) || original == ""
	if !changed {
		return "", false
	}
	encoded, err := json.Marshal(normalized)
	if err != nil {
		return "", false
	}
	if original != "" && string(encoded) == original {
		return "", false
	}
	return string(encoded), true
}

func decodeStringifiedSchemaValues(args map[string]any, parameters map[string]any) bool {
	props := schemaObjectProperties(parameters)
	if len(props) == 0 {
		return false
	}
	changed := false
	for key, value := range args {
		schema, ok := props[key]
		if !ok {
			continue
		}
		if decoded, ok := decodeStringifiedSchemaValue(value, schema); ok {
			args[key] = decoded
			changed = true
		}
	}
	return changed
}

func decodeStringifiedSchemaValue(value any, schema any) (any, bool) {
	switch v := value.(type) {
	case string:
		trimmed := strings.TrimSpace(v)
		if trimmed == "" || (!strings.HasPrefix(trimmed, "[") && !strings.HasPrefix(trimmed, "{")) {
			return nil, false
		}
		var decoded any
		if err := json.Unmarshal([]byte(trimmed), &decoded); err != nil {
			return nil, false
		}
		if !schemaAcceptsDecodedValue(decoded, schema) {
			return nil, false
		}
		return decoded, true
	case map[string]any:
		if changed := decodeStringifiedSchemaValues(v, mapValue(schema)); changed {
			return v, true
		}
	case []any:
		itemSchema := schemaArrayItemSchema(mapValue(schema))
		if itemSchema == nil {
			return nil, false
		}
		var out []any
		for i, item := range v {
			decoded, ok := decodeStringifiedSchemaValue(item, itemSchema)
			if !ok {
				if out != nil {
					out[i] = item
				}
				continue
			}
			if out == nil {
				out = make([]any, len(v))
				copy(out, v)
			}
			out[i] = decoded
		}
		if out != nil {
			return out, true
		}
	}
	return nil, false
}

func schemaAcceptsDecodedValue(value any, schema any) bool {
	m := mapValue(schema)
	switch value.(type) {
	case []any:
		return schemaAllowsType(m, "array") || schemaArrayItemSchema(m) != nil
	case map[string]any:
		return schemaAllowsType(m, "object") || len(schemaObjectProperties(m)) > 0
	default:
		return false
	}
}

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 parseTextToolCallBlock(block string, specs map[string]textToolSpec) (string, string, bool) {
	match := textToolFunctionRE.FindStringSubmatchIndex(block)
	if match == nil {
		return "", "", false
	}
	name := strings.TrimSpace(block[match[2]:match[3]])
	spec, ok := specs[name]
	if !ok {
		return "", "", false
	}
	body := block[match[4]:match[5]]
	args := map[string]any{}
	for _, param := range textToolParameterRE.FindAllStringSubmatchIndex(body, -1) {
		key := strings.TrimSpace(body[param[2]:param[3]])
		if key == "" {
			continue
		}
		args[key] = parseTextToolParameterValue(body[param[4]:param[5]])
	}
	args = normalizeTextToolCallArguments(name, args, spec)
	encoded, err := json.Marshal(args)
	if err != nil {
		return "", "", false
	}
	return name, string(encoded), true
}

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
}

func normalizeTextToolCallArguments(_ string, args map[string]any, spec textToolSpec) map[string]any {
	topProps := schemaObjectProperties(spec.parameters)
	commandsSchema := topProps["commands"]
	commandField := schemaSingleCommandField(topProps)
	commands, ok := args["commands"].([]any)
	var out map[string]any
	ensureOut := func() map[string]any {
		if out != nil {
			return out
		}
		out = make(map[string]any, len(args))
		for key, value := range args {
			if shouldStripToolArgumentKey(key, topProps) {
				continue
			}
			out[key] = value
		}
		return out
	}
	for key := range args {
		if shouldStripToolArgumentKey(key, topProps) {
			ensureOut()
			break
		}
	}
	if !ok {
		if out != nil {
			return out
		}
		return args
	}

	if commandField != "" && !schemaHasProperty(topProps, "commands") {
		if rendered, ok := firstCommandAsShellString(commands); ok {
			collapsed := ensureOut()
			delete(collapsed, "commands")
			collapsed[commandField] = rendered
			return collapsed
		}
	}

	itemSchema := schemaArrayItemSchema(commandsSchema)
	itemAllowsString := schemaAllowsType(itemSchema, "string")
	itemAllowsObject := schemaAllowsType(itemSchema, "object") || len(schemaObjectProperties(itemSchema)) > 0
	itemObjectProps := schemaObjectProperties(itemSchema)
	var normalized []any
	for i, command := range commands {
		cleaned, changed := normalizeCommandArgument(command, commandArgSchema{
			allowsString: itemAllowsString,
			allowsObject: itemAllowsObject,
			objectProps:  itemObjectProps,
		})
		if !changed {
			if normalized != nil {
				normalized[i] = command
			}
			continue
		}
		if normalized == nil {
			normalized = make([]any, len(commands))
			copy(normalized, commands)
		}
		normalized[i] = cleaned
	}
	if normalized == nil {
		if out != nil {
			return out
		}
		return args
	}
	ensureOut()["commands"] = normalized
	return out
}

type commandArgSchema struct {
	allowsString bool
	allowsObject bool
	objectProps  map[string]any
}

func normalizeCommandArgument(command any, schema commandArgSchema) (any, bool) {
	if text, ok := command.(string); ok {
		text = strings.TrimSpace(text)
		if text == "" {
			return command, false
		}
		if schema.allowsObject && !schema.allowsString && !commandStringNeedsShell(text) {
			if structured, ok := structuredCommandFromShellWords(text); ok {
				return structured, true
			}
		}
		return text, text != command
	}
	m, ok := command.(map[string]any)
	if !ok {
		return command, false
	}
	commandText, ok := m["command"].(string)
	if !ok {
		return command, false
	}
	if args, ok := stringSliceArgument(m["args"]); ok {
		commandText = strings.TrimSpace(commandText)
		if schema.allowsString {
			return renderShellCommand(commandText, args), true
		}
		if commandExecutableNeedsShell(commandText) || commandArgsNeedShell(args) {
			if schema.allowsString || !schema.allowsObject {
				return renderShellCommand(commandText, args), true
			}
			return cleanedStructuredCommand(commandText, args, schema.objectProps), true
		}
		if schema.allowsObject || !schema.allowsString {
			cleaned := cleanedStructuredCommand(commandText, args, schema.objectProps)
			return cleaned, !commandStructuredArgsAlreadyClean(m, args) || commandObjectHasStrippedKeys(m, schema.objectProps)
		}
		return renderShellCommand(commandText, args), true
	}
	commandText = strings.TrimSpace(commandText)
	if commandText != "" {
		if schema.allowsString {
			return commandText, true
		}
		if commandStringNeedsShell(commandText) {
			return commandText, true
		}
		if (schema.allowsObject || !schema.allowsString) && !commandStringNeedsShell(commandText) {
			structured, ok := structuredCommandFromShellWords(commandText)
			if ok {
				if len(schema.objectProps) > 0 {
					structured = filterMapToSchemaProperties(structured, schema.objectProps)
				}
				return structured, true
			}
		}
		if structured, ok := structuredCommandFromShellWords(commandText); ok && schema.allowsObject && !schema.allowsString {
			return structured, true
		}
		return commandText, true
	}
	return command, false
}

func shouldStripToolArgumentKey(key string, props map[string]any) bool {
	if len(props) > 0 {
		return !schemaHasProperty(props, key)
	}
	return key == "description" || key == "runInTerminal"
}

func schemaHasProperty(props map[string]any, key string) bool {
	_, ok := props[key]
	return ok
}

func schemaSingleCommandField(props map[string]any) string {
	for _, key := range []string{"command", "cmd"} {
		if schemaAllowsType(props[key], "string") {
			return key
		}
	}
	return ""
}

func firstCommandAsShellString(commands []any) (string, bool) {
	if len(commands) == 0 {
		return "", false
	}
	return commandAsShellString(commands[0])
}

func commandAsShellString(command any) (string, bool) {
	switch v := command.(type) {
	case string:
		text := strings.TrimSpace(v)
		return text, text != ""
	case map[string]any:
		commandText, ok := v["command"].(string)
		if !ok {
			return "", false
		}
		args, _ := stringSliceArgument(v["args"])
		text := renderShellCommand(commandText, args)
		return text, strings.TrimSpace(text) != ""
	default:
		return "", false
	}
}

func cleanedStructuredCommand(commandText string, args []string, props map[string]any) map[string]any {
	cleaned := map[string]any{
		"command": commandText,
	}
	if len(args) > 0 {
		cleaned["args"] = args
	}
	if len(props) > 0 {
		return filterMapToSchemaProperties(cleaned, props)
	}
	return cleaned
}

func filterMapToSchemaProperties(m map[string]any, props map[string]any) map[string]any {
	out := make(map[string]any, len(m))
	for key, value := range m {
		if schemaHasProperty(props, key) {
			out[key] = value
		}
	}
	return out
}

func commandObjectHasStrippedKeys(m map[string]any, props map[string]any) bool {
	for key := range m {
		switch key {
		case "command", "args":
			continue
		default:
			if len(props) == 0 || !schemaHasProperty(props, key) {
				return true
			}
		}
	}
	return false
}

func stringSliceArgument(value any) ([]string, bool) {
	if value == nil {
		return nil, false
	}
	raw, ok := value.([]any)
	if !ok {
		return nil, false
	}
	out := make([]string, 0, len(raw))
	for _, item := range raw {
		s, ok := item.(string)
		if !ok {
			return nil, false
		}
		out = append(out, s)
	}
	return out, true
}

func commandStructuredArgsAlreadyClean(m map[string]any, args []string) bool {
	if len(m) != 2 {
		return false
	}
	rawArgs, ok := m["args"].([]any)
	if !ok || len(rawArgs) != len(args) {
		return false
	}
	for i, arg := range args {
		if rawArgs[i] != arg {
			return false
		}
	}
	return true
}

func shellJoinArgs(args []string) string {
	if len(args) == 0 {
		return ""
	}
	quoted := make([]string, 0, len(args))
	for _, arg := range args {
		quoted = append(quoted, shellQuoteArg(arg))
	}
	return " " + strings.Join(quoted, " ")
}

func renderShellCommand(command string, args []string) string {
	return strings.TrimSpace(command) + shellJoinArgs(args)
}

func shellQuoteArg(arg string) string {
	if arg == "" {
		return "''"
	}
	if isShellOperatorToken(arg) {
		return arg
	}
	if !strings.ContainsAny(arg, " \t\n\r|&;<>(){}[]*$`'\"\\") {
		return arg
	}
	return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}

func structuredCommandFromShellWords(text string) (map[string]any, bool) {
	words := strings.Fields(text)
	if len(words) == 0 {
		return nil, false
	}
	out := map[string]any{
		"command": words[0],
	}
	if len(words) > 1 {
		out["args"] = words[1:]
	}
	return out, true
}

func commandExecutableNeedsShell(text string) bool {
	text = strings.TrimSpace(text)
	return text == "" || strings.ContainsAny(text, " \t\n\r|&;<>(){}[]*$`'\"\\") || commandStringNeedsShell(text)
}

func commandArgsNeedShell(args []string) bool {
	for _, arg := range args {
		if isShellOperatorToken(arg) {
			return true
		}
	}
	return false
}

func isShellOperatorToken(arg string) bool {
	switch arg {
	case "&&", "||", "|", "|&", ";", "&", ">", ">>", "<", "<<", "<<<", "2>", "2>>", "&>", "2>&1":
		return true
	default:
		return false
	}
}

func commandStringNeedsShell(text string) bool {
	text = strings.TrimSpace(text)
	if text == "" {
		return false
	}
	if strings.ContainsAny(text, "\n\r|&;<>(){}[]*$`'\"\\") {
		return true
	}
	first := firstShellWord(text)
	switch first {
	case "alias", "bg", "cd", "command", "export", "fg", "hash", "jobs", "read", "set", "source", "test", "type", "ulimit", "umask", "unalias", "unset", "[", "[[", ":":
		return true
	default:
		return false
	}
}

func firstShellWord(s string) string {
	for i, r := range s {
		switch r {
		case ' ', '\t', '\n', '\r':
			return s[:i]
		}
	}
	return s
}

func schemaObjectProperties(schema any) map[string]any {
	m, ok := schema.(map[string]any)
	if !ok {
		return nil
	}
	props, _ := m["properties"].(map[string]any)
	if len(props) > 0 {
		return props
	}
	for _, key := range []string{"oneOf", "anyOf", "allOf"} {
		for _, branch := range schemaBranches(m[key]) {
			if branchProps := schemaObjectProperties(branch); len(branchProps) > 0 {
				if props == nil {
					props = map[string]any{}
				}
				for propKey, propValue := range branchProps {
					props[propKey] = propValue
				}
			}
		}
	}
	return props
}

func schemaArrayItemSchema(schema any) any {
	m, ok := schema.(map[string]any)
	if !ok {
		return nil
	}
	if items, ok := m["items"]; ok {
		return items
	}
	for _, key := range []string{"oneOf", "anyOf", "allOf"} {
		for _, branch := range schemaBranches(m[key]) {
			if item := schemaArrayItemSchema(branch); item != nil {
				return item
			}
		}
	}
	return nil
}

func schemaAllowsType(schema any, want string) bool {
	if schema == nil {
		return false
	}
	m, ok := schema.(map[string]any)
	if !ok {
		return false
	}
	switch t := m["type"].(type) {
	case string:
		if t == want {
			return true
		}
	case []any:
		for _, item := range t {
			if item == want {
				return true
			}
		}
	}
	if want == "object" && len(schemaObjectProperties(schema)) > 0 {
		return true
	}
	if want == "array" && schemaArrayItemSchema(schema) != nil {
		return true
	}
	for _, key := range []string{"oneOf", "anyOf"} {
		for _, branch := range schemaBranches(m[key]) {
			if schemaAllowsType(branch, want) {
				return true
			}
		}
	}
	if branches := schemaBranches(m["allOf"]); len(branches) > 0 {
		for _, branch := range branches {
			if schemaAllowsType(branch, want) {
				return true
			}
		}
	}
	return false
}

func schemaBranches(value any) []any {
	raw, ok := value.([]any)
	if !ok {
		return nil
	}
	return raw
}

func parseMustacheToolCallArguments(raw string) (map[string]any, bool) {
	if args, ok := parseMustacheToolCallArgumentsStrict(raw); ok {
		return args, true
	}
	// Some Cline/provider outputs include one extra "}" before the closing
	// template parens, e.g. `...True}]})}}`. Recover only after strict parsing.
	trimmed := strings.TrimSpace(raw)
	if strings.HasSuffix(trimmed, "}") {
		return parseMustacheToolCallArgumentsStrict(strings.TrimSpace(trimmed[:len(trimmed)-1]))
	}
	return nil, false
}

func parseMustacheToolCallArgumentsStrict(raw string) (map[string]any, bool) {
	trimmed := strings.TrimSpace(raw)
	args := map[string]any{}
	if trimmed == "" {
		return args, true
	}
	for _, part := range splitTopLevel(trimmed, ',') {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		eq := indexTopLevel(part, '=')
		if eq < 0 {
			return nil, false
		}
		key := strings.TrimSpace(part[:eq])
		if !isTextToolIdentifier(key) {
			return nil, false
		}
		value, ok := parseTextToolLiteral(part[eq+1:])
		if !ok {
			return nil, false
		}
		args[key] = value
	}
	return args, true
}

func parseTextToolLiteral(raw string) (any, bool) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", true
	}
	var decoded any
	if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil {
		return decoded, true
	}
	parser := pythonishLiteralParser{s: trimmed}
	value, ok := parser.parseValue()
	if !ok {
		return nil, false
	}
	parser.skipSpaces()
	if parser.pos != len(parser.s) {
		return nil, false
	}
	return value, true
}

func splitTopLevel(s string, sep byte) []string {
	parts := []string{}
	start := 0
	depth := 0
	var quote byte
	escaped := false
	for i := 0; i < len(s); i++ {
		c := s[i]
		if quote != 0 {
			if escaped {
				escaped = false
				continue
			}
			if c == '\\' {
				escaped = true
				continue
			}
			if c == quote {
				quote = 0
			}
			continue
		}
		switch c {
		case '\'', '"':
			quote = c
		case '[', '{', '(':
			depth++
		case ']', '}', ')':
			if depth > 0 {
				depth--
			}
		case sep:
			if depth == 0 {
				parts = append(parts, s[start:i])
				start = i + 1
			}
		}
	}
	parts = append(parts, s[start:])
	return parts
}

func indexTopLevel(s string, target byte) int {
	depth := 0
	var quote byte
	escaped := false
	for i := 0; i < len(s); i++ {
		c := s[i]
		if quote != 0 {
			if escaped {
				escaped = false
				continue
			}
			if c == '\\' {
				escaped = true
				continue
			}
			if c == quote {
				quote = 0
			}
			continue
		}
		switch c {
		case '\'', '"':
			quote = c
		case '[', '{', '(':
			depth++
		case ']', '}', ')':
			if depth > 0 {
				depth--
			}
		default:
			if c == target && depth == 0 {
				return i
			}
		}
	}
	return -1
}

func skipASCIISpaces(s string, pos int) int {
	for pos < len(s) {
		switch s[pos] {
		case ' ', '\n', '\r', '\t':
			pos++
		default:
			return pos
		}
	}
	return pos
}

type pythonishLiteralParser struct {
	s   string
	pos int
}

func (p *pythonishLiteralParser) parseValue() (any, bool) {
	p.skipSpaces()
	if p.pos >= len(p.s) {
		return nil, false
	}
	switch p.s[p.pos] {
	case '\'', '"':
		return p.parseString()
	case '[':
		return p.parseArray()
	case '{':
		return p.parseObject()
	}
	if p.consumeWord("True") || p.consumeWord("true") {
		return true, true
	}
	if p.consumeWord("False") || p.consumeWord("false") {
		return false, true
	}
	if p.consumeWord("None") || p.consumeWord("null") {
		return nil, true
	}
	return p.parseNumberOrBare()
}

func (p *pythonishLiteralParser) parseString() (string, bool) {
	if p.pos >= len(p.s) {
		return "", false
	}
	quote := p.s[p.pos]
	p.pos++
	var b strings.Builder
	for p.pos < len(p.s) {
		c := p.s[p.pos]
		p.pos++
		if c == quote {
			return b.String(), true
		}
		if c != '\\' {
			b.WriteByte(c)
			continue
		}
		if p.pos >= len(p.s) {
			return "", false
		}
		esc := p.s[p.pos]
		p.pos++
		switch esc {
		case '\\', '\'', '"':
			b.WriteByte(esc)
		case 'n':
			b.WriteByte('\n')
		case 'r':
			b.WriteByte('\r')
		case 't':
			b.WriteByte('\t')
		case 'b':
			b.WriteByte('\b')
		case 'f':
			b.WriteByte('\f')
		case 'u':
			if p.pos+4 > len(p.s) {
				return "", false
			}
			r, err := strconv.ParseInt(p.s[p.pos:p.pos+4], 16, 32)
			if err != nil {
				return "", false
			}
			b.WriteRune(rune(r))
			p.pos += 4
		default:
			b.WriteByte(esc)
		}
	}
	return "", false
}

func (p *pythonishLiteralParser) parseArray() ([]any, bool) {
	p.pos++
	items := []any{}
	p.skipSpaces()
	if p.consumeByte(']') {
		return items, true
	}
	for {
		value, ok := p.parseValue()
		if !ok {
			return nil, false
		}
		items = append(items, value)
		p.skipSpaces()
		if p.consumeByte(']') {
			return items, true
		}
		if !p.consumeByte(',') {
			return nil, false
		}
		p.skipSpaces()
		if p.consumeByte(']') {
			return items, true
		}
	}
}

func (p *pythonishLiteralParser) parseObject() (map[string]any, bool) {
	p.pos++
	obj := map[string]any{}
	p.skipSpaces()
	if p.consumeByte('}') {
		return obj, true
	}
	for {
		key, ok := p.parseObjectKey()
		if !ok {
			return nil, false
		}
		p.skipSpaces()
		if !p.consumeByte(':') {
			return nil, false
		}
		value, ok := p.parseValue()
		if !ok {
			return nil, false
		}
		obj[key] = value
		p.skipSpaces()
		if p.consumeByte('}') {
			return obj, true
		}
		if !p.consumeByte(',') {
			return nil, false
		}
		p.skipSpaces()
		if p.consumeByte('}') {
			return obj, true
		}
	}
}

func (p *pythonishLiteralParser) parseObjectKey() (string, bool) {
	p.skipSpaces()
	if p.pos >= len(p.s) {
		return "", false
	}
	if p.s[p.pos] == '\'' || p.s[p.pos] == '"' {
		return p.parseString()
	}
	start := p.pos
	for p.pos < len(p.s) && isTextToolIdentifierChar(p.s[p.pos], p.pos == start) {
		p.pos++
	}
	if start == p.pos {
		return "", false
	}
	return p.s[start:p.pos], true
}

func (p *pythonishLiteralParser) parseNumberOrBare() (any, bool) {
	start := p.pos
	for p.pos < len(p.s) && !isLiteralDelimiter(p.s[p.pos]) {
		p.pos++
	}
	if start == p.pos {
		return nil, false
	}
	token := p.s[start:p.pos]
	if value, ok := parseNumberToken(token); ok {
		return value, true
	}
	if isTextToolIdentifier(token) {
		return token, true
	}
	return nil, false
}

func (p *pythonishLiteralParser) skipSpaces() {
	for p.pos < len(p.s) {
		switch p.s[p.pos] {
		case ' ', '\n', '\r', '\t':
			p.pos++
		default:
			return
		}
	}
}

func (p *pythonishLiteralParser) consumeByte(b byte) bool {
	if p.pos < len(p.s) && p.s[p.pos] == b {
		p.pos++
		return true
	}
	return false
}

func (p *pythonishLiteralParser) consumeWord(word string) bool {
	if !strings.HasPrefix(p.s[p.pos:], word) {
		return false
	}
	end := p.pos + len(word)
	if end < len(p.s) && isBareLiteralChar(p.s[end]) {
		return false
	}
	p.pos = end
	return true
}

func parseNumberToken(token string) (any, bool) {
	if token == "" {
		return nil, false
	}
	if strings.ContainsAny(token, ".eE") {
		value, err := strconv.ParseFloat(token, 64)
		if err != nil {
			return nil, false
		}
		return value, true
	}
	value, err := strconv.ParseInt(token, 10, 64)
	if err != nil {
		return nil, false
	}
	return value, true
}

func isTextToolIdentifier(s string) bool {
	if s == "" {
		return false
	}
	for i := 0; i < len(s); i++ {
		if !isTextToolIdentifierChar(s[i], i == 0) {
			return false
		}
	}
	return true
}

func isTextToolIdentifierChar(c byte, first bool) bool {
	if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' {
		return true
	}
	if !first && (c >= '0' && c <= '9' || c == '.' || c == ':' || c == '-') {
		return true
	}
	return false
}

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

func isLiteralDelimiter(c byte) bool {
	switch c {
	case ',', ']', '}', ' ', '\n', '\r', '\t':
		return true
	default:
		return false
	}
}

func previewString(s string, max int) string {
	if max <= 0 || len(s) <= max {
		return s
	}
	return s[:max] + "...[truncated]"
}

func mapKeys(m map[string]any) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

type chatCompletionResponse struct {
	ID      string                 `json:"id"`
	Object  string                 `json:"object"`
	Created int64                  `json:"created"`
	Model   string                 `json:"model"`
	Choices []chatCompletionChoice `json:"choices"`
	Usage   *openAIUsage           `json:"usage,omitempty"`
}

type chatCompletionChoice struct {
	Index        int         `json:"index"`
	Message      chatMessage `json:"message"`
	FinishReason string      `json:"finish_reason"`
}

type chatCompletionChunk struct {
	ID      string                      `json:"id"`
	Object  string                      `json:"object"`
	Created int64                       `json:"created"`
	Model   string                      `json:"model"`
	Choices []chatCompletionChunkChoice `json:"choices"`
}

type chatCompletionChunkChoice struct {
	Index        int       `json:"index"`
	Delta        chatDelta `json:"delta"`
	FinishReason string    `json:"finish_reason,omitempty"`
}

type chatDelta struct {
	Role             string `json:"role,omitempty"`
	Content          string `json:"content,omitempty"`
	ReasoningContent string `json:"reasoning_content,omitempty"`
	ToolCalls        []any  `json:"tool_calls,omitempty"`
}

type openAIUsage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

type openAIModel struct {
	ID      string `json:"id"`
	Object  string `json:"object"`
	Created int64  `json:"created"`
	OwnedBy string `json:"owned_by"`
}

type openAIModelsResponse struct {
	Object string        `json:"object"`
	Data   []openAIModel `json:"data"`
}

type errorResponse struct {
	Error errorBody `json:"error"`
}

type errorBody struct {
	Type    string `json:"type"`
	Message string `json:"message"`
}

// Responses API types

type responsesRequest struct {
	Model           string          `json:"model"`
	Input           json.RawMessage `json:"input"`
	Instructions    string          `json:"instructions,omitempty"`
	Stream          bool            `json:"stream"`
	Background      bool            `json:"background,omitempty"`
	Metadata        json.RawMessage `json:"metadata,omitempty"`
	MaxOutputTokens *int            `json:"max_output_tokens,omitempty"`
	Temperature     *float64        `json:"temperature,omitempty"`
	TopP            *float64        `json:"top_p,omitempty"`
}

func (req responsesRequest) providerOptions() map[string]any {
	options := map[string]any{}
	if req.MaxOutputTokens != nil {
		options["max_tokens"] = *req.MaxOutputTokens
	}
	setOptionFloat(options, "temperature", req.Temperature)
	setOptionFloat(options, "top_p", req.TopP)
	return options
}

type responsesResponse struct {
	ID         string                `json:"id"`
	Object     string                `json:"object"`
	CreatedAt  int64                 `json:"created_at"`
	Model      string                `json:"model"`
	OutputText string                `json:"output_text"`
	Output     []responsesOutputItem `json:"output"`
	Usage      openAIUsage           `json:"usage"`
}

type responsesOutputItem struct {
	Type    string                 `json:"type"`
	Role    string                 `json:"role"`
	Content []responsesContentItem `json:"content"`
}

type responsesContentItem struct {
	Type string `json:"type"`
	Text string `json:"text"`
}
