// Package gemini provides request translation functionality for Gemini to Claude Code API compatibility.
// It handles parsing and transforming Gemini API requests into Claude Code API format,
// extracting model information, system instructions, message contents, and tool declarations.
// The package performs JSON data transformation to ensure compatibility
// between Gemini API format and Claude Code API's expected format.
package gemini

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math/big"
	"strings"

	"github.com/google/uuid"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
	"github.com/tidwall/gjson"
	"github.com/tidwall/sjson"
)

var (
	user    = ""
	account = ""
	session = ""
)

// ConvertGeminiRequestToClaude parses and transforms a Gemini API request into Claude Code API format.
// It extracts the model name, system instruction, message contents, and tool declarations
// from the raw JSON request and returns them in the format expected by the Claude Code API.
// The function performs comprehensive transformation including:
// 1. Model name mapping and generation configuration extraction
// 2. System instruction conversion to Claude Code format
// 3. Message content conversion with proper role mapping
// 4. Tool call and tool result handling with FIFO queue for ID matching
// 5. Image and file data conversion to Claude Code base64 format
// 6. Tool declaration and tool choice configuration mapping
//
// Parameters:
//   - modelName: The name of the model to use for the request
//   - rawJSON: The raw JSON request data from the Gemini API
//   - stream: A boolean indicating if the request is for a streaming response
//
// Returns:
//   - []byte: The transformed request data in Claude Code API format
func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
	rawJSON := inputRawJSON

	if account == "" {
		u, _ := uuid.NewRandom()
		account = u.String()
	}
	if session == "" {
		u, _ := uuid.NewRandom()
		session = u.String()
	}
	if user == "" {
		sum := sha256.Sum256([]byte(account + session))
		user = hex.EncodeToString(sum[:])
	}
	userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)

	// Base Claude message payload
	out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID))

	root := gjson.ParseBytes(rawJSON)

	// Helper for generating tool call IDs in the form: toolu_<alphanum>
	// This ensures unique identifiers for tool calls in the Claude Code format
	genToolCallID := func() string {
		const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
		var b strings.Builder
		// 24 chars random suffix for uniqueness
		for i := 0; i < 24; i++ {
			n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
			b.WriteByte(letters[n.Int64()])
		}
		return "toolu_" + b.String()
	}

	// FIFO queue to store tool call IDs for matching with tool results
	// Gemini uses sequential pairing across possibly multiple in-flight
	// functionCalls, so we keep a FIFO queue of generated tool IDs and
	// consume them in order when functionResponses arrive.
	var pendingToolIDs []string

	// Model mapping to specify which Claude Code model to use
	out, _ = sjson.SetBytes(out, "model", modelName)

	// Generation config extraction from Gemini format
	if genConfig := root.Get("generationConfig"); genConfig.Exists() {
		// Max output tokens configuration
		if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
			out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int())
		}
		// Temperature setting for controlling response randomness
		if temp := genConfig.Get("temperature"); temp.Exists() {
			out, _ = sjson.SetBytes(out, "temperature", temp.Float())
		} else if topP := genConfig.Get("topP"); topP.Exists() {
			// Top P setting for nucleus sampling (filtered out if temperature is set)
			out, _ = sjson.SetBytes(out, "top_p", topP.Float())
		}
		// Stop sequences configuration for custom termination conditions
		if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() {
			var stopSequences []string
			stopSeqs.ForEach(func(_, value gjson.Result) bool {
				stopSequences = append(stopSequences, value.String())
				return true
			})
			if len(stopSequences) > 0 {
				out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences)
			}
		}
		// Include thoughts configuration for reasoning process visibility
		// Translator only does format conversion, ApplyThinking handles model capability validation.
		if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
			mi := registry.LookupModelInfo(modelName, "claude")
			supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0
			supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))

			// MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid
			// validation errors since validate treats same-provider unsupported levels as errors.
			thinkingLevel := thinkingConfig.Get("thinkingLevel")
			if !thinkingLevel.Exists() {
				thinkingLevel = thinkingConfig.Get("thinking_level")
			}
			if thinkingLevel.Exists() {
				level := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
				if supportsAdaptive {
					switch level {
					case "":
					case "none":
						out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
						out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
						out, _ = sjson.DeleteBytes(out, "output_config.effort")
					default:
						if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
							level = mapped
						}
						out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
						out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
						out, _ = sjson.SetBytes(out, "output_config.effort", level)
					}
				} else {
					switch level {
					case "":
					case "none":
						out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
						out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
					case "auto":
						out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
						out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
					default:
						if budget, ok := thinking.ConvertLevelToBudget(level); ok {
							out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
							out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
						}
					}
				}
			} else {
				thinkingBudget := thinkingConfig.Get("thinkingBudget")
				if !thinkingBudget.Exists() {
					thinkingBudget = thinkingConfig.Get("thinking_budget")
				}
				if thinkingBudget.Exists() {
					budget := int(thinkingBudget.Int())
					if supportsAdaptive {
						switch budget {
						case 0:
							out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
							out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
							out, _ = sjson.DeleteBytes(out, "output_config.effort")
						default:
							level, ok := thinking.ConvertBudgetToLevel(budget)
							if ok {
								if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
									level = mapped
								}
								out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
								out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
								out, _ = sjson.SetBytes(out, "output_config.effort", level)
							}
						}
					} else {
						switch budget {
						case 0:
							out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
							out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
						case -1:
							out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
							out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
						default:
							out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
							out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
						}
					}
				} else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
					out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
				} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
					out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
				}
			}
		}
	}

	// System instruction conversion to Claude Code format
	if sysInstr := root.Get("system_instruction"); sysInstr.Exists() {
		if parts := sysInstr.Get("parts"); parts.Exists() && parts.IsArray() {
			var systemText strings.Builder
			parts.ForEach(func(_, part gjson.Result) bool {
				if text := part.Get("text"); text.Exists() {
					if systemText.Len() > 0 {
						systemText.WriteString("\n")
					}
					systemText.WriteString(text.String())
				}
				return true
			})
			if systemText.Len() > 0 {
				// Create system message in Claude Code format
				systemMessage := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`)
				systemMessage, _ = sjson.SetBytes(systemMessage, "content.0.text", systemText.String())
				out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage)
			}
		}
	}

	// Contents conversion to messages with proper role mapping
	if contents := root.Get("contents"); contents.Exists() && contents.IsArray() {
		contents.ForEach(func(_, content gjson.Result) bool {
			role := content.Get("role").String()
			// Map Gemini roles to Claude Code roles
			if role == "model" {
				role = "assistant"
			}

			if role == "function" {
				role = "user"
			}

			if role == "tool" {
				role = "user"
			}

			// Create message structure in Claude Code format
			msg := []byte(`{"role":"","content":[]}`)
			msg, _ = sjson.SetBytes(msg, "role", role)

			if parts := content.Get("parts"); parts.Exists() && parts.IsArray() {
				parts.ForEach(func(_, part gjson.Result) bool {
					// Text content conversion
					if text := part.Get("text"); text.Exists() {
						textContent := []byte(`{"type":"text","text":""}`)
						textContent, _ = sjson.SetBytes(textContent, "text", text.String())
						msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent)
						return true
					}

					// Function call (from model/assistant) conversion to tool use
					if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" {
						toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)

						// Generate a unique tool ID and enqueue it for later matching
						// with the corresponding functionResponse
						toolID := genToolCallID()
						pendingToolIDs = append(pendingToolIDs, toolID)
						toolUse, _ = sjson.SetBytes(toolUse, "id", toolID)

						if name := fc.Get("name"); name.Exists() {
							toolUse, _ = sjson.SetBytes(toolUse, "name", name.String())
						}
						if args := fc.Get("args"); args.Exists() && args.IsObject() {
							toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw))
						}
						msg, _ = sjson.SetRawBytes(msg, "content.-1", toolUse)
						return true
					}

					// Function response (from user) conversion to tool result
					if fr := part.Get("functionResponse"); fr.Exists() {
						toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`)

						// Attach the oldest queued tool_id to pair the response
						// with its call. If the queue is empty, generate a new id.
						var toolID string
						if len(pendingToolIDs) > 0 {
							toolID = pendingToolIDs[0]
							// Pop the first element from the queue
							pendingToolIDs = pendingToolIDs[1:]
						} else {
							// Fallback: generate new ID if no pending tool_use found
							toolID = genToolCallID()
						}
						toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", toolID)

						// Extract result content from the function response
						if result := fr.Get("response.result"); result.Exists() {
							toolResult, _ = sjson.SetBytes(toolResult, "content", result.String())
						} else if response := fr.Get("response"); response.Exists() {
							toolResult, _ = sjson.SetBytes(toolResult, "content", response.Raw)
						}
						msg, _ = sjson.SetRawBytes(msg, "content.-1", toolResult)
						return true
					}

					// Image content (inline_data) conversion to Claude Code format
					if inlineData := part.Get("inline_data"); inlineData.Exists() {
						imageContent := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`)
						if mimeType := inlineData.Get("mime_type"); mimeType.Exists() {
							imageContent, _ = sjson.SetBytes(imageContent, "source.media_type", mimeType.String())
						}
						if data := inlineData.Get("data"); data.Exists() {
							imageContent, _ = sjson.SetBytes(imageContent, "source.data", data.String())
						}
						msg, _ = sjson.SetRawBytes(msg, "content.-1", imageContent)
						return true
					}

					// File data conversion to text content with file info
					if fileData := part.Get("file_data"); fileData.Exists() {
						// For file data, we'll convert to text content with file info
						textContent := []byte(`{"type":"text","text":""}`)
						fileInfo := "File: " + fileData.Get("file_uri").String()
						if mimeType := fileData.Get("mime_type"); mimeType.Exists() {
							fileInfo += " (Type: " + mimeType.String() + ")"
						}
						textContent, _ = sjson.SetBytes(textContent, "text", fileInfo)
						msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent)
						return true
					}

					return true
				})
			}

			// Only add message if it has content
			if contentArray := gjson.GetBytes(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 {
				out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
			}

			return true
		})
	}

	// Tools mapping: Gemini functionDeclarations -> Claude Code tools
	if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
		var anthropicTools []interface{}

		tools.ForEach(func(_, tool gjson.Result) bool {
			if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() {
				funcDecls.ForEach(func(_, funcDecl gjson.Result) bool {
					anthropicTool := []byte(`{"name":"","description":"","input_schema":{}}`)

					if name := funcDecl.Get("name"); name.Exists() {
						anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", name.String())
					}
					if desc := funcDecl.Get("description"); desc.Exists() {
						anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", desc.String())
					}
					if params := funcDecl.Get("parameters"); params.Exists() {
						// Clean up the parameters schema for Claude Code compatibility
						cleaned := []byte(params.Raw)
						cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
						cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
						anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned)
					} else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() {
						// Clean up the parameters schema for Claude Code compatibility
						cleaned := []byte(params.Raw)
						cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
						cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
						anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned)
					}

					anthropicTools = append(anthropicTools, gjson.ParseBytes(anthropicTool).Value())
					return true
				})
			}
			return true
		})

		if len(anthropicTools) > 0 {
			out, _ = sjson.SetBytes(out, "tools", anthropicTools)
		}
	}

	// Tool config mapping from Gemini format to Claude Code format
	if toolConfig := root.Get("tool_config"); toolConfig.Exists() {
		if funcCalling := toolConfig.Get("function_calling_config"); funcCalling.Exists() {
			if mode := funcCalling.Get("mode"); mode.Exists() {
				switch mode.String() {
				case "AUTO":
					out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
				case "NONE":
					out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"none"}`))
				case "ANY":
					out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
				}
			}
		}
	}

	// Stream setting configuration
	out, _ = sjson.SetBytes(out, "stream", stream)

	// Convert tool parameter types to lowercase for Claude Code compatibility
	var pathsToLower []string
	toolsResult := gjson.GetBytes(out, "tools")
	util.Walk(toolsResult, "", "type", &pathsToLower)
	for _, p := range pathsToLower {
		fullPath := fmt.Sprintf("tools.%s", p)
		out, _ = sjson.SetBytes(out, fullPath, strings.ToLower(gjson.GetBytes(out, fullPath).String()))
	}

	return out
}
