package openai

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"

	"go.uber.org/zap"
)

var geminiPathToken = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
var geminiCallerModelLabel = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$`)
var geminiToolCallID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)

const geminiRejectionLogMessage = "edge_gemini_rejection"

type geminiRejectionClass string

const (
	geminiRejectionPreIngress   geminiRejectionClass = "pre_ingress"
	geminiRejectionProviderHTTP geminiRejectionClass = "provider_http"
)

func isGeminiRequest(r *http.Request) bool {
	return r != nil && strings.HasPrefix(r.URL.Path, geminiPathPrefix)
}

func writeGeminiError(w http.ResponseWriter, status int, code, message string) {
	writeJSON(w, status, geminiErrorResponse{Error: geminiErrorBody{
		Code: status, Message: message, Status: code,
	}})
}

func (s *Server) observeGeminiRejection(class geminiRejectionClass, status int) {
	s.logger.Info(
		geminiRejectionLogMessage,
		zap.String("surface", "gemini"),
		zap.String("bridge", "chat"),
		zap.String("rejection_class", string(class)),
		zap.Int("http_status", status),
	)
}

func (s *Server) writeGeminiPreIngressError(w http.ResponseWriter, status int, code, message string) {
	s.observeGeminiRejection(geminiRejectionPreIngress, status)
	writeGeminiError(w, status, code, message)
}

func (s *Server) handleGeminiStreamGenerateContent(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		s.writeGeminiPreIngressError(w, http.StatusMethodNotAllowed, "INVALID_ARGUMENT", "method not allowed")
		return
	}
	routeID, callerModel, err := parseGeminiStreamPath(r)
	if err != nil {
		s.writeGeminiPreIngressError(w, http.StatusBadRequest, "INVALID_ARGUMENT", "request path is invalid")
		return
	}
	defer r.Body.Close()
	body, err := readOpenAIIngressBody(w, r, s.maxIngressSnapshotBytes())
	if err != nil {
		s.writeGeminiPreIngressError(w, http.StatusBadRequest, "INVALID_ARGUMENT", "request body is invalid")
		return
	}
	chatBody, err := prepareGeminiChatBridge(body, routeID)
	if err != nil {
		s.writeGeminiPreIngressError(w, http.StatusBadRequest, "INVALID_ARGUMENT", "request body is invalid")
		return
	}

	internal := r.Clone(r.Context())
	internal.URL.Path = "/v1/chat/completions"
	internal.URL.RawPath = ""
	internal.URL.RawQuery = ""
	internal.RequestURI = "/v1/chat/completions"
	internal.Body = io.NopCloser(bytes.NewReader(chatBody))
	internal.ContentLength = int64(len(chatBody))
	internal.Header = r.Header.Clone()
	internal.Header.Del("Authorization")
	internal.Header.Del("X-Goog-Api-Key")
	internal.Header.Set("Content-Type", "application/json")

	bridge := newGeminiBridgeResponseWriter(w, callerModel, func(status int) {
		s.observeGeminiRejection(geminiRejectionProviderHTTP, status)
	})
	s.handleChatCompletions(bridge, internal)
	bridge.Finish()
}

func parseGeminiStreamPath(r *http.Request) (string, string, error) {
	if r == nil || r.URL == nil {
		return "", "", fmt.Errorf("missing URL")
	}
	parts := strings.Split(strings.TrimPrefix(r.URL.Path, geminiPathPrefix), "/")
	if len(parts) != 4 || parts[1] != "v1beta" || parts[2] != "models" {
		return "", "", fmt.Errorf("unexpected path")
	}
	const suffix = ":streamGenerateContent"
	if !strings.HasSuffix(parts[3], suffix) {
		return "", "", fmt.Errorf("unexpected method")
	}
	routeID := parts[0]
	callerModel := strings.TrimSuffix(parts[3], suffix)
	if !geminiPathToken.MatchString(routeID) || !geminiCallerModelLabel.MatchString(callerModel) || strings.TrimSpace(callerModel) != callerModel {
		return "", "", fmt.Errorf("invalid path token")
	}
	query := r.URL.Query()
	if len(query) != 1 || len(query["alt"]) != 1 || query.Get("alt") != "sse" {
		return "", "", fmt.Errorf("alt=sse is required")
	}
	return routeID, callerModel, nil
}

func prepareGeminiChatBridge(body []byte, routeID string) ([]byte, error) {
	if err := validateJSONMembers(body); err != nil {
		return nil, err
	}
	decoder := json.NewDecoder(bytes.NewReader(body))
	decoder.DisallowUnknownFields()
	var req geminiRequest
	if err := decoder.Decode(&req); err != nil {
		return nil, err
	}
	if err := requireGeminiJSONEOF(decoder); err != nil {
		return nil, err
	}
	if len(req.Contents) == 0 {
		return nil, fmt.Errorf("contents are required")
	}
	chat := map[string]any{
		"model": routeID, "stream": true,
		"stream_options": map[string]any{"include_usage": true},
	}
	messages := make([]map[string]any, 0, len(req.Contents)+1)
	if req.SystemInstruction != nil {
		text, err := geminiTextOnly(*req.SystemInstruction)
		if err != nil || strings.TrimSpace(text) == "" {
			return nil, fmt.Errorf("systemInstruction is invalid")
		}
		messages = append(messages, map[string]any{"role": "system", "content": text})
	}
	pendingCalls := newGeminiPendingCalls()
	for contentIndex, content := range req.Contents {
		converted, err := geminiContentToChat(content, contentIndex, pendingCalls)
		if err != nil {
			return nil, err
		}
		messages = append(messages, converted...)
	}
	chat["messages"] = messages
	if config := req.GenerationConfig; config != nil {
		if config.CandidateCount != nil && *config.CandidateCount != 1 {
			return nil, fmt.Errorf("candidateCount must be one")
		}
		if config.MaxOutputTokens != nil {
			if *config.MaxOutputTokens <= 0 {
				return nil, fmt.Errorf("maxOutputTokens must be positive")
			}
			chat["max_tokens"] = *config.MaxOutputTokens
		}
		if len(config.StopSequences) > 0 {
			chat["stop"] = config.StopSequences
		}
		if config.Temperature != nil {
			if *config.Temperature < 0 || *config.Temperature > 2 {
				return nil, fmt.Errorf("temperature is invalid")
			}
		}
		if config.TopK != nil {
			if *config.TopK <= 0 {
				return nil, fmt.Errorf("topK is invalid")
			}
		}
		if config.TopP != nil {
			if *config.TopP < 0 || *config.TopP > 1 {
				return nil, fmt.Errorf("topP is invalid")
			}
		}
		if thinking := config.ThinkingConfig; thinking != nil {
			googleThinking := make(map[string]any)
			if thinking.IncludeThoughts != nil {
				googleThinking["include_thoughts"] = *thinking.IncludeThoughts
			}
			if thinking.ThinkingBudget != nil {
				if *thinking.ThinkingBudget < -1 {
					return nil, fmt.Errorf("thinkingBudget is invalid")
				}
				googleThinking["thinking_budget"] = *thinking.ThinkingBudget
			}
			if len(googleThinking) > 0 {
				// Gemini's OpenAI-compatible endpoint accepts native options only
				// below extra_body.google; generic think/include_reasoning fields
				// are rejected by that endpoint.
				chat["extra_body"] = map[string]any{
					"google": map[string]any{"thinking_config": googleThinking},
				}
			}
		}
		responseSchema, err := geminiExclusiveSchema(config.ResponseSchema, config.ResponseJSONSchema)
		if err != nil {
			return nil, fmt.Errorf("response schema is invalid")
		}
		switch config.ResponseMIMEType {
		case "":
			if responseSchema != nil {
				return nil, fmt.Errorf("response MIME type is required")
			}
		case "text/plain":
			if responseSchema != nil {
				return nil, fmt.Errorf("text response schema is invalid")
			}
		case "application/json":
			if responseSchema == nil {
				chat["response_format"] = map[string]any{"type": "json_object"}
			} else {
				chat["response_format"] = map[string]any{
					"type": "json_schema",
					"json_schema": map[string]any{
						"name": "agy_response", "strict": true, "schema": responseSchema,
					},
				}
			}
		default:
			return nil, fmt.Errorf("response MIME type is invalid")
		}
	}
	if len(req.Tools) > 0 {
		tools := make([]map[string]any, 0)
		for _, group := range req.Tools {
			if len(group.FunctionDeclarations) == 0 {
				return nil, fmt.Errorf("functionDeclarations are required")
			}
			for _, declaration := range group.FunctionDeclarations {
				if !geminiPathToken.MatchString(declaration.Name) {
					return nil, fmt.Errorf("function declaration is invalid")
				}
				schema, err := geminiExclusiveSchema(
					declaration.Parameters, declaration.ParametersJSONSchema,
				)
				if err != nil || schema == nil {
					return nil, fmt.Errorf("function schema is invalid")
				}
				if _, err := geminiExclusiveSchema(
					declaration.Response, declaration.ResponseJSONSchema,
				); err != nil {
					return nil, fmt.Errorf("function response schema is invalid")
				}
				function := map[string]any{"name": declaration.Name, "parameters": schema}
				if declaration.Description != "" {
					function["description"] = declaration.Description
				}
				tools = append(tools, map[string]any{"type": "function", "function": function})
			}
		}
		chat["tools"] = tools
	}
	if req.ToolConfig != nil && req.ToolConfig.FunctionCallingConfig != nil {
		switch strings.ToUpper(req.ToolConfig.FunctionCallingConfig.Mode) {
		case "", "AUTO":
			chat["tool_choice"] = "auto"
		case "ANY":
			chat["tool_choice"] = "required"
		case "NONE":
			chat["tool_choice"] = "none"
		default:
			return nil, fmt.Errorf("function calling mode is invalid")
		}
	}
	return json.Marshal(chat)
}

func geminiExclusiveSchema(first, second json.RawMessage) (map[string]any, error) {
	if len(first) > 0 && len(second) > 0 {
		return nil, fmt.Errorf("schema alternatives conflict")
	}
	raw := first
	if len(raw) == 0 {
		raw = second
	}
	if len(raw) == 0 {
		return nil, nil
	}
	var schema map[string]any
	if err := json.Unmarshal(raw, &schema); err != nil || schema == nil {
		return nil, fmt.Errorf("schema is not an object")
	}
	if err := normalizeGeminiSchemaTypes(schema); err != nil {
		return nil, err
	}
	return schema, nil
}

func normalizeGeminiSchemaTypes(value any) error {
	switch typed := value.(type) {
	case map[string]any:
		for key, item := range typed {
			if key == "type" {
				name, ok := item.(string)
				if !ok {
					return fmt.Errorf("schema type is invalid")
				}
				normalized := strings.ToLower(name)
				switch normalized {
				case "null", "boolean", "object", "array", "number", "integer", "string":
					typed[key] = normalized
				default:
					return fmt.Errorf("schema type is invalid")
				}
				continue
			}
			if err := normalizeGeminiSchemaTypes(item); err != nil {
				return err
			}
		}
	case []any:
		for _, item := range typed {
			if err := normalizeGeminiSchemaTypes(item); err != nil {
				return err
			}
		}
	}
	return nil
}

func geminiTextOnly(content geminiContent) (string, error) {
	// Gemini represents systemInstruction as Content and official agy 1.1.12
	// labels that Content with the API-native "user" role.
	if content.Role != "" && content.Role != "user" {
		return "", fmt.Errorf("role is invalid")
	}
	texts := make([]string, 0, len(content.Parts))
	for _, part := range content.Parts {
		if part.Text == nil || part.FunctionCall != nil || part.FunctionResponse != nil || part.Thought || part.ThoughtSignature != "" {
			return "", fmt.Errorf("only text is supported")
		}
		texts = append(texts, *part.Text)
	}
	return strings.Join(texts, "\n"), nil
}

type geminiPendingCall struct {
	id       string
	name     string
	explicit bool
}

type geminiPendingCalls struct {
	byID   map[string]geminiPendingCall
	byName map[string][]string
}

func newGeminiPendingCalls() *geminiPendingCalls {
	return &geminiPendingCalls{byID: make(map[string]geminiPendingCall), byName: make(map[string][]string)}
}

func (p *geminiPendingCalls) add(name, id string, explicit bool) error {
	if _, duplicate := p.byID[id]; duplicate {
		return fmt.Errorf("duplicate functionCall id")
	}
	p.byID[id] = geminiPendingCall{id: id, name: name, explicit: explicit}
	p.byName[name] = append(p.byName[name], id)
	return nil
}

func (p *geminiPendingCalls) consume(name, id string) (string, error) {
	if id != "" {
		if !geminiToolCallID.MatchString(id) {
			return "", fmt.Errorf("functionResponse id is invalid")
		}
		call, ok := p.byID[id]
		if !ok || call.name != name {
			return "", fmt.Errorf("functionResponse has no matching call")
		}
		p.remove(call)
		return id, nil
	}
	ids := p.byName[name]
	if len(ids) == 0 {
		return "", fmt.Errorf("functionResponse has no matching call")
	}
	call, ok := p.byID[ids[0]]
	if !ok || call.explicit {
		return "", fmt.Errorf("functionResponse id is required")
	}
	p.remove(call)
	return call.id, nil
}

func (p *geminiPendingCalls) remove(call geminiPendingCall) {
	delete(p.byID, call.id)
	ids := p.byName[call.name]
	for index, id := range ids {
		if id != call.id {
			continue
		}
		ids = append(ids[:index], ids[index+1:]...)
		break
	}
	if len(ids) == 0 {
		delete(p.byName, call.name)
	} else {
		p.byName[call.name] = ids
	}
}

func geminiContentToChat(content geminiContent, contentIndex int, pending *geminiPendingCalls) ([]map[string]any, error) {
	role := strings.ToLower(strings.TrimSpace(content.Role))
	if role != "user" && role != "model" {
		return nil, fmt.Errorf("content role is invalid")
	}
	if len(content.Parts) == 0 {
		return nil, fmt.Errorf("content parts are required")
	}
	var texts []string
	var reasoning []string
	var toolCalls []any
	var toolMessages []map[string]any
	for partIndex, part := range content.Parts {
		set := 0
		if part.Text != nil {
			set++
		}
		if part.FunctionCall != nil {
			set++
		}
		if part.FunctionResponse != nil {
			set++
		}
		if set != 1 {
			return nil, fmt.Errorf("content part is invalid")
		}
		if part.Text != nil {
			if role == "model" && part.Thought {
				reasoning = append(reasoning, *part.Text)
			} else if part.Thought {
				return nil, fmt.Errorf("user thought is invalid")
			} else {
				texts = append(texts, *part.Text)
			}
			continue
		}
		if part.FunctionCall != nil {
			if role != "model" || !geminiPathToken.MatchString(part.FunctionCall.Name) {
				return nil, fmt.Errorf("functionCall is invalid")
			}
			var args map[string]any
			if json.Unmarshal(part.FunctionCall.Args, &args) != nil {
				return nil, fmt.Errorf("functionCall args are invalid")
			}
			callID := part.FunctionCall.ID
			explicitID := callID != ""
			if !explicitID {
				callID = fmt.Sprintf("gemini_call_%d_%d", contentIndex, partIndex)
			} else if !geminiToolCallID.MatchString(callID) {
				return nil, fmt.Errorf("functionCall id is invalid")
			}
			if err := pending.add(part.FunctionCall.Name, callID, explicitID); err != nil {
				return nil, err
			}
			call := map[string]any{"id": callID, "type": "function", "function": map[string]any{"name": part.FunctionCall.Name, "arguments": string(part.FunctionCall.Args)}}
			if part.ThoughtSignature != "" {
				call["extra_content"] = openAIChatThoughtSignature(part.ThoughtSignature)
			}
			toolCalls = append(toolCalls, call)
			continue
		}
		response := part.FunctionResponse
		if !geminiPathToken.MatchString(response.Name) {
			return nil, fmt.Errorf("functionResponse is invalid")
		}
		if !json.Valid(response.Response) {
			return nil, fmt.Errorf("functionResponse has no matching call")
		}
		callID, err := pending.consume(response.Name, response.ID)
		if err != nil {
			return nil, err
		}
		toolMessages = append(toolMessages, map[string]any{"role": "tool", "tool_call_id": callID, "content": string(response.Response)})
	}
	if role == "model" {
		if len(toolMessages) > 0 {
			if len(texts) > 0 || len(reasoning) > 0 || len(toolCalls) > 0 {
				return nil, fmt.Errorf("model functionResponse cannot be mixed with assistant content")
			}
			return toolMessages, nil
		}
		message := map[string]any{"role": "assistant", "content": strings.Join(texts, "\n")}
		if len(reasoning) > 0 {
			message["reasoning_content"] = strings.Join(reasoning, "")
		}
		if len(toolCalls) > 0 {
			message["tool_calls"] = toolCalls
		}
		return []map[string]any{message}, nil
	}
	if len(texts) > 0 {
		toolMessages = append(toolMessages, map[string]any{"role": "user", "content": strings.Join(texts, "\n")})
	}
	if len(toolMessages) == 0 {
		return nil, fmt.Errorf("user content is empty")
	}
	return toolMessages, nil
}

func validateJSONMembers(raw []byte) error {
	dec := json.NewDecoder(bytes.NewReader(raw))
	if err := validateGeminiJSONValue(dec); err != nil {
		return err
	}
	return requireGeminiJSONEOF(dec)
}

func validateGeminiJSONValue(dec *json.Decoder) error {
	token, err := dec.Token()
	if err != nil {
		return err
	}
	delim, ok := token.(json.Delim)
	if !ok {
		return nil
	}
	switch delim {
	case '{':
		seen := map[string]struct{}{}
		for dec.More() {
			keyToken, err := dec.Token()
			if err != nil {
				return err
			}
			key, ok := keyToken.(string)
			if !ok {
				return fmt.Errorf("object key is invalid")
			}
			if _, duplicate := seen[key]; duplicate {
				return fmt.Errorf("duplicate member")
			}
			seen[key] = struct{}{}
			if err := validateGeminiJSONValue(dec); err != nil {
				return err
			}
		}
		closeToken, err := dec.Token()
		if err != nil || closeToken != json.Delim('}') {
			return fmt.Errorf("object is invalid")
		}
	case '[':
		for dec.More() {
			if err := validateGeminiJSONValue(dec); err != nil {
				return err
			}
		}
		closeToken, err := dec.Token()
		if err != nil || closeToken != json.Delim(']') {
			return fmt.Errorf("array is invalid")
		}
	default:
		return fmt.Errorf("JSON delimiter is invalid")
	}
	return nil
}

func requireGeminiJSONEOF(dec *json.Decoder) error {
	var extra any
	if err := dec.Decode(&extra); err != io.EOF {
		return fmt.Errorf("trailing JSON value")
	}
	return nil
}
