package openai

import (
	"encoding/json"
	"reflect"
	"strings"
)

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 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
}
