package clients

import (
	"bufio"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"mime"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

type GeminiClient struct {
	HTTPClient *http.Client
}

func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error) {
	apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
	if apiKey == "" {
		return Response{}, &ClientError{Code: "missing_credentials", Message: "gemini api key is required", Retryable: false}
	}
	if geminiVeoRequest(request) {
		return c.runVeo(ctx, request, apiKey)
	}
	body := geminiBody(request)
	_, nativeContents := request.Body["contents"]
	if !nativeContents && GeminiUsesOfficialAPI(request.Candidate) {
		var err error
		body, err = c.prepareOfficialGeminiImageFiles(ctx, request, apiKey, body)
		if err != nil {
			return Response{}, err
		}
	} else if !nativeContents {
		var err error
		body, err = prepareCompatibleGeminiInlineImages(ctx, body)
		if err != nil {
			return Response{}, err
		}
	}
	raw, _ := json.Marshal(body)
	endpoint := geminiURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey)
	if request.Stream {
		endpoint = geminiStreamURL(request.Candidate.BaseURL, upstreamModelName(request.Candidate), apiKey)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw))
	if err != nil {
		return Response{}, err
	}
	req.Header.Set("Content-Type", "application/json")
	applyUpstreamIdempotency(req, request)
	responseStartedAt := time.Now()
	if err := notifySubmissionStarted(request); err != nil {
		return Response{}, err
	}
	resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
	if err != nil {
		return Response{}, transportClientError(err)
	}
	if err := notifyResponseReceived(request); err != nil {
		return Response{}, err
	}
	requestID := requestIDFromHTTPResponse(resp)
	var result map[string]any
	var wire *WireResponse
	if request.Stream && resp.StatusCode >= 200 && resp.StatusCode < 300 {
		wire = &WireResponse{Protocol: ProtocolGeminiGenerateContent, StatusCode: resp.StatusCode, Headers: compatibleResponseHeaders(resp.Header)}
		result, err = decodeGeminiStreamResponse(resp, request.StreamDelta, wire)
	} else {
		result, wire, err = decodeHTTPResponseForProtocol(resp, ProtocolGeminiGenerateContent)
	}
	responseFinishedAt := time.Now()
	if err != nil {
		return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
	}
	output, err := geminiResult(request, result)
	if err != nil {
		if requestID == "" {
			requestID = firstNonEmptyString(result["responseId"], result["response_id"])
		}
		return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
	}
	if wire != nil && geminiImageModelType(request.ModelType) {
		// Gemini image responses can contain multi-megabyte Base64 fields. The
		// decoded wire body is still used for native synchronous passthrough, so
		// retaining the original JSON bytes would keep a second full copy alive
		// until the client response finishes.
		wire.RawJSON = nil
	}
	if requestID == "" {
		requestID = requestIDFromResult(output)
	}
	return Response{
		Result:             output,
		RequestID:          requestID,
		Usage:              geminiUsage(result),
		Progress:           providerProgress(request),
		ResponseStartedAt:  responseStartedAt,
		ResponseFinishedAt: responseFinishedAt,
		ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
		UpstreamProtocol:   ProtocolGeminiGenerateContent,
		Wire:               wire,
	}, nil
}

// GeminiUsesOfficialAPI distinguishes the native Google Gemini endpoint from
// Gemini-shaped compatibility services and Google's OpenAI compatibility path.
func GeminiUsesOfficialAPI(candidate store.RuntimeModelCandidate) bool {
	base := strings.TrimSpace(candidate.BaseURL)
	if base == "" {
		return true
	}
	parsed, err := url.Parse(base)
	if err != nil || !strings.EqualFold(parsed.Hostname(), "generativelanguage.googleapis.com") {
		return false
	}
	pathValue := strings.ToLower(strings.TrimRight(parsed.Path, "/"))
	return pathValue != "/openai" && !strings.HasSuffix(pathValue, "/openai")
}

func (c GeminiClient) prepareOfficialGeminiImageFiles(ctx context.Context, request Request, apiKey string, body map[string]any) (map[string]any, error) {
	client := httpClient(request.HTTPClient, c.HTTPClient)
	cache := map[string]map[string]any{}
	prepared, err := prepareOfficialGeminiImageFileValue(ctx, client, request.Candidate, apiKey, body, cache)
	if err != nil {
		return nil, err
	}
	out, _ := prepared.(map[string]any)
	if out == nil {
		return body, nil
	}
	return out, nil
}

func prepareOfficialGeminiImageFileValue(ctx context.Context, client *http.Client, candidate store.RuntimeModelCandidate, apiKey string, value any, cache map[string]map[string]any) (any, error) {
	switch typed := value.(type) {
	case map[string]any:
		next := make(map[string]any, len(typed))
		for key, item := range typed {
			prepared, err := prepareOfficialGeminiImageFileValue(ctx, client, candidate, apiKey, item, cache)
			if err != nil {
				return nil, err
			}
			next[key] = prepared
		}
		for _, key := range []string{"fileData", "file_data"} {
			fileData, ok := next[key].(map[string]any)
			if !ok {
				continue
			}
			fileURI := firstNonEmptyString(fileData["fileUri"], fileData["file_uri"], fileData["uri"])
			mimeType := firstNonEmptyString(fileData["mimeType"], fileData["mime_type"], mimeFromURI(fileURI))
			if !requestAssetStringIsHTTPURL(fileURI) || !strings.HasPrefix(strings.ToLower(mimeType), "image/") || geminiOfficialFileURI(fileURI) {
				continue
			}
			cacheKey := fileURI + "\x00" + mimeType
			uploaded := cache[cacheKey]
			if uploaded == nil {
				var err error
				uploaded, err = uploadOfficialGeminiImageFile(ctx, client, candidate, apiKey, fileURI, mimeType)
				if err != nil {
					fallback, fallbackErr := compatibleGeminiInlineImage(ctx, fileURI, mimeType)
					if fallbackErr != nil {
						return nil, err
					}
					delete(next, key)
					next["inlineData"] = fallback
					continue
				}
				cache[cacheKey] = uploaded
			}
			next[key] = cloneBody(uploaded)
		}
		return next, nil
	case []any:
		next := make([]any, 0, len(typed))
		for _, item := range typed {
			prepared, err := prepareOfficialGeminiImageFileValue(ctx, client, candidate, apiKey, item, cache)
			if err != nil {
				return nil, err
			}
			next = append(next, prepared)
		}
		return next, nil
	default:
		return value, nil
	}
}

func prepareCompatibleGeminiInlineImages(ctx context.Context, body map[string]any) (map[string]any, error) {
	prepared, err := prepareCompatibleGeminiInlineImageValue(ctx, body, map[string]map[string]any{})
	if err != nil {
		return nil, err
	}
	out, _ := prepared.(map[string]any)
	if out == nil {
		return body, nil
	}
	return out, nil
}

func prepareCompatibleGeminiInlineImageValue(ctx context.Context, value any, cache map[string]map[string]any) (any, error) {
	switch typed := value.(type) {
	case map[string]any:
		next := make(map[string]any, len(typed))
		for key, item := range typed {
			prepared, err := prepareCompatibleGeminiInlineImageValue(ctx, item, cache)
			if err != nil {
				return nil, err
			}
			next[key] = prepared
		}
		for _, key := range []string{"fileData", "file_data"} {
			fileData, ok := next[key].(map[string]any)
			if !ok {
				continue
			}
			fileURI := firstNonEmptyString(fileData["fileUri"], fileData["file_uri"], fileData["uri"])
			mimeType := firstNonEmptyString(fileData["mimeType"], fileData["mime_type"], mimeFromURI(fileURI))
			if !requestAssetStringIsHTTPURL(fileURI) || !strings.HasPrefix(strings.ToLower(mimeType), "image/") {
				continue
			}
			cacheKey := fileURI + "\x00" + mimeType
			inline := cache[cacheKey]
			if inline == nil {
				var err error
				inline, err = compatibleGeminiInlineImage(ctx, fileURI, mimeType)
				if err != nil {
					return nil, err
				}
				cache[cacheKey] = inline
			}
			delete(next, key)
			next["inlineData"] = cloneBody(inline)
		}
		return next, nil
	case []any:
		next := make([]any, 0, len(typed))
		for _, item := range typed {
			prepared, err := prepareCompatibleGeminiInlineImageValue(ctx, item, cache)
			if err != nil {
				return nil, err
			}
			next = append(next, prepared)
		}
		return next, nil
	default:
		return value, nil
	}
}

func compatibleGeminiInlineImage(ctx context.Context, sourceURL string, declaredMimeType string) (map[string]any, error) {
	fetchedMimeType, payload, err := fetchRemoteMediaInputPayload(ctx, sourceURL, 256<<20)
	if err != nil {
		return nil, err
	}
	return map[string]any{
		"mimeType": geminiMediaMime(firstNonEmptyString(declaredMimeType, fetchedMimeType), "image"),
		"data":     base64.StdEncoding.EncodeToString(payload),
	}, nil
}

func uploadOfficialGeminiImageFile(ctx context.Context, client *http.Client, candidate store.RuntimeModelCandidate, apiKey string, sourceURL string, declaredMimeType string) (map[string]any, error) {
	fetchedMimeType, payload, err := fetchRemoteMediaInputPayload(ctx, sourceURL, 256<<20)
	if err != nil {
		return nil, err
	}
	mimeType := geminiMediaMime(firstNonEmptyString(declaredMimeType, fetchedMimeType), "image")
	base := strings.TrimRight(strings.TrimSpace(candidate.BaseURL), "/")
	if base == "" {
		base = "https://generativelanguage.googleapis.com"
	}
	parsed, err := url.Parse(base)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return nil, &ClientError{Code: "invalid_parameter", Message: "invalid official Gemini base URL", Retryable: false}
	}
	uploadStartURL := parsed.Scheme + "://" + parsed.Host + "/upload/v1beta/files"
	metadata, _ := json.Marshal(map[string]any{"file": map[string]any{"display_name": fmt.Sprintf("easyai-image-%d", time.Now().UnixMilli())}})
	startRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadStartURL, bytes.NewReader(metadata))
	if err != nil {
		return nil, err
	}
	startRequest.Header.Set("x-goog-api-key", apiKey)
	startRequest.Header.Set("X-Goog-Upload-Protocol", "resumable")
	startRequest.Header.Set("X-Goog-Upload-Command", "start")
	startRequest.Header.Set("X-Goog-Upload-Header-Content-Length", fmt.Sprintf("%d", len(payload)))
	startRequest.Header.Set("X-Goog-Upload-Header-Content-Type", mimeType)
	startRequest.Header.Set("Content-Type", "application/json")
	startResponse, err := client.Do(startRequest)
	if err != nil {
		return nil, transportClientError(err)
	}
	if startResponse.StatusCode < http.StatusOK || startResponse.StatusCode >= http.StatusMultipleChoices {
		_, responseErr := decodeHTTPResponse(startResponse)
		return nil, responseErr
	}
	uploadURL := strings.TrimSpace(startResponse.Header.Get("X-Goog-Upload-URL"))
	_ = startResponse.Body.Close()
	if uploadURL == "" {
		return nil, &ClientError{Code: "invalid_response", Message: "Gemini Files API did not return an upload URL", Retryable: false}
	}
	uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	uploadRequest.Header.Set("X-Goog-Upload-Offset", "0")
	uploadRequest.Header.Set("X-Goog-Upload-Command", "upload, finalize")
	uploadRequest.Header.Set("Content-Type", mimeType)
	uploadResponse, err := client.Do(uploadRequest)
	if err != nil {
		return nil, transportClientError(err)
	}
	result, err := decodeHTTPResponse(uploadResponse)
	if err != nil {
		return nil, err
	}
	file := mapFromAny(result["file"])
	fileURI := firstNonEmptyString(file["uri"], result["uri"])
	if fileURI == "" {
		return nil, &ClientError{Code: "invalid_response", Message: "Gemini Files API response did not include file.uri", Retryable: false}
	}
	return map[string]any{"fileUri": fileURI, "mimeType": firstNonEmptyString(file["mimeType"], file["mime_type"], mimeType)}, nil
}

func geminiOfficialFileURI(value string) bool {
	parsed, err := url.Parse(strings.TrimSpace(value))
	return err == nil && strings.EqualFold(parsed.Hostname(), "generativelanguage.googleapis.com") && strings.Contains(strings.ToLower(parsed.Path), "/files/")
}

func geminiURL(baseURL string, model string, apiKey string) string {
	return geminiActionURL(baseURL, model, "generateContent", apiKey, false)
}

func geminiStreamURL(baseURL string, model string, apiKey string) string {
	return geminiActionURL(baseURL, model, "streamGenerateContent", apiKey, true)
}

func geminiActionURL(baseURL string, model string, action string, apiKey string, stream bool) string {
	base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
	if base == "" {
		base = "https://generativelanguage.googleapis.com"
	}
	base = strings.TrimSuffix(base, "/openai")
	if !strings.HasSuffix(base, "/v1") &&
		!strings.HasSuffix(base, "/v1beta") &&
		!strings.HasSuffix(base, "/v1alpha") {
		base += "/v1beta"
	}
	escapedModel := url.PathEscape(model)
	endpoint := fmt.Sprintf("%s/models/%s:%s?key=%s", base, escapedModel, action, url.QueryEscape(apiKey))
	if stream {
		endpoint += "&alt=sse"
	}
	return endpoint
}

func decodeGeminiStreamResponse(resp *http.Response, onDelta StreamDelta, wire *WireResponse) (map[string]any, error) {
	defer resp.Body.Close()
	scanner := bufio.NewScanner(resp.Body)
	scanner.Buffer(make([]byte, 64*1024), 16*1024*1024)
	result := map[string]any{}
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if data == "" || data == "[DONE]" {
			continue
		}
		var event map[string]any
		if err := json.Unmarshal([]byte(data), &event); err != nil {
			return nil, &ClientError{Code: "invalid_response", Message: err.Error(), StatusCode: resp.StatusCode, Retryable: false, Wire: wire}
		}
		result = event
		if onDelta != nil {
			if err := onDelta(StreamDeltaEvent{
				Text:           geminiText(event),
				Event:          event,
				WireProtocol:   ProtocolGeminiGenerateContent,
				WireStatusCode: resp.StatusCode,
				WireHeaders:    compatibleResponseHeaders(resp.Header),
			}); err != nil {
				return nil, err
			}
		}
	}
	if err := scanner.Err(); err != nil {
		clientErr := transportClientError(err)
		clientErr.Wire = wire
		return nil, clientErr
	}
	if len(result) == 0 {
		return nil, &ClientError{Code: "invalid_response", Message: "gemini stream returned no events", StatusCode: resp.StatusCode, Retryable: false, Wire: wire}
	}
	return result, nil
}

func geminiBody(request Request) map[string]any {
	if geminiImageModelType(request.ModelType) {
		return geminiImageBody(request)
	}
	if contents, ok := request.Body["contents"]; ok {
		return geminiApplyRequestOptions(map[string]any{"contents": contents}, request, false)
	}
	prompt := firstNonEmptyPrompt(request.Body, "")
	if prompt != "" {
		return geminiApplyRequestOptions(map[string]any{
			"contents": []any{map[string]any{
				"role":  "user",
				"parts": []any{map[string]any{"text": prompt}},
			}},
		}, request, false)
	}
	body := map[string]any{"contents": geminiContentsFromMessages(request.Body)}
	if tools := geminiToolsFromOpenAITools(request.Body["tools"]); len(tools) > 0 {
		body["tools"] = tools
	}
	contents, _ := body["contents"].([]any)
	if len(contents) > 0 {
		return geminiApplyRequestOptions(body, request, false)
	}
	return geminiApplyRequestOptions(map[string]any{"contents": []any{map[string]any{
		"role":  "user",
		"parts": []any{map[string]any{"text": textFromMessages(request.Body)}},
	}}}, request, false)
}

func geminiImageBody(request Request) map[string]any {
	body := map[string]any{}
	if contents, ok := request.Body["contents"]; ok {
		body["contents"] = contents
	} else {
		parts := make([]any, 0)
		for _, image := range geminiImageInputValues(request.Body) {
			if media := geminiMediaURLPart(image.URI, image.MimeType, "image"); media != nil {
				parts = append(parts, media)
			}
		}
		if prompt := firstNonEmptyPrompt(request.Body, ""); prompt != "" {
			parts = append(parts, map[string]any{"text": prompt})
		}
		body["contents"] = []any{map[string]any{
			"role":  "user",
			"parts": parts,
		}}
	}
	return geminiApplyRequestOptions(body, request, true)
}

type geminiImageInputValue struct {
	URI      string
	MimeType string
}

func geminiImageInputValues(body map[string]any) []geminiImageInputValue {
	values := make([]geminiImageInputValue, 0)
	for _, key := range []string{"image", "images", "image_url", "imageUrl", "image_urls", "imageUrls"} {
		values = append(values, geminiImageInputValuesFromAny(body[key])...)
	}
	return values
}

func geminiImageInputValuesFromAny(value any) []geminiImageInputValue {
	switch typed := value.(type) {
	case string:
		if uri := strings.TrimSpace(typed); uri != "" {
			return []geminiImageInputValue{{URI: uri}}
		}
	case []any:
		out := make([]geminiImageInputValue, 0, len(typed))
		for _, item := range typed {
			out = append(out, geminiImageInputValuesFromAny(item)...)
		}
		return out
	case []string:
		out := make([]geminiImageInputValue, 0, len(typed))
		for _, item := range typed {
			if uri := strings.TrimSpace(item); uri != "" {
				out = append(out, geminiImageInputValue{URI: uri})
			}
		}
		return out
	case map[string]any:
		if uri := firstNonEmptyString(typed["url"], typed["fileUri"], typed["file_uri"]); uri != "" {
			return []geminiImageInputValue{{
				URI:      uri,
				MimeType: firstNonEmptyString(typed["mime_type"], typed["mimeType"]),
			}}
		}
	}
	return nil
}

func geminiApplyRequestOptions(body map[string]any, request Request, imageResponse bool) map[string]any {
	if generationConfig := geminiGenerationConfig(request.Body, imageResponse); len(generationConfig) > 0 {
		body["generationConfig"] = generationConfig
	}
	for _, item := range []struct {
		from []string
		to   string
	}{
		{from: []string{"systemInstruction", "system_instruction"}, to: "systemInstruction"},
		{from: []string{"safetySettings", "safety_settings"}, to: "safetySettings"},
		{from: []string{"toolConfig", "tool_config"}, to: "toolConfig"},
		{from: []string{"cachedContent", "cached_content"}, to: "cachedContent"},
	} {
		for _, key := range item.from {
			if value, ok := request.Body[key]; ok {
				body[item.to] = value
				break
			}
		}
	}
	if _, exists := body["tools"]; !exists {
		if tools, ok := request.Body["tools"]; ok {
			body["tools"] = tools
		}
	}
	return body
}

func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]any {
	source := mapFromAny(firstPresent(body["generationConfig"], body["generation_config"]))
	out := cloneMapAny(source)
	if out == nil {
		out = map[string]any{}
	}
	imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"])))
	if imageConfig != nil {
		aspectRatio := normalizedProviderAspectRatio(firstNonEmptyString(imageConfig["aspectRatio"], imageConfig["aspect_ratio"]))
		imageSize := normalizedProviderImageResolution(firstNonEmptyString(imageConfig["imageSize"], imageConfig["image_size"]))
		delete(imageConfig, "aspectRatio")
		delete(imageConfig, "aspect_ratio")
		delete(imageConfig, "imageSize")
		delete(imageConfig, "image_size")
		if aspectRatio != "" {
			imageConfig["aspectRatio"] = aspectRatio
		}
		if imageSize != "" {
			imageConfig["imageSize"] = imageSize
		}
	}
	if aspectRatio := normalizedProviderAspectRatio(firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"], body["ratio"])); aspectRatio != "" {
		if imageConfig == nil {
			imageConfig = map[string]any{}
		}
		imageConfig["aspectRatio"] = aspectRatio
	}
	if imageSize := normalizedProviderImageResolution(firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"])); imageSize != "" {
		if imageConfig == nil {
			imageConfig = map[string]any{}
		}
		imageConfig["imageSize"] = imageSize
	}
	if len(imageConfig) > 0 {
		out["imageConfig"] = imageConfig
	} else {
		delete(out, "imageConfig")
	}
	delete(out, "image_config")
	if count := intFromAny(firstPresent(body["n"], body["candidateCount"], body["candidate_count"])); count > 0 {
		out["candidateCount"] = count
	}
	if imageResponse {
		out["responseModalities"] = ensureGeminiResponseModalityImage(out["responseModalities"])
	}
	return out
}

func ensureGeminiResponseModalityImage(value any) []any {
	items := []any{}
	switch typed := value.(type) {
	case []any:
		items = append(items, typed...)
	case []string:
		for _, item := range typed {
			items = append(items, item)
		}
	case string:
		if strings.TrimSpace(typed) != "" {
			items = append(items, typed)
		}
	}
	for _, item := range items {
		if strings.EqualFold(strings.TrimSpace(stringFromAny(item)), "IMAGE") {
			return items
		}
	}
	return append(items, "IMAGE")
}

func geminiImageModelType(modelType string) bool {
	return modelType == "image_generate" || modelType == "image_edit"
}

func geminiContentsFromMessages(body map[string]any) []any {
	normalized := NormalizeChatCompletionRequestBody(body)
	messages, _ := normalized["messages"].([]any)
	contents := make([]any, 0, len(messages))
	toolNames := map[string]string{}
	for _, rawMessage := range messages {
		message, _ := rawMessage.(map[string]any)
		if len(message) == 0 {
			continue
		}
		role := stringFromAny(message["role"])
		if role == "tool" {
			toolCallID := stringFromAny(message["tool_call_id"])
			name := toolNames[toolCallID]
			if name == "" {
				name = toolCallID
			}
			if name == "" {
				name = "tool"
			}
			contents = append(contents, map[string]any{
				"role": "user",
				"parts": []any{map[string]any{"functionResponse": map[string]any{
					"name":     name,
					"response": geminiFunctionResponsePayload(message["content"]),
				}}},
			})
			continue
		}
		parts := geminiContentParts(message["content"])
		if role == "assistant" {
			for _, rawToolCall := range toolCallsSlice(message["tool_calls"]) {
				toolCall, _ := rawToolCall.(map[string]any)
				function, _ := toolCall["function"].(map[string]any)
				name := stringFromAny(function["name"])
				if name == "" {
					continue
				}
				if id := stringFromAny(toolCall["id"]); id != "" {
					toolNames[id] = name
				}
				parts = append(parts, map[string]any{"functionCall": map[string]any{
					"name": name,
					"args": geminiFunctionArgs(function["arguments"]),
				}})
			}
		}
		if len(parts) == 0 {
			continue
		}
		contents = append(contents, map[string]any{
			"role":  geminiRole(role),
			"parts": parts,
		})
	}
	return contents
}

func geminiRole(role string) string {
	if role == "assistant" {
		return "model"
	}
	return "user"
}

func geminiContentParts(content any) []any {
	parts := make([]any, 0)
	switch typed := content.(type) {
	case string:
		if strings.TrimSpace(typed) != "" {
			parts = append(parts, map[string]any{"text": typed})
		}
	case []any:
		for _, rawPart := range typed {
			part, _ := rawPart.(map[string]any)
			if len(part) == 0 {
				continue
			}
			switch stringFromAny(part["type"]) {
			case "text":
				if text := strings.TrimSpace(stringFromAny(firstPresent(part["text"], part["content"]))); text != "" {
					parts = append(parts, map[string]any{"text": text})
				}
			case "image_url":
				if media := geminiMediaPart(part, "image_url", "image"); media != nil {
					parts = append(parts, media)
				}
			case "video_url":
				if media := geminiMediaPart(part, "video_url", "video"); media != nil {
					parts = append(parts, media)
				}
			case "audio_url":
				if media := geminiMediaPart(part, "audio_url", "audio"); media != nil {
					parts = append(parts, media)
				}
			case "input_audio":
				if media := geminiInputAudioPart(part); media != nil {
					parts = append(parts, media)
				}
			default:
				if text := strings.TrimSpace(stringFromAny(firstPresent(part["text"], part["content"]))); text != "" {
					parts = append(parts, map[string]any{"text": text})
				}
			}
		}
	}
	return parts
}

func geminiMediaPart(part map[string]any, key string, mediaType string) map[string]any {
	nested := mapFromAny(part[key])
	uri := firstNonEmptyString(nested["url"], part["url"], part[key])
	if uri == "" {
		return nil
	}
	mimeType := firstNonEmptyString(nested["mime_type"], nested["mimeType"], part["mime_type"], part["mimeType"])
	return geminiMediaURLPart(uri, mimeType, mediaType)
}

func geminiInputAudioPart(part map[string]any) map[string]any {
	audio := mapFromAny(part["input_audio"])
	uri := firstNonEmptyString(audio["data"], audio["url"])
	if uri == "" {
		return nil
	}
	mimeType := firstNonEmptyString(audio["mime_type"], audio["mimeType"])
	if mimeType == "" {
		format := strings.ToLower(strings.TrimPrefix(stringFromAny(audio["format"]), "."))
		if strings.Contains(format, "/") {
			mimeType = format
		} else if format == "mp3" {
			mimeType = "audio/mpeg"
		} else if format != "" {
			mimeType = "audio/" + format
		}
	}
	return geminiMediaURLPart(uri, mimeType, "audio")
}

func geminiMediaURLPart(uri string, explicitMimeType string, mediaType string) map[string]any {
	if parsed := geminiDataURL(uri); parsed != nil {
		return map[string]any{"inlineData": map[string]any{
			"mimeType": geminiMediaMime(firstNonEmptyString(explicitMimeType, parsed.mimeType), mediaType),
			"data":     parsed.data,
		}}
	}
	return map[string]any{"fileData": map[string]any{
		"fileUri":  uri,
		"mimeType": geminiMediaMime(firstNonEmptyString(explicitMimeType, mimeFromURI(uri)), mediaType),
	}}
}

type geminiParsedDataURL struct {
	mimeType string
	data     string
}

func geminiDataURL(value string) *geminiParsedDataURL {
	if !strings.HasPrefix(value, "data:") {
		return nil
	}
	prefix, data, ok := strings.Cut(value, ",")
	if !ok || !strings.Contains(prefix, ";base64") {
		return nil
	}
	mimeType := strings.TrimPrefix(strings.Split(prefix, ";")[0], "data:")
	if mimeType == "" {
		mimeType = "application/octet-stream"
	}
	return &geminiParsedDataURL{mimeType: mimeType, data: data}
}

func mimeFromURI(value string) string {
	pathValue := value
	if parsed, err := url.Parse(value); err == nil && parsed.Path != "" {
		pathValue = parsed.Path
	}
	extension := strings.ToLower(path.Ext(pathValue))
	if extension == "" {
		return ""
	}
	return mime.TypeByExtension(extension)
}

func geminiMediaMime(mimeType string, mediaType string) string {
	normalized := strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0]))
	switch mediaType {
	case "image":
		if strings.HasPrefix(normalized, "image/") && normalized != "image/svg+xml" {
			return normalized
		}
		return "image/png"
	case "video":
		switch normalized {
		case "video/x-msvideo":
			return "video/avi"
		case "video/quicktime", "video/mpeg", "video/mp4", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp":
			return normalized
		default:
			return "video/mp4"
		}
	case "audio":
		switch normalized {
		case "audio/x-wav", "audio/wave":
			return "audio/wav"
		case "audio/mpeg", "audio/mp3", "audio/wav", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mp4", "audio/webm":
			return normalized
		default:
			return "audio/mpeg"
		}
	default:
		return "application/octet-stream"
	}
}

func toolCallsSlice(value any) []any {
	switch typed := value.(type) {
	case []any:
		return typed
	case map[string]any:
		return []any{typed}
	default:
		return nil
	}
}

func geminiFunctionArgs(value any) map[string]any {
	if value == nil {
		return map[string]any{}
	}
	if args, ok := value.(map[string]any); ok {
		return args
	}
	if text, ok := value.(string); ok {
		if strings.TrimSpace(text) == "" {
			return map[string]any{}
		}
		var args map[string]any
		if err := json.Unmarshal([]byte(text), &args); err == nil {
			return args
		}
		return map[string]any{"arguments": text}
	}
	return map[string]any{"arguments": value}
}

func geminiFunctionResponsePayload(value any) map[string]any {
	if payload, ok := value.(map[string]any); ok {
		return payload
	}
	if text, ok := value.(string); ok {
		var payload map[string]any
		if err := json.Unmarshal([]byte(text), &payload); err == nil {
			return payload
		}
		return map[string]any{"content": text}
	}
	if value == nil {
		return map[string]any{}
	}
	return map[string]any{"content": value}
}

func geminiToolsFromOpenAITools(value any) []any {
	tools, ok := value.([]any)
	if !ok || len(tools) == 0 {
		return nil
	}
	declarations := make([]any, 0, len(tools))
	for _, rawTool := range tools {
		tool, _ := rawTool.(map[string]any)
		function, _ := tool["function"].(map[string]any)
		name := stringFromAny(function["name"])
		if name == "" {
			continue
		}
		declaration := map[string]any{"name": name}
		if description := stringFromAny(function["description"]); description != "" {
			declaration["description"] = description
		}
		if parameters, ok := function["parameters"]; ok {
			declaration["parametersJsonSchema"] = parameters
		}
		declarations = append(declarations, declaration)
	}
	if len(declarations) == 0 {
		return nil
	}
	return []any{map[string]any{"functionDeclarations": declarations}}
}

func geminiResult(request Request, raw map[string]any) (map[string]any, error) {
	if geminiImageModelType(request.ModelType) {
		data := geminiImageData(raw)
		if len(data) == 0 {
			details := geminiMissingImageDetails(raw)
			return nil, &ClientError{
				Code:      "gemini_image_output_missing",
				Message:   geminiMissingImageMessage(details),
				Details:   details,
				Retryable: false,
			}
		}
		return map[string]any{
			"id":      "gemini-image",
			"created": nowUnix(),
			"model":   request.Model,
			"data":    data,
		}, nil
	}
	message, finishReason := geminiChatMessage(raw)
	return map[string]any{
		"id":      "gemini-chat",
		"object":  "chat.completion",
		"created": nowUnix(),
		"model":   request.Model,
		"choices": []any{map[string]any{
			"index":         0,
			"finish_reason": finishReason,
			"message":       message,
		}},
		"usage": geminiUsageMap(raw),
	}, nil
}

func textFromMessages(body map[string]any) string {
	messages, _ := body["messages"].([]any)
	parts := make([]string, 0, len(messages))
	for _, message := range messages {
		item, _ := message.(map[string]any)
		content := item["content"]
		switch typed := content.(type) {
		case string:
			parts = append(parts, typed)
		case []any:
			for _, part := range typed {
				partMap, _ := part.(map[string]any)
				if text, ok := partMap["text"].(string); ok {
					parts = append(parts, text)
				}
			}
		}
	}
	return strings.TrimSpace(strings.Join(parts, "\n"))
}

func geminiText(raw map[string]any) string {
	message, _ := geminiChatMessage(raw)
	content, _ := message["content"].(string)
	return content
}

func geminiChatMessage(raw map[string]any) (map[string]any, string) {
	candidates, _ := raw["candidates"].([]any)
	for _, candidate := range candidates {
		candidateMap, _ := candidate.(map[string]any)
		content, _ := candidateMap["content"].(map[string]any)
		parts, _ := content["parts"].([]any)
		textParts := make([]string, 0, len(parts))
		toolCalls := make([]any, 0)
		for _, part := range parts {
			partMap, _ := part.(map[string]any)
			if text, ok := partMap["text"].(string); ok && text != "" {
				textParts = append(textParts, text)
			}
			functionCall := mapFromAny(firstPresent(partMap["functionCall"], partMap["function_call"]))
			if len(functionCall) == 0 {
				continue
			}
			if toolCall := normalizeGeminiFunctionCall(functionCall, len(toolCalls), false); toolCall != nil {
				toolCalls = append(toolCalls, toolCall)
			}
		}
		message := map[string]any{
			"role":    "assistant",
			"content": strings.Join(textParts, ""),
		}
		if len(toolCalls) > 0 {
			message["tool_calls"] = toolCalls
			if len(textParts) == 0 {
				message["content"] = nil
			}
		}
		return message, geminiFinishReason(candidateMap, len(toolCalls) > 0)
	}
	return map[string]any{"role": "assistant", "content": ""}, "stop"
}

func geminiFinishReason(candidate map[string]any, hasToolCalls bool) string {
	if hasToolCalls {
		return "tool_calls"
	}
	switch strings.ToUpper(stringFromAny(candidate["finishReason"])) {
	case "MAX_TOKENS":
		return "length"
	case "SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII":
		return "content_filter"
	default:
		return "stop"
	}
}

func geminiImageData(raw map[string]any) []any {
	candidates, _ := raw["candidates"].([]any)
	out := []any{}
	for _, candidate := range candidates {
		candidateMap, _ := candidate.(map[string]any)
		content, _ := candidateMap["content"].(map[string]any)
		parts, _ := content["parts"].([]any)
		for _, part := range parts {
			partMap, _ := part.(map[string]any)
			inline, _ := partMap["inlineData"].(map[string]any)
			if inline == nil {
				inline, _ = partMap["inline_data"].(map[string]any)
			}
			if data, ok := inline["data"].(string); ok && data != "" {
				out = append(out, map[string]any{
					"b64_json":  data,
					"mime_type": firstNonEmptyString(inline["mimeType"], inline["mime_type"]),
				})
				continue
			}
			fileData, _ := partMap["fileData"].(map[string]any)
			if fileData == nil {
				fileData, _ = partMap["file_data"].(map[string]any)
			}
			if uri := firstNonEmptyString(fileData["fileUri"], fileData["file_uri"], fileData["uri"]); uri != "" {
				out = append(out, map[string]any{
					"url":       uri,
					"mime_type": firstNonEmptyString(fileData["mimeType"], fileData["mime_type"], mimeFromURI(uri)),
				})
			}
		}
	}
	return out
}

func geminiMissingImageDetails(raw map[string]any) map[string]any {
	details := map[string]any{
		"provider":     "gemini",
		"resourceType": "image",
	}
	upstreamError, _ := raw["error"].(map[string]any)
	upstreamErrorCode := stringFromPathValue(upstreamError["code"])
	upstreamErrorStatus := strings.TrimSpace(stringFromAny(upstreamError["status"]))
	upstreamErrorMessage := compactGeminiDiagnosticText(stringFromAny(upstreamError["message"]), 320)
	if upstreamErrorCode != "" {
		details["upstreamErrorCode"] = upstreamErrorCode
	}
	if upstreamErrorStatus != "" {
		details["upstreamErrorStatus"] = upstreamErrorStatus
	}
	if upstreamErrorMessage != "" {
		details["upstreamErrorMessage"] = upstreamErrorMessage
	}
	promptFeedback, _ := raw["promptFeedback"].(map[string]any)
	if promptFeedback == nil {
		promptFeedback, _ = raw["prompt_feedback"].(map[string]any)
	}
	promptBlockReason := firstNonEmptyString(promptFeedback["blockReason"], promptFeedback["block_reason"])
	promptBlockMessage := compactGeminiDiagnosticText(firstNonEmptyString(
		promptFeedback["blockReasonMessage"],
		promptFeedback["block_reason_message"],
		promptFeedback["message"],
	), 320)
	if promptBlockReason != "" {
		details["promptBlockReason"] = promptBlockReason
	}
	if promptBlockMessage != "" {
		details["promptBlockMessage"] = promptBlockMessage
	}

	candidates, _ := raw["candidates"].([]any)
	details["candidateCount"] = len(candidates)
	finishReasons := make([]string, 0)
	safetyCategories := make([]string, 0)
	partKinds := make([]string, 0)
	providerMessages := make([]string, 0)
	partCount := 0
	emptyImagePayload := false
	for _, rawCandidate := range candidates {
		candidate, _ := rawCandidate.(map[string]any)
		if finishReason := strings.TrimSpace(stringFromAny(firstPresent(candidate["finishReason"], candidate["finish_reason"]))); finishReason != "" {
			finishReasons = appendGeminiDiagnosticValue(finishReasons, finishReason)
		}
		safetyRatings, _ := candidate["safetyRatings"].([]any)
		if safetyRatings == nil {
			safetyRatings, _ = candidate["safety_ratings"].([]any)
		}
		for _, rawRating := range safetyRatings {
			rating, _ := rawRating.(map[string]any)
			blocked, _ := firstPresent(rating["blocked"], rating["isBlocked"], rating["is_blocked"]).(bool)
			if !blocked {
				continue
			}
			if category := strings.TrimSpace(stringFromAny(rating["category"])); category != "" {
				safetyCategories = appendGeminiDiagnosticValue(safetyCategories, category)
			}
		}
		content, _ := candidate["content"].(map[string]any)
		parts, _ := content["parts"].([]any)
		partCount += len(parts)
		for _, rawPart := range parts {
			part, _ := rawPart.(map[string]any)
			if text := compactGeminiDiagnosticText(stringFromAny(part["text"]), 320); text != "" {
				partKinds = appendGeminiDiagnosticValue(partKinds, "text")
				providerMessages = appendGeminiDiagnosticValue(providerMessages, text)
			}
			inline, inlinePresent := part["inlineData"].(map[string]any)
			if !inlinePresent {
				inline, inlinePresent = part["inline_data"].(map[string]any)
			}
			if inlinePresent {
				partKinds = appendGeminiDiagnosticValue(partKinds, "inlineData")
				if strings.TrimSpace(stringFromAny(inline["data"])) == "" {
					emptyImagePayload = true
				}
			}
			fileData, filePresent := part["fileData"].(map[string]any)
			if !filePresent {
				fileData, filePresent = part["file_data"].(map[string]any)
			}
			if filePresent {
				partKinds = appendGeminiDiagnosticValue(partKinds, "fileData")
				if firstNonEmptyString(fileData["fileUri"], fileData["file_uri"], fileData["uri"]) == "" {
					emptyImagePayload = true
				}
			}
			for _, item := range []struct {
				key  string
				kind string
			}{
				{key: "thought", kind: "thought"},
				{key: "functionCall", kind: "functionCall"},
				{key: "function_call", kind: "functionCall"},
				{key: "executableCode", kind: "executableCode"},
				{key: "executable_code", kind: "executableCode"},
				{key: "codeExecutionResult", kind: "codeExecutionResult"},
				{key: "code_execution_result", kind: "codeExecutionResult"},
			} {
				if _, ok := part[item.key]; ok {
					partKinds = appendGeminiDiagnosticValue(partKinds, item.kind)
				}
			}
		}
	}
	details["partCount"] = partCount
	if len(finishReasons) > 0 {
		details["candidateFinishReasons"] = finishReasons
	}
	if len(safetyCategories) > 0 {
		details["blockedSafetyCategories"] = safetyCategories
	}
	if len(partKinds) > 0 {
		details["observedPartKinds"] = partKinds
	}
	if len(providerMessages) > 0 {
		details["providerMessages"] = providerMessages
	}

	reason := "no_supported_image_parts"
	switch {
	case upstreamErrorCode != "" || upstreamErrorStatus != "" || upstreamErrorMessage != "":
		reason = "upstream_error"
	case promptBlockReason != "":
		reason = "prompt_blocked"
	case len(safetyCategories) > 0 || geminiFinishReasonsIndicateFiltering(finishReasons):
		reason = "candidate_filtered"
	case len(candidates) == 0:
		reason = "no_candidates"
	case emptyImagePayload:
		reason = "empty_image_payload"
	case partCount == 0:
		reason = "no_content_parts"
	}
	details["reason"] = reason
	return details
}

func geminiMissingImageMessage(details map[string]any) string {
	message := "Gemini image response contains no extractable image"
	switch stringFromAny(details["reason"]) {
	case "upstream_error":
		message += ": upstream returned an error envelope"
	case "prompt_blocked":
		message += ": prompt blocked"
	case "candidate_filtered":
		message += ": candidate filtered"
	case "no_candidates":
		message += ": upstream response contains no candidates"
	case "empty_image_payload":
		message += ": image part is missing inline data or file URI"
	case "no_content_parts":
		message += ": candidate content contains no parts"
	default:
		message += ": candidate parts contain no supported image resource"
	}
	if reason := stringFromAny(details["promptBlockReason"]); reason != "" {
		message += " (blockReason=" + reason + ")"
	}
	if code := stringFromAny(details["upstreamErrorCode"]); code != "" {
		message += "; upstreamErrorCode=" + code
	}
	if status := stringFromAny(details["upstreamErrorStatus"]); status != "" {
		message += "; upstreamErrorStatus=" + status
	}
	if values := geminiDiagnosticStrings(details["candidateFinishReasons"]); len(values) > 0 {
		message += "; finishReason=" + strings.Join(values, ",")
	}
	if values := geminiDiagnosticStrings(details["blockedSafetyCategories"]); len(values) > 0 {
		message += "; blockedSafetyCategories=" + strings.Join(values, ",")
	}
	if values := geminiDiagnosticStrings(details["providerMessages"]); len(values) > 0 {
		message += "; providerMessage=" + values[0]
	} else if upstreamMessage := stringFromAny(details["upstreamErrorMessage"]); upstreamMessage != "" {
		message += "; providerMessage=" + upstreamMessage
	} else if blockMessage := stringFromAny(details["promptBlockMessage"]); blockMessage != "" {
		message += "; providerMessage=" + blockMessage
	}
	if values := geminiDiagnosticStrings(details["observedPartKinds"]); len(values) > 0 {
		message += "; observedPartKinds=" + strings.Join(values, ",")
	}
	return compactGeminiDiagnosticText(message, 1800)
}

func geminiFinishReasonsIndicateFiltering(values []string) bool {
	for _, value := range values {
		normalized := strings.ToUpper(strings.TrimSpace(value))
		switch normalized {
		case "SAFETY", "IMAGE_SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII":
			return true
		}
	}
	return false
}

func geminiDiagnosticStrings(value any) []string {
	switch typed := value.(type) {
	case []string:
		return typed
	case []any:
		values := make([]string, 0, len(typed))
		for _, item := range typed {
			if text := strings.TrimSpace(stringFromAny(item)); text != "" {
				values = append(values, text)
			}
		}
		return values
	default:
		return nil
	}
}

func appendGeminiDiagnosticValue(values []string, value string) []string {
	value = strings.TrimSpace(value)
	if value == "" {
		return values
	}
	for _, existing := range values {
		if existing == value {
			return values
		}
	}
	if len(values) >= 8 {
		return values
	}
	return append(values, value)
}

func compactGeminiDiagnosticText(value string, maxRunes int) string {
	value = strings.Join(strings.Fields(value), " ")
	if maxRunes <= 0 {
		return ""
	}
	runes := []rune(value)
	if len(runes) <= maxRunes {
		return value
	}
	return string(runes[:maxRunes]) + "…"
}

func geminiUsage(raw map[string]any) Usage {
	usageMap := geminiUsageMap(raw)
	input := intFromAny(usageMap["prompt_tokens"])
	output := intFromAny(usageMap["completion_tokens"])
	total := intFromAny(usageMap["total_tokens"])
	cachedInput, cachedInputKnown := cachedInputTokensValueFromOpenAIUsage(usageMap)
	if cachedInput > input && input > 0 {
		cachedInput = input
	}
	return Usage{InputTokens: input, OutputTokens: output, CachedInputTokens: cachedInput, CachedInputTokensKnown: cachedInputKnown, TotalTokens: total}
}

func geminiUsageMap(raw map[string]any) map[string]any {
	meta, _ := raw["usageMetadata"].(map[string]any)
	input := intFromAny(meta["promptTokenCount"])
	output := intFromAny(meta["candidatesTokenCount"])
	total := intFromAny(meta["totalTokenCount"])
	cachedInput := intFromAny(firstPresent(meta["cachedContentTokenCount"], meta["cached_content_token_count"]))
	if total == 0 {
		total = input + output
	}
	usage := map[string]any{"prompt_tokens": input, "completion_tokens": output, "total_tokens": total}
	if cachedInput > 0 {
		usage["prompt_tokens_details"] = map[string]any{"cached_tokens": cachedInput}
	}
	return usage
}
