package clients

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/http"
	"sort"
	"strings"
	"time"
)

const (
	defaultMaxJSONResponseBytes int64 = 16 << 20
	mediaMaxJSONResponseBytes   int64 = 128 << 20
)

func credential(candidate map[string]any, keys ...string) string {
	for _, key := range keys {
		if value, ok := candidate[key].(string); ok && strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func boolValue(body map[string]any, key string) bool {
	value, _ := body[key].(bool)
	return value
}

func stringValue(body map[string]any, key string) string {
	value, _ := body[key].(string)
	return strings.TrimSpace(value)
}

func intValue(body map[string]any, key string, fallback int) int {
	switch value := body[key].(type) {
	case float64:
		return int(math.Round(value))
	case int:
		return value
	default:
		return fallback
	}
}

func decodeHTTPResponse(resp *http.Response) (map[string]any, error) {
	result, _, err := decodeHTTPResponseForProtocol(resp, "")
	return result, err
}

func decodeHTTPResponseForProtocol(resp *http.Response, protocol string) (map[string]any, *WireResponse, error) {
	defer resp.Body.Close()
	maxBytes := maxJSONResponseBytesForProtocol(protocol)
	raw, readErr := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
	tooLarge := int64(len(raw)) > maxBytes
	if tooLarge {
		raw = raw[:maxBytes]
	}
	wire := &WireResponse{
		Protocol:   strings.TrimSpace(protocol),
		StatusCode: resp.StatusCode,
		Headers:    compatibleResponseHeaders(resp.Header),
		// raw is already owned by this response. Reusing it avoids retaining a
		// second copy of media responses that may be as large as 128 MiB.
		RawJSON: raw,
	}
	if readErr != nil {
		if len(raw) > 0 {
			_ = json.Unmarshal(raw, &wire.Body)
		}
		if timeoutErr := transportClientError(readErr); timeoutErr.Code == "timeout" {
			timeoutErr.StatusCode = resp.StatusCode
			timeoutErr.RequestID = requestIDFromHTTPResponse(resp)
			timeoutErr.Wire = wire
			return nil, wire, timeoutErr
		}
		return nil, wire, &ClientError{
			Code:       "response_read_error",
			Message:    readErr.Error(),
			StatusCode: resp.StatusCode,
			RequestID:  requestIDFromHTTPResponse(resp),
			Retryable:  true,
			Wire:       wire,
		}
	}
	if tooLarge {
		if len(raw) > 0 {
			_ = json.Unmarshal(raw, &wire.Body)
		}
		return nil, wire, &ClientError{
			Code:       "response_too_large",
			Message:    fmt.Sprintf("upstream JSON response exceeds %d bytes", maxBytes),
			StatusCode: resp.StatusCode,
			RequestID:  requestIDFromHTTPResponse(resp),
			Retryable:  false,
			Wire:       wire,
		}
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		if len(raw) > 0 {
			_ = json.Unmarshal(raw, &wire.Body)
		}
		return nil, wire, &ClientError{
			Code:       statusCodeName(resp.StatusCode),
			Message:    errorMessage(raw, resp.Status),
			StatusCode: resp.StatusCode,
			RequestID:  requestIDFromHTTPResponse(resp),
			Retryable:  HTTPRetryable(resp.StatusCode),
			Wire:       wire,
		}
	}
	var out map[string]any
	if len(raw) == 0 {
		wire.Body = map[string]any{}
		return map[string]any{}, wire, nil
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, wire, &ClientError{Code: "invalid_response", Message: err.Error(), StatusCode: resp.StatusCode, Retryable: false, Wire: wire}
	}
	// Result and wire metadata are read-only until the runner replaces the
	// canonical result, so they can share the decoded object instead of keeping
	// another full copy of a large Base64 media response.
	wire.Body = out
	return out, wire, nil
}

func maxJSONResponseBytesForProtocol(protocol string) int64 {
	switch strings.TrimSpace(protocol) {
	case ProtocolGeminiGenerateContent, ProtocolOpenAIImages:
		return mediaMaxJSONResponseBytes
	default:
		return defaultMaxJSONResponseBytes
	}
}

func compatibleResponseHeaders(source http.Header) map[string][]string {
	if len(source) == 0 {
		return nil
	}
	allowed := map[string]bool{
		"content-type": true, "retry-after": true,
		"x-request-id": true, "request-id": true,
		"x-ratelimit-limit-requests": true, "x-ratelimit-remaining-requests": true,
		"x-ratelimit-reset-requests": true, "x-ratelimit-limit-tokens": true,
		"x-ratelimit-remaining-tokens": true, "x-ratelimit-reset-tokens": true,
		"x-goog-request-id": true, "x-goog-upload-status": true,
		"x-goog-upload-url": true, "x-goog-upload-size-received": true,
	}
	out := map[string][]string{}
	for key, values := range source {
		if !allowed[strings.ToLower(strings.TrimSpace(key))] {
			continue
		}
		out[http.CanonicalHeaderKey(key)] = append([]string(nil), values...)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func notifySubmissionStarted(request Request) error {
	if request.OnUpstreamSubmissionStarted == nil {
		return nil
	}
	return request.OnUpstreamSubmissionStarted()
}

func notifyResponseReceived(request Request) error {
	if request.OnUpstreamResponseReceived == nil {
		return nil
	}
	return request.OnUpstreamResponseReceived()
}

func notifyWireResponse(request Request, wire *WireResponse) error {
	if wire == nil || request.OnUpstreamWireResponse == nil {
		return nil
	}
	return request.OnUpstreamWireResponse(wire)
}

func decodeOpenAIStreamResponse(resp *http.Response, onDelta StreamDelta) (map[string]any, error) {
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		raw, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024*1024))
		return nil, &ClientError{
			Code:       statusCodeName(resp.StatusCode),
			Message:    errorMessage(raw, resp.Status),
			StatusCode: resp.StatusCode,
			RequestID:  requestIDFromHTTPResponse(resp),
			Retryable:  HTTPRetryable(resp.StatusCode),
		}
	}
	if result, ok, err := decodeOpenAIStreamReader(resp.Body, onDelta); ok || err != nil {
		return result, err
	}
	return map[string]any{}, nil
}

func decodeOpenAIStreamReader(reader io.Reader, onDelta StreamDelta) (map[string]any, bool, error) {
	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
	rawLines := make([]string, 0)
	parts := make([]string, 0)
	refusalParts := make([]string, 0)
	streamLogprobs := make([]any, 0)
	streamAnnotations := make([]any, 0)
	reasoningParts := make([]string, 0)
	var last map[string]any
	var usage Usage
	finishReason := ""
	toolCalls := map[int]map[string]any{}
	for scanner.Scan() {
		rawLine := scanner.Text()
		rawLines = append(rawLines, rawLine)
		line := strings.TrimSpace(rawLine)
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if payload == "" || payload == "[DONE]" {
			continue
		}
		var event map[string]any
		if err := json.Unmarshal([]byte(payload), &event); err != nil {
			continue
		}
		event = NormalizeChatCompletionStreamEvent(event)
		last = event
		text := streamEventText(event)
		reasoningText := streamEventReasoningContent(event)
		if text != "" {
			parts = append(parts, text)
		}
		if reasoningText != "" {
			reasoningParts = append(reasoningParts, reasoningText)
		}
		if refusal := streamEventRefusal(event); refusal != "" {
			refusalParts = append(refusalParts, refusal)
		}
		streamLogprobs = append(streamLogprobs, streamEventLogprobs(event)...)
		streamAnnotations = append(streamAnnotations, streamEventAnnotations(event)...)
		aggregateStreamToolCalls(event, toolCalls)
		if reason := streamEventFinishReason(event); reason != "" {
			finishReason = reason
		}
		if onDelta != nil {
			if err := onDelta(StreamDeltaEvent{Text: text, ReasoningContent: reasoningText, Event: event}); err != nil {
				return nil, true, err
			}
		}
		if eventUsage := usageFromOpenAI(event); eventUsage.TotalTokens > 0 {
			usage = eventUsage
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, true, &ClientError{Code: "stream_read_error", Message: err.Error(), Retryable: true}
	}
	if last == nil {
		raw := []byte(strings.Join(rawLines, "\n"))
		if len(raw) == 0 {
			return map[string]any{}, true, nil
		}
		var out map[string]any
		if err := json.Unmarshal(raw, &out); err != nil {
			return nil, false, nil
		}
		return out, true, nil
	}
	return buildOpenAIStreamResult(last, parts, refusalParts, streamLogprobs, streamAnnotations, reasoningParts, toolCalls, finishReason, usage), true, nil
}

func decodeOpenAIStream(raw []byte) (map[string]any, bool) {
	if !bytes.Contains(raw, []byte("data:")) {
		return nil, false
	}
	result, ok, err := decodeOpenAIStreamReader(bytes.NewReader(raw), nil)
	return result, ok && err == nil
}

func buildOpenAIStreamResult(last map[string]any, parts []string, refusalParts []string, streamLogprobs []any, streamAnnotations []any, reasoningParts []string, toolCalls map[int]map[string]any, finishReason string, usage Usage) map[string]any {
	if len(parts) == 0 && len(refusalParts) == 0 && len(streamLogprobs) == 0 && len(streamAnnotations) == 0 && len(reasoningParts) == 0 && len(toolCalls) == 0 {
		return last
	}
	message := map[string]any{
		"role":    "assistant",
		"content": strings.Join(parts, ""),
	}
	if len(reasoningParts) > 0 {
		message["reasoning_content"] = strings.Join(reasoningParts, "")
	}
	if len(refusalParts) > 0 {
		message["refusal"] = strings.Join(refusalParts, "")
	}
	if len(streamAnnotations) > 0 {
		message["annotations"] = streamAnnotations
	}
	if len(toolCalls) > 0 {
		message["tool_calls"] = sortedStreamToolCalls(toolCalls)
	}
	if finishReason == "" {
		finishReason = "stop"
	}
	var out map[string]any
	choice := map[string]any{
		"index": 0, "message": message, "finish_reason": finishReason,
	}
	if len(streamLogprobs) > 0 {
		choice["logprobs"] = map[string]any{"content": streamLogprobs, "refusal": nil}
	}
	out = map[string]any{
		"id":      stringFromAny(firstPresent(last["id"], "chatcmpl-stream")),
		"object":  "chat.completion",
		"model":   stringFromAny(last["model"]),
		"choices": []any{choice},
	}
	if usage.TotalTokens > 0 {
		usageMap := map[string]any{
			"prompt_tokens":     usage.InputTokens,
			"completion_tokens": usage.OutputTokens,
			"total_tokens":      usage.TotalTokens,
		}
		if usage.CachedInputTokens > 0 {
			usageMap["prompt_tokens_details"] = map[string]any{
				"cached_tokens": usage.CachedInputTokens,
			}
		}
		out["usage"] = usageMap
	}
	return out
}

func streamEventRefusal(event map[string]any) string {
	choices, _ := event["choices"].([]any)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		delta, _ := choice["delta"].(map[string]any)
		if refusal := stringFromAny(delta["refusal"]); refusal != "" {
			return refusal
		}
	}
	return ""
}

func streamEventLogprobs(event map[string]any) []any {
	choices, _ := event["choices"].([]any)
	out := make([]any, 0)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		logprobs, _ := choice["logprobs"].(map[string]any)
		content, _ := logprobs["content"].([]any)
		out = append(out, content...)
	}
	return out
}

func streamEventAnnotations(event map[string]any) []any {
	choices, _ := event["choices"].([]any)
	out := make([]any, 0)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		delta, _ := choice["delta"].(map[string]any)
		annotations, _ := delta["annotations"].([]any)
		out = append(out, annotations...)
	}
	return out
}

// NormalizeChatCompletionRequestBody 将后续请求里的工具调用上下文还原为
// OpenAI Chat Completions 标准格式，便于再次发送给 OpenAI-compatible 上游。
func NormalizeChatCompletionRequestBody(body map[string]any) map[string]any {
	if body == nil {
		return nil
	}
	out := cloneBody(body)
	messages, ok := out["messages"].([]any)
	if !ok {
		return out
	}
	normalizedMessages := make([]any, 0, len(messages))
	for _, rawMessage := range messages {
		message, ok := rawMessage.(map[string]any)
		if !ok {
			normalizedMessages = append(normalizedMessages, rawMessage)
			continue
		}
		copied := cloneMapAny(message)
		normalizeRequestToolCallsContainer(copied)
		normalizeToolMessageFields(copied)
		toolMessages, cleanContent, changed := toolResultMessagesFromContent(copied["content"])
		if changed {
			if cleanContent != nil && contentHasText(cleanContent) {
				copied["content"] = cleanContent
				normalizedMessages = append(normalizedMessages, copied)
			} else if len(copied) > 1 || copied["role"] != nil {
				delete(copied, "content")
				if len(copied) > 1 {
					normalizedMessages = append(normalizedMessages, copied)
				}
			}
			normalizedMessages = append(normalizedMessages, toolMessages...)
			continue
		}
		normalizedMessages = append(normalizedMessages, copied)
	}
	out["messages"] = normalizedMessages
	return out
}

// normalizeRequestToolCallsContainer preserves canonical OpenAI Chat
// structures. Only known provider aliases are normalized. Legacy
// function_call and standard custom tool calls must remain intact.
func normalizeRequestToolCallsContainer(container map[string]any) {
	if container == nil {
		return
	}
	toolCalls := canonicalToolCalls(container["tool_calls"], false)
	for _, key := range []string{"tool_call", "toolCall", "toolCalls"} {
		if raw, ok := container[key]; ok {
			for _, normalized := range normalizeRawToolCalls(raw, len(toolCalls), false) {
				toolCalls = append(toolCalls, normalized)
			}
			delete(container, key)
		}
	}
	if contentToolCalls, cleanContent, changed := toolCallsFromContent(container["content"], len(toolCalls), false); changed {
		toolCalls = append(toolCalls, contentToolCalls...)
		setNormalizedContent(container, cleanContent, false)
	}
	if partToolCalls := toolCallsFromParts(container["parts"], len(toolCalls), false); len(partToolCalls) > 0 {
		toolCalls = append(toolCalls, partToolCalls...)
		delete(container, "parts")
	}
	if len(toolCalls) > 0 {
		container["tool_calls"] = toolCalls
	}
	if functionCall, ok := container["functionCall"]; ok {
		if _, canonical := container["function_call"]; !canonical {
			if normalized := normalizeToolCall(functionCall, 0, false); normalized != nil {
				container["function_call"] = normalized["function"]
			}
		}
		delete(container, "functionCall")
	}
}

func canonicalToolCalls(value any, stream bool) []any {
	items, ok := value.([]any)
	if !ok {
		if value == nil {
			return nil
		}
		items = []any{value}
	}
	out := make([]any, 0, len(items))
	for index, raw := range items {
		toolCall, _ := raw.(map[string]any)
		typeName := stringFromAny(toolCall["type"])
		if (typeName == "custom" && len(mapFromAny(toolCall["custom"])) > 0) ||
			((typeName == "" || typeName == "function") && len(mapFromAny(toolCall["function"])) > 0) {
			out = append(out, cloneMapAny(toolCall))
			continue
		}
		if normalized := normalizeToolCall(raw, index, stream); normalized != nil {
			out = append(out, normalized)
		}
	}
	return out
}

func cloneMapAny(source map[string]any) map[string]any {
	if source == nil {
		return nil
	}
	out := make(map[string]any, len(source))
	for key, value := range source {
		out[key] = value
	}
	return out
}

// NormalizeChatCompletionResult 将供应商自定义推理字段归一化到
// message.reasoning_content，并从最终回答 content 中剥离内联推理块。
// 加密推理载荷不可展示，且不应作为正文输出，因此会被忽略。
func NormalizeChatCompletionResult(result map[string]any) map[string]any {
	if result == nil {
		return nil
	}
	if usage, ok := result["usage"].(map[string]any); ok && len(usage) > 0 {
		result["usage"] = NormalizeChatCompletionUsage(usage)
	}
	choices, _ := result["choices"].([]any)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		if message, ok := choice["message"].(map[string]any); ok {
			normalizeToolCallsContainer(message, false)
			normalizeReasoningContainer(message, false)
		}
		if delta, ok := choice["delta"].(map[string]any); ok {
			normalizeToolCallsContainer(delta, true)
			normalizeReasoningContainer(delta, true)
		}
	}
	return result
}

// NormalizeChatCompletionStreamEvent 将供应商自定义流式推理字段
// （例如 reasoning_details 或 reasoning）归一化到 delta.reasoning_content。
func NormalizeChatCompletionStreamEvent(event map[string]any) map[string]any {
	if event == nil {
		return nil
	}
	if usage, ok := event["usage"].(map[string]any); ok && len(usage) > 0 {
		event["usage"] = NormalizeChatCompletionUsage(usage)
	}
	choices, _ := event["choices"].([]any)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		if delta, ok := choice["delta"].(map[string]any); ok {
			normalizeToolCallsContainer(delta, true)
			normalizeReasoningContainer(delta, true)
		}
		if message, ok := choice["message"].(map[string]any); ok {
			normalizeToolCallsContainer(message, false)
			normalizeReasoningContainer(message, false)
		}
	}
	return event
}

func NormalizeChatCompletionUsage(usage map[string]any) map[string]any {
	if len(usage) == 0 {
		return usage
	}
	normalized := usageFromOpenAIUsage(usage)
	out := cloneMapAny(usage)
	if normalized.InputTokens > 0 && intFromAny(out["prompt_tokens"]) == 0 {
		out["prompt_tokens"] = normalized.InputTokens
	}
	if normalized.OutputTokens > 0 && intFromAny(out["completion_tokens"]) == 0 {
		out["completion_tokens"] = normalized.OutputTokens
	}
	if normalized.TotalTokens > 0 && intFromAny(out["total_tokens"]) == 0 {
		out["total_tokens"] = normalized.TotalTokens
	}
	if normalized.CachedInputTokens > 0 {
		promptDetails, _ := firstPresent(out["prompt_tokens_details"], out["promptTokensDetails"]).(map[string]any)
		normalizedPromptDetails := cloneMapAny(promptDetails)
		if normalizedPromptDetails == nil {
			normalizedPromptDetails = map[string]any{}
		}
		normalizedPromptDetails["cached_tokens"] = normalized.CachedInputTokens
		out["prompt_tokens_details"] = normalizedPromptDetails
	}
	return out
}

func normalizeToolCallsContainer(container map[string]any, stream bool) {
	if container == nil {
		return
	}
	toolCalls := canonicalToolCalls(container["tool_calls"], stream)
	for _, key := range []string{"tool_call", "toolCall", "toolCalls"} {
		if rawToolCall, ok := container[key]; ok {
			for _, normalized := range normalizeRawToolCalls(rawToolCall, len(toolCalls), stream) {
				toolCalls = append(toolCalls, normalized)
			}
			delete(container, key)
		}
	}
	if functionCall, ok := container["functionCall"]; ok {
		if _, canonical := container["function_call"]; !canonical {
			if normalized := normalizeToolCall(functionCall, 0, stream); normalized != nil {
				container["function_call"] = normalized["function"]
			}
		}
		delete(container, "functionCall")
	}
	if contentToolCalls, cleanContent, changed := toolCallsFromContent(container["content"], len(toolCalls), stream); changed {
		for _, normalized := range contentToolCalls {
			toolCalls = append(toolCalls, normalized)
		}
		setNormalizedContent(container, cleanContent, stream)
	}
	if partToolCalls := toolCallsFromParts(container["parts"], len(toolCalls), stream); len(partToolCalls) > 0 {
		toolCalls = append(toolCalls, partToolCalls...)
		delete(container, "parts")
	}
	if len(toolCalls) > 0 {
		container["tool_calls"] = toolCalls
	}
}

func normalizeToolMessageFields(message map[string]any) {
	if message == nil {
		return
	}
	if id := firstNonEmptyString(message["tool_call_id"], message["toolCallId"], message["tool_use_id"], message["toolUseId"], message["call_id"], message["callId"]); id != "" {
		message["tool_call_id"] = id
	}
	for _, key := range []string{"toolCallId", "tool_use_id", "toolUseId", "call_id", "callId"} {
		delete(message, key)
	}
}

func toolResultMessagesFromContent(value any) ([]any, any, bool) {
	blocks, ok := value.([]any)
	if !ok {
		return nil, nil, false
	}
	toolMessages := make([]any, 0)
	remaining := make([]any, 0, len(blocks))
	for _, rawBlock := range blocks {
		block, _ := rawBlock.(map[string]any)
		if len(block) == 0 || stringFromAny(block["type"]) != "tool_result" {
			remaining = append(remaining, rawBlock)
			continue
		}
		message := map[string]any{
			"role":         "tool",
			"tool_call_id": firstNonEmptyString(block["tool_call_id"], block["toolCallId"], block["tool_use_id"], block["toolUseId"], block["id"]),
			"content":      toolResultContent(block["content"]),
		}
		toolMessages = append(toolMessages, message)
	}
	if len(toolMessages) == 0 {
		return nil, nil, false
	}
	return toolMessages, contentBlocksText(remaining), true
}

func toolResultContent(value any) any {
	if text, ok := value.(string); ok {
		return text
	}
	return jsonStringFromAny(value)
}

func contentHasText(value any) bool {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed) != ""
	case []any:
		return len(typed) > 0
	default:
		return value != nil
	}
}

func rawToolCallValues(container map[string]any) []any {
	values := make([]any, 0, 6)
	for _, key := range []string{"tool_calls", "tool_call", "toolCalls", "toolCall", "function_call", "functionCall"} {
		if value, ok := container[key]; ok {
			values = append(values, value)
		}
	}
	return values
}

func normalizeRawToolCalls(value any, startIndex int, stream bool) []any {
	switch typed := value.(type) {
	case []any:
		out := make([]any, 0, len(typed))
		for _, raw := range typed {
			if toolCall := normalizeToolCall(raw, startIndex+len(out), stream); toolCall != nil {
				out = append(out, toolCall)
			}
		}
		return out
	default:
		if toolCall := normalizeToolCall(value, startIndex, stream); toolCall != nil {
			return []any{toolCall}
		}
		return nil
	}
}

func normalizeToolCall(value any, index int, stream bool) map[string]any {
	source, _ := value.(map[string]any)
	if len(source) == 0 {
		return nil
	}
	if stringFromAny(source["type"]) == "custom" {
		customSource := mapFromAny(source["custom"])
		if len(customSource) == 0 {
			return nil
		}
		toolCall := cloneMapAny(source)
		toolCall["custom"] = cloneMapAny(customSource)
		if stream {
			if _, ok := toolCall["index"]; !ok {
				toolCall["index"] = index
			}
		}
		return toolCall
	}
	functionSource := mapFromAny(source["function"])
	if len(functionSource) == 0 {
		functionSource = mapFromAny(firstPresent(source["function_call"], source["functionCall"]))
	}
	name := firstNonEmptyString(
		functionSource["name"], source["name"], source["function_name"], source["functionName"], source["tool_name"], source["toolName"],
	)
	arguments, hasArguments := toolCallArguments(functionSource)
	if !hasArguments {
		arguments, hasArguments = toolCallArguments(source)
	}
	if name == "" && !hasArguments && firstNonEmptyString(source["id"], source["call_id"], source["callId"], source["tool_call_id"], source["toolCallId"]) == "" {
		return nil
	}
	function := map[string]any{}
	if name != "" {
		function["name"] = name
	}
	if hasArguments {
		function["arguments"] = arguments
	}
	toolCall := map[string]any{
		"type":     firstNonEmptyString(source["type"], "function"),
		"function": function,
	}
	if id := firstNonEmptyString(source["id"], source["call_id"], source["callId"], source["tool_call_id"], source["toolCallId"]); id != "" {
		toolCall["id"] = id
	} else if !stream {
		toolCall["id"] = fmt.Sprintf("call_%d", index)
	}
	if stream {
		if rawIndex, ok := firstPresent(source["index"], source["idx"]).(float64); ok {
			toolCall["index"] = int(math.Round(rawIndex))
		} else if rawIndex, ok := firstPresent(source["index"], source["idx"]).(int); ok {
			toolCall["index"] = rawIndex
		} else {
			toolCall["index"] = index
		}
	}
	return toolCall
}

func toolCallsFromContent(value any, startIndex int, stream bool) ([]any, any, bool) {
	blocks, ok := value.([]any)
	if !ok {
		return nil, nil, false
	}
	toolCalls := make([]any, 0)
	remaining := make([]any, 0, len(blocks))
	containsReasoning := false
	for _, rawBlock := range blocks {
		block, _ := rawBlock.(map[string]any)
		if len(block) == 0 {
			remaining = append(remaining, rawBlock)
			continue
		}
		switch stringFromAny(block["type"]) {
		case "tool_use":
			if toolCall := normalizeToolUseBlock(block, startIndex+len(toolCalls), stream); toolCall != nil {
				toolCalls = append(toolCalls, toolCall)
			}
		case "tool_result":
			remaining = append(remaining, rawBlock)
		default:
			if isReasoningContentBlock(block) {
				containsReasoning = true
			}
			remaining = append(remaining, rawBlock)
		}
	}
	if len(toolCalls) == 0 {
		return nil, nil, false
	}
	if containsReasoning {
		return toolCalls, remaining, true
	}
	return toolCalls, contentBlocksText(remaining), true
}

func normalizeToolUseBlock(block map[string]any, index int, stream bool) map[string]any {
	toolCall := map[string]any{
		"type": "function",
		"function": map[string]any{
			"name":      stringFromAny(block["name"]),
			"arguments": jsonStringFromAny(block["input"]),
		},
	}
	if id := firstNonEmptyString(block["id"], block["tool_use_id"], block["toolUseId"]); id != "" {
		toolCall["id"] = id
	} else if !stream {
		toolCall["id"] = fmt.Sprintf("call_%d", index)
	}
	if stream {
		toolCall["index"] = index
	}
	return toolCall
}

func toolCallsFromParts(value any, startIndex int, stream bool) []any {
	parts, ok := value.([]any)
	if !ok {
		return nil
	}
	out := make([]any, 0)
	for _, rawPart := range parts {
		part, _ := rawPart.(map[string]any)
		if functionCall := mapFromAny(firstPresent(part["functionCall"], part["function_call"])); len(functionCall) > 0 {
			if toolCall := normalizeGeminiFunctionCall(functionCall, startIndex+len(out), stream); toolCall != nil {
				out = append(out, toolCall)
			}
		}
	}
	return out
}

func normalizeGeminiFunctionCall(functionCall map[string]any, index int, stream bool) map[string]any {
	toolCall := map[string]any{
		"type": "function",
		"function": map[string]any{
			"name":      stringFromAny(functionCall["name"]),
			"arguments": jsonStringFromAny(firstPresent(functionCall["args"], functionCall["arguments"])),
		},
	}
	if id := firstNonEmptyString(functionCall["id"], functionCall["call_id"], functionCall["callId"]); id != "" {
		toolCall["id"] = id
	} else if !stream {
		toolCall["id"] = fmt.Sprintf("call_%d", index)
	}
	if stream {
		toolCall["index"] = index
	}
	return toolCall
}

func setNormalizedContent(container map[string]any, value any, stream bool) {
	if text, ok := value.(string); ok && text == "" {
		if stream {
			delete(container, "content")
			return
		}
		container["content"] = nil
		return
	}
	container["content"] = value
}

func isReasoningContentBlock(block map[string]any) bool {
	switch stringFromAny(block["type"]) {
	case "thinking", "redacted_thinking", "reasoning.text", "reasoning.summary", "reasoning.encrypted":
		return true
	default:
		return false
	}
}

func contentBlocksText(blocks []any) string {
	parts := make([]string, 0, len(blocks))
	for _, rawBlock := range blocks {
		switch block := rawBlock.(type) {
		case string:
			parts = append(parts, block)
		case map[string]any:
			if text := stringFromAny(firstPresent(block["text"], block["content"])); text != "" {
				parts = append(parts, text)
			}
		}
	}
	return strings.Join(parts, "")
}

func toolCallArguments(source map[string]any) (string, bool) {
	for _, key := range []string{"arguments", "args", "input", "parameters"} {
		if value, ok := source[key]; ok {
			return jsonStringFromAny(value), true
		}
	}
	return "", false
}

func jsonStringFromAny(value any) string {
	if value == nil {
		return ""
	}
	if text, ok := value.(string); ok {
		return text
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return ""
	}
	return string(encoded)
}

func normalizeReasoningContainer(container map[string]any, deleteEmptyContent bool) {
	if container == nil {
		return
	}
	reasoningParts := make([]string, 0, 3)
	if reasoning := reasoningDetailsText(container["reasoning_details"]); reasoning != "" {
		reasoningParts = append(reasoningParts, reasoning)
	} else if reasoning := stringFromAny(container["reasoning_content"]); reasoning != "" {
		reasoningParts = append(reasoningParts, reasoning)
	} else if reasoning := stringFromAny(container["reasoning"]); reasoning != "" {
		reasoningParts = append(reasoningParts, reasoning)
	}
	if content, ok := container["content"]; ok {
		cleanContent, contentReasoning, changed := normalizeReasoningContentValue(content)
		if changed {
			if deleteEmptyContent {
				if text, ok := cleanContent.(string); ok && text == "" {
					delete(container, "content")
				} else {
					container["content"] = cleanContent
				}
			} else {
				container["content"] = cleanContent
			}
		}
		if contentReasoning != "" {
			reasoningParts = append(reasoningParts, contentReasoning)
		}
	}
	if len(reasoningParts) > 0 {
		container["reasoning_content"] = strings.Join(reasoningParts, "")
	}
	delete(container, "reasoning_details")
	delete(container, "reasoning")
}

func reasoningDetailsText(value any) string {
	rawItems, ok := value.([]any)
	if !ok {
		return ""
	}
	parts := make([]string, 0, len(rawItems))
	for _, rawItem := range rawItems {
		item, _ := rawItem.(map[string]any)
		switch stringFromAny(item["type"]) {
		case "reasoning.text":
			if text := stringFromAny(item["text"]); text != "" {
				parts = append(parts, text)
			}
		case "reasoning.summary":
			if summary := stringFromAny(item["summary"]); summary != "" {
				parts = append(parts, summary)
			}
		}
	}
	return strings.Join(parts, "")
}

func normalizeReasoningContentValue(value any) (any, string, bool) {
	switch typed := value.(type) {
	case string:
		cleanContent, reasoning, changed := splitTaggedReasoningText(typed)
		return cleanContent, reasoning, changed
	case []any:
		contentParts := make([]string, 0, len(typed))
		reasoningParts := make([]string, 0)
		changed := false
		for _, rawItem := range typed {
			switch item := rawItem.(type) {
			case string:
				contentParts = append(contentParts, item)
			case map[string]any:
				switch stringFromAny(item["type"]) {
				case "thinking":
					if thinking := stringFromAny(item["thinking"]); thinking != "" {
						reasoningParts = append(reasoningParts, thinking)
					}
					changed = true
				case "redacted_thinking", "reasoning.encrypted":
					changed = true
				case "reasoning.text":
					if text := stringFromAny(item["text"]); text != "" {
						reasoningParts = append(reasoningParts, text)
					}
					changed = true
				case "reasoning.summary":
					if summary := stringFromAny(item["summary"]); summary != "" {
						reasoningParts = append(reasoningParts, summary)
					}
					changed = true
				case "text", "output_text":
					if text := stringFromAny(firstPresent(item["text"], item["content"])); text != "" {
						cleanText, reasoning, tagged := splitTaggedReasoningText(text)
						contentParts = append(contentParts, cleanText)
						if reasoning != "" {
							reasoningParts = append(reasoningParts, reasoning)
						}
						changed = changed || tagged
					}
				default:
					if text := stringFromAny(firstPresent(item["text"], item["content"])); text != "" {
						contentParts = append(contentParts, text)
					}
				}
			}
		}
		if !changed {
			return value, "", false
		}
		return strings.Join(contentParts, ""), strings.Join(reasoningParts, ""), true
	default:
		return value, "", false
	}
}

func splitTaggedReasoningText(text string) (string, string, bool) {
	lower := strings.ToLower(text)
	clean := strings.Builder{}
	reasoning := strings.Builder{}
	changed := false
	for offset := 0; offset < len(text); {
		start, tag := nextReasoningOpenTag(lower, offset)
		if start < 0 {
			clean.WriteString(text[offset:])
			break
		}
		clean.WriteString(text[offset:start])
		openEnd := start + len("<"+tag+">")
		closeToken := "</" + tag + ">"
		closeStart := strings.Index(lower[openEnd:], closeToken)
		if closeStart < 0 {
			reasoning.WriteString(text[openEnd:])
			offset = len(text)
			changed = true
			break
		}
		closeStart += openEnd
		reasoning.WriteString(text[openEnd:closeStart])
		offset = closeStart + len(closeToken)
		changed = true
	}
	return clean.String(), reasoning.String(), changed
}

func nextReasoningOpenTag(lower string, offset int) (int, string) {
	bestStart := -1
	bestTag := ""
	for _, tag := range []string{"think", "reasoning", "analysis"} {
		needle := "<" + tag + ">"
		idx := strings.Index(lower[offset:], needle)
		if idx < 0 {
			continue
		}
		absolute := offset + idx
		if bestStart < 0 || absolute < bestStart {
			bestStart = absolute
			bestTag = tag
		}
	}
	return bestStart, bestTag
}

func streamEventText(event map[string]any) string {
	if choices, ok := event["choices"].([]any); ok {
		for _, rawChoice := range choices {
			choice, _ := rawChoice.(map[string]any)
			if delta, ok := choice["delta"].(map[string]any); ok {
				if content, ok := delta["content"].(string); ok {
					return content
				}
			}
			if message, ok := choice["message"].(map[string]any); ok {
				if content, ok := message["content"].(string); ok {
					return content
				}
			}
		}
	}
	if delta, ok := event["delta"].(string); ok {
		return delta
	}
	if text, ok := event["output_text"].(string); ok {
		return text
	}
	return ""
}

func streamEventReasoningContent(event map[string]any) string {
	if choices, ok := event["choices"].([]any); ok {
		for _, rawChoice := range choices {
			choice, _ := rawChoice.(map[string]any)
			if delta, ok := choice["delta"].(map[string]any); ok {
				if content, ok := delta["reasoning_content"].(string); ok {
					return content
				}
				if content, ok := delta["reasoning"].(string); ok {
					return content
				}
			}
			if message, ok := choice["message"].(map[string]any); ok {
				if content, ok := message["reasoning_content"].(string); ok {
					return content
				}
			}
		}
	}
	return ""
}

func streamEventFinishReason(event map[string]any) string {
	if choices, ok := event["choices"].([]any); ok {
		for _, rawChoice := range choices {
			choice, _ := rawChoice.(map[string]any)
			if reason, ok := choice["finish_reason"].(string); ok && reason != "" {
				return reason
			}
		}
	}
	return ""
}

func aggregateStreamToolCalls(event map[string]any, toolCalls map[int]map[string]any) {
	choices, _ := event["choices"].([]any)
	for _, rawChoice := range choices {
		choice, _ := rawChoice.(map[string]any)
		delta, _ := choice["delta"].(map[string]any)
		rawToolCalls := streamToolCallsFromDelta(delta)
		for _, rawToolCall := range rawToolCalls {
			incoming, _ := rawToolCall.(map[string]any)
			index := intFromAny(incoming["index"])
			current := toolCalls[index]
			if current == nil {
				current = map[string]any{}
				toolCalls[index] = current
			}
			for _, key := range []string{"id", "type"} {
				if value, ok := incoming[key].(string); ok && value != "" {
					current[key] = value
				}
			}
			aggregateStreamToolPayload(current, incoming, "function", "arguments")
			aggregateStreamToolPayload(current, incoming, "custom", "input")
		}
	}
}

func streamToolCallsFromDelta(delta map[string]any) []any {
	rawToolCalls, _ := delta["tool_calls"].([]any)
	out := append([]any(nil), rawToolCalls...)
	if functionCall, ok := delta["function_call"].(map[string]any); ok {
		out = append(out, map[string]any{"index": 0, "type": "function", "function": functionCall})
	}
	return out
}

func aggregateStreamToolPayload(current map[string]any, incoming map[string]any, containerKey string, payloadKey string) {
	incomingPayload, _ := incoming[containerKey].(map[string]any)
	if len(incomingPayload) == 0 {
		return
	}
	currentPayload, _ := current[containerKey].(map[string]any)
	if currentPayload == nil {
		currentPayload = map[string]any{}
		current[containerKey] = currentPayload
	}
	if name, ok := incomingPayload["name"].(string); ok && name != "" {
		currentPayload["name"] = stringFromAny(currentPayload["name"]) + name
	}
	if payload, ok := incomingPayload[payloadKey].(string); ok && payload != "" {
		currentPayload[payloadKey] = stringFromAny(currentPayload[payloadKey]) + payload
	}
}

func sortedStreamToolCalls(toolCalls map[int]map[string]any) []any {
	indices := make([]int, 0, len(toolCalls))
	for index := range toolCalls {
		indices = append(indices, index)
	}
	sort.Ints(indices)
	out := make([]any, 0, len(indices))
	for _, index := range indices {
		out = append(out, toolCalls[index])
	}
	return out
}

func usageFromOpenAI(result map[string]any) Usage {
	usage, _ := result["usage"].(map[string]any)
	return usageFromOpenAIUsage(usage)
}

func usageFromOpenAIUsage(usage map[string]any) Usage {
	input := intFromAny(firstPresent(usage["prompt_tokens"], usage["input_tokens"]))
	output := intFromAny(firstPresent(usage["completion_tokens"], usage["output_tokens"]))
	total := intFromAny(usage["total_tokens"])
	if total == 0 {
		total = input + output
	}
	cachedInput, cachedInputKnown := cachedInputTokensValueFromOpenAIUsage(usage)
	if cachedInput > input && input > 0 {
		cachedInput = input
	}
	return Usage{InputTokens: input, OutputTokens: output, CachedInputTokens: cachedInput, CachedInputTokensKnown: cachedInputKnown, TotalTokens: total}
}

func cachedInputTokensFromOpenAIUsage(usage map[string]any) int {
	cachedInput, _ := cachedInputTokensValueFromOpenAIUsage(usage)
	return cachedInput
}

func cachedInputTokensValueFromOpenAIUsage(usage map[string]any) (int, bool) {
	if len(usage) == 0 {
		return 0, false
	}
	promptDetails, _ := firstPresent(usage["prompt_tokens_details"], usage["promptTokensDetails"]).(map[string]any)
	inputDetails, _ := firstPresent(usage["input_tokens_details"], usage["inputTokensDetails"]).(map[string]any)
	usageMetadata, _ := usage["usageMetadata"].(map[string]any)
	value, ok := firstPresentValue(
		promptDetails["cached_tokens"],
		promptDetails["cachedTokens"],
		promptDetails["cache_read_input_tokens"],
		promptDetails["cacheReadInputTokens"],
		inputDetails["cached_tokens"],
		inputDetails["cachedTokens"],
		inputDetails["cache_read_input_tokens"],
		inputDetails["cacheReadInputTokens"],
		usage["prompt_cache_hit_tokens"],
		usage["promptCacheHitTokens"],
		usage["cached_tokens"],
		usage["cachedTokens"],
		usage["cached_input_tokens"],
		usage["cachedInputTokens"],
		usage["cache_read_input_tokens"],
		usage["cacheReadInputTokens"],
		usage["cached_content_token_count"],
		usage["cachedContentTokenCount"],
		usageMetadata["cached_content_token_count"],
		usageMetadata["cachedContentTokenCount"],
	)
	if !ok {
		return 0, false
	}
	return intFromAny(value), true
}

func requestIDFromHTTPResponse(resp *http.Response) string {
	if resp == nil {
		return ""
	}
	for _, key := range []string{
		"x-request-id",
		"x-requestid",
		"request-id",
		"x-amzn-requestid",
		"x-amz-request-id",
		"cf-ray",
	} {
		if value := strings.TrimSpace(resp.Header.Get(key)); value != "" {
			return value
		}
	}
	return ""
}

func requestIDFromResult(result map[string]any) string {
	for _, key := range []string{"request_id", "requestId", "id", "response_id", "responseId"} {
		if value := strings.TrimSpace(stringFromAny(result[key])); value != "" {
			return value
		}
	}
	return ""
}

func intFromAny(value any) int {
	switch typed := value.(type) {
	case float64:
		return int(math.Round(typed))
	case int:
		return typed
	case int64:
		return int(typed)
	default:
		return 0
	}
}

func stringFromAny(value any) string {
	if text, ok := value.(string); ok {
		return text
	}
	return ""
}

func firstNonEmptyString(values ...any) string {
	for _, value := range values {
		if text := strings.TrimSpace(stringFromAny(value)); text != "" {
			return text
		}
	}
	return ""
}

func firstPresent(values ...any) any {
	value, _ := firstPresentValue(values...)
	return value
}

func firstPresentValue(values ...any) (any, bool) {
	for _, value := range values {
		if value != nil {
			return value, true
		}
	}
	return nil, false
}

func errorMessage(raw []byte, fallback string) string {
	if len(raw) == 0 {
		return fallback
	}
	var parsed map[string]any
	if json.Unmarshal(raw, &parsed) == nil {
		if errObj, ok := parsed["error"].(map[string]any); ok {
			if message, ok := errObj["message"].(string); ok {
				return message
			}
		}
		if message, ok := parsed["message"].(string); ok {
			return message
		}
	}
	return string(raw)
}

func statusCodeName(status int) string {
	switch status {
	case http.StatusTooManyRequests:
		return "rate_limit"
	case http.StatusRequestTimeout:
		return "timeout"
	case http.StatusUnauthorized, http.StatusForbidden:
		return "auth_failed"
	default:
		if status >= 500 {
			return "server_error"
		}
		return fmt.Sprintf("http_%d", status)
	}
}

func nowUnix() int64 {
	return time.Now().Unix()
}
