package openai_compat

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"iop/packages/go/config"
	runtime "iop/packages/go/execution"
)

func (a *Adapter) applyHeaders(req *http.Request, jsonBody bool) {
	for k, v := range a.headers {
		if jsonBody && http.CanonicalHeaderKey(k) == "Content-Type" {
			continue
		}
		req.Header.Set(k, v)
	}
	if jsonBody {
		req.Header.Set("Content-Type", "application/json")
	}
}

// prepareRequest is the single operation/auth/model preparation path used by
// Models, normalized Chat, Responses, and raw provider tunnels.
func (a *Adapter) prepareRequest(ctx context.Context, method, operation, legacyPath string, body []byte, requestHeaders map[string]string, jsonBody bool) (*http.Request, error) {
	urlStr, err := a.operationURL(operation, legacyPath)
	if err != nil {
		return nil, fmt.Errorf("resolve operation URL: %w", err)
	}
	preparedBody, err := a.mapProfileBodyModel(body)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, method, urlStr, bytes.NewReader(preparedBody))
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	a.applyHeaders(req, jsonBody)
	for key, value := range requestHeaders {
		req.Header.Set(key, value)
	}
	a.applyProfileAuth(req)
	return req, nil
}

func (a *Adapter) mapProfileBodyModel(body []byte) ([]byte, error) {
	if a.profile == nil || len(a.profile.ModelMapping) == 0 || len(bytes.TrimSpace(body)) == 0 {
		return body, nil
	}
	trimmed := bytes.TrimSpace(body)
	if len(trimmed) < 2 || trimmed[0] != '{' || trimmed[len(trimmed)-1] != '}' {
		return nil, fmt.Errorf("decode profile request body: top-level JSON value must be an object")
	}
	var payload map[string]json.RawMessage
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("decode profile request body: %w", err)
	}
	start, end, model, found, err := topLevelJSONStringField(body, "model")
	if err != nil {
		return nil, fmt.Errorf("decode profile request body: %w", err)
	}
	if !found {
		return body, nil
	}
	mapped := a.profile.MapModel(model)
	if mapped == model {
		return body, nil
	}
	encoded, err := json.Marshal(mapped)
	if err != nil {
		return nil, fmt.Errorf("encode profile request body: %w", err)
	}
	prepared := make([]byte, 0, len(body)-(end-start)+len(encoded))
	prepared = append(prepared, body[:start]...)
	prepared = append(prepared, encoded...)
	prepared = append(prepared, body[end:]...)
	return prepared, nil
}

// topLevelJSONStringField locates the last top-level occurrence of key when
// its value is a JSON string. It returns byte offsets into the original body,
// allowing a surgical splice that preserves every unrelated byte.
func topLevelJSONStringField(body []byte, wanted string) (start, end int, value string, found bool, err error) {
	pos := skipJSONSpace(body, 0)
	if pos >= len(body) || body[pos] != '{' {
		return 0, 0, "", false, fmt.Errorf("top-level JSON value must be an object")
	}
	pos++
	for {
		pos = skipJSONSpace(body, pos)
		if pos >= len(body) {
			return 0, 0, "", false, fmt.Errorf("unterminated object")
		}
		if body[pos] == '}' {
			return start, end, value, found, nil
		}
		keyStart := pos
		keyEnd, scanErr := scanJSONStringEnd(body, keyStart)
		if scanErr != nil {
			return 0, 0, "", false, scanErr
		}
		var key string
		if unmarshalErr := json.Unmarshal(body[keyStart:keyEnd], &key); unmarshalErr != nil {
			return 0, 0, "", false, unmarshalErr
		}
		pos = skipJSONSpace(body, keyEnd)
		if pos >= len(body) || body[pos] != ':' {
			return 0, 0, "", false, fmt.Errorf("object key %q has no value", key)
		}
		valueStart := skipJSONSpace(body, pos+1)
		valueEnd, scanErr := scanJSONValueEnd(body, valueStart)
		if scanErr != nil {
			return 0, 0, "", false, scanErr
		}
		if key == wanted {
			found = false
			if valueStart < len(body) && body[valueStart] == '"' {
				var decoded string
				if unmarshalErr := json.Unmarshal(body[valueStart:valueEnd], &decoded); unmarshalErr != nil {
					return 0, 0, "", false, unmarshalErr
				}
				start, end, value, found = valueStart, valueEnd, decoded, true
			}
		}
		pos = skipJSONSpace(body, valueEnd)
		if pos < len(body) && body[pos] == ',' {
			pos++
			continue
		}
		if pos < len(body) && body[pos] == '}' {
			return start, end, value, found, nil
		}
		return 0, 0, "", false, fmt.Errorf("invalid object delimiter")
	}
}

func skipJSONSpace(body []byte, pos int) int {
	for pos < len(body) {
		switch body[pos] {
		case ' ', '\t', '\r', '\n':
			pos++
		default:
			return pos
		}
	}
	return pos
}

func scanJSONStringEnd(body []byte, start int) (int, error) {
	if start >= len(body) || body[start] != '"' {
		return 0, fmt.Errorf("expected JSON string")
	}
	escaped := false
	for pos := start + 1; pos < len(body); pos++ {
		if escaped {
			escaped = false
			continue
		}
		switch body[pos] {
		case '\\':
			escaped = true
		case '"':
			return pos + 1, nil
		}
	}
	return 0, fmt.Errorf("unterminated JSON string")
}

func scanJSONValueEnd(body []byte, start int) (int, error) {
	if start >= len(body) {
		return 0, fmt.Errorf("missing JSON value")
	}
	if body[start] == '"' {
		return scanJSONStringEnd(body, start)
	}
	depth := 0
	inString := false
	escaped := false
	for pos := start; pos < len(body); pos++ {
		c := body[pos]
		if inString {
			if escaped {
				escaped = false
				continue
			}
			if c == '\\' {
				escaped = true
			} else if c == '"' {
				inString = false
			}
			continue
		}
		switch c {
		case '"':
			inString = true
		case '{', '[':
			depth++
		case ']':
			depth--
		case '}':
			if depth == 0 {
				return trimJSONSpaceEnd(body, start, pos), nil
			}
			depth--
		case ',':
			if depth == 0 {
				return trimJSONSpaceEnd(body, start, pos), nil
			}
		}
	}
	return trimJSONSpaceEnd(body, start, len(body)), nil
}

func trimJSONSpaceEnd(body []byte, start, end int) int {
	for end > start {
		switch body[end-1] {
		case ' ', '\t', '\r', '\n':
			end--
		default:
			return end
		}
	}
	return end
}

func (a *Adapter) applyProfileAuth(req *http.Request) {
	if a.profile == nil || strings.TrimSpace(a.profile.Auth.Header) == "" {
		return
	}
	target := http.CanonicalHeaderKey(a.profile.Auth.Header)
	value := strings.TrimSpace(req.Header.Get(target))
	if value == "" {
		for _, source := range []string{"Authorization", "X-Api-Key"} {
			if candidate := strings.TrimSpace(req.Header.Get(source)); candidate != "" {
				value = candidate
				break
			}
		}
	}
	if value == "" {
		return
	}
	raw := value
	if fields := strings.Fields(value); len(fields) > 1 {
		raw = strings.Join(fields[1:], " ")
	}
	if scheme := strings.TrimSpace(a.profile.Auth.Scheme); scheme != "" {
		value = scheme + " " + raw
	} else {
		value = raw
	}
	for _, source := range []string{"Authorization", "X-Api-Key"} {
		if !strings.EqualFold(source, target) {
			req.Header.Del(source)
		}
	}
	req.Header.Set(target, value)
}

func (a *Adapter) doChatCompletion(ctx context.Context, body []byte) (*http.Response, error) {
	req, err := a.prepareRequest(ctx, http.MethodPost, string(config.OperationChatCompletions), "/v1/chat/completions", body, nil, true)
	if err != nil {
		return nil, err
	}
	return a.client.Do(req)
}

func (a *Adapter) buildRequestBody(model string, messages []chatMessage, input map[string]any) (map[string]any, error) {
	body := make(map[string]any)
	// Copy caller options as top-level OpenAI-compatible request fields first so
	// adapter-owned fields below always win over them.
	if opts, ok := input["options"].(map[string]any); ok {
		for k, v := range opts {
			body[k] = v
		}
	}
	// Pass through optional OpenAI-compatible fields when present in the input (except think).
	for _, key := range []string{"tools", "tool_choice", "format", "keep_alive"} {
		if v, ok := input[key]; ok {
			body[key] = v
		}
	}
	body["model"] = model
	body["messages"] = messages
	body["stream"] = true

	// Extract think-control fields
	hasThink := false
	var thinkVal bool
	if v, ok := input["think"]; ok {
		if bv, ok := v.(bool); ok {
			hasThink = true
			thinkVal = bv
		}
	}

	hasReasoningEffort := false
	var reasoningEffortVal string
	if v, ok := input["reasoning_effort"]; ok {
		if sv, ok := v.(string); ok {
			hasReasoningEffort = true
			reasoningEffortVal = sv
		}
	}

	hasBudget := false
	var budgetVal any
	if v, ok := input["thinking_token_budget"]; ok {
		hasBudget = true
		budgetVal = v
	}

	// vLLM, vLLM-MLX and Lemonade all serve Qwen3-family templates that honor
	// chat_template_kwargs.enable_thinking / thinking_token_budget. The Lemonade
	// build ignores the top-level `think` field, so all three providers map
	// think-control into chat_template_kwargs rather than top-level fields.
	switch a.provider {
	case "vllm", "vllm-mlx", "lemonade":
		thinkDisableRequested := (hasThink && !thinkVal) || (hasReasoningEffort && reasoningEffortVal == "none")

		// The vLLM-MLX runtime labels its entire streamed output as
		// reasoning_content and keeps generating reasoning even with
		// enable_thinking=false, so a think-disable request can never yield a
		// reasoning-free stream. Fail with a clear compatibility error instead
		// of silently returning a reasoning stream for think=false.
		if a.provider == "vllm-mlx" && thinkDisableRequested {
			return nil, fmt.Errorf("unsupported think control: provider %q cannot disable reasoning generation for streaming responses (think=false / reasoning_effort=none)", a.provider)
		}

		if hasReasoningEffort && reasoningEffortVal != "" && reasoningEffortVal != "none" {
			return nil, fmt.Errorf("unsupported think control: reasoning_effort %q is not supported by %s", reasoningEffortVal, a.provider)
		}

		if hasBudget {
			var budgetInt int64
			valid := false
			switch bv := budgetVal.(type) {
			case int:
				budgetInt = int64(bv)
				valid = true
			case int64:
				budgetInt = bv
				valid = true
			case float64:
				if bv == float64(int64(bv)) {
					budgetInt = int64(bv)
					valid = true
				}
			}
			if !valid || budgetInt < 0 {
				return nil, fmt.Errorf("unsupported think control: invalid thinking_token_budget %v", budgetVal)
			}
		}

		if hasThink || (hasReasoningEffort && reasoningEffortVal == "none") || hasBudget {
			var chatTemplateKwargs map[string]any
			if ctk, ok := body["chat_template_kwargs"].(map[string]any); ok {
				chatTemplateKwargs = make(map[string]any)
				for k, v := range ctk {
					chatTemplateKwargs[k] = v
				}
			} else {
				chatTemplateKwargs = make(map[string]any)
			}

			if (hasThink && !thinkVal) || (hasReasoningEffort && reasoningEffortVal == "none") {
				chatTemplateKwargs["enable_thinking"] = false
			} else if (hasThink && thinkVal) || hasBudget {
				chatTemplateKwargs["enable_thinking"] = true
			}

			if hasBudget {
				chatTemplateKwargs["thinking_token_budget"] = budgetVal
			}

			body["chat_template_kwargs"] = chatTemplateKwargs
		}
	default:
		// empty/unknown provider: pass requested fields through as-is.
		if hasThink {
			body["think"] = thinkVal
		}
		if hasReasoningEffort {
			body["reasoning_effort"] = reasoningEffortVal
		}
		if hasBudget {
			body["thinking_token_budget"] = budgetVal
		}
	}

	return body, nil
}

func retryBodyWithForcedSingleTool(body map[string]any, errorBody string) (map[string]any, bool) {
	if !isAutoToolChoiceUnsupportedError(errorBody) {
		return nil, false
	}
	forced, ok := forcedToolChoiceForSingleTool(body["tools"])
	if !ok {
		return nil, false
	}
	next := make(map[string]any, len(body)+1)
	for key, value := range body {
		next[key] = value
	}
	next["tool_choice"] = forced
	return next, true
}

func isAutoToolChoiceUnsupportedError(errorBody string) bool {
	errorBody = strings.ToLower(errorBody)
	return strings.Contains(errorBody, "auto") &&
		strings.Contains(errorBody, "tool choice") &&
		isToolCallingUnsupportedError(errorBody)
}

func isToolCallingUnsupportedError(errorBody string) bool {
	errorBody = strings.ToLower(errorBody)
	return strings.Contains(errorBody, "enable-auto-tool-choice") ||
		strings.Contains(errorBody, "tool-call-parser")
}

func forcedToolChoiceForSingleTool(tools any) (map[string]any, bool) {
	items, ok := tools.([]any)
	if !ok || len(items) != 1 {
		return nil, false
	}
	tool, ok := items[0].(map[string]any)
	if !ok {
		return nil, false
	}
	name := ""
	if fn, ok := tool["function"].(map[string]any); ok {
		name, _ = fn["name"].(string)
	}
	if name == "" {
		name, _ = tool["name"].(string)
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, false
	}
	return map[string]any{
		"type": "function",
		"function": map[string]any{
			"name": name,
		},
	}, true
}

func retryBodyWithTextToolFallback(body map[string]any, errorBody string) (map[string]any, bool) {
	if !isToolCallingUnsupportedError(errorBody) {
		return nil, false
	}
	instruction, ok := textToolFallbackInstruction(body["tools"])
	if !ok {
		return nil, false
	}
	messages, ok := prependTextToolFallbackInstruction(body["messages"], instruction)
	if !ok {
		return nil, false
	}
	next := make(map[string]any, len(body)+1)
	for key, value := range body {
		next[key] = value
	}
	delete(next, "tools")
	delete(next, "tool_choice")
	next["messages"] = messages
	return next, true
}

func prependTextToolFallbackInstruction(value any, instruction string) (any, bool) {
	switch messages := value.(type) {
	case []chatMessage:
		system := chatMessage{Role: "system", Content: instruction}
		out := make([]chatMessage, 0, len(messages)+1)
		for _, msg := range messages {
			if strings.EqualFold(strings.TrimSpace(msg.Role), "system") {
				system.Content = joinTextToolFallbackSystemContent(system.Content, msg.Content)
				continue
			}
			out = append(out, msg)
		}
		return append([]chatMessage{system}, out...), true
	case []any:
		systemContent := instruction
		out := make([]any, 0, len(messages)+1)
		for _, item := range messages {
			msg, ok := item.(map[string]any)
			if !ok {
				out = append(out, item)
				continue
			}
			role, _ := msg["role"].(string)
			if strings.EqualFold(strings.TrimSpace(role), "system") {
				systemContent = joinTextToolFallbackSystemContent(systemContent, stringFromAny(msg["content"]))
				continue
			}
			out = append(out, item)
		}
		return append([]any{map[string]any{"role": "system", "content": systemContent}}, out...), true
	default:
		return nil, false
	}
}

func joinTextToolFallbackSystemContent(first, next string) string {
	first = strings.TrimSpace(first)
	next = strings.TrimSpace(next)
	switch {
	case first == "":
		return next
	case next == "":
		return first
	default:
		return first + "\n\n" + next
	}
}

func stringFromAny(value any) string {
	switch v := value.(type) {
	case nil:
		return ""
	case string:
		return v
	default:
		encoded, _ := json.Marshal(v)
		return string(encoded)
	}
}

func textToolFallbackInstruction(tools any) (string, bool) {
	items, ok := anyItems(tools)
	if !ok || len(items) == 0 {
		return "", false
	}
	encoded, err := json.Marshal(items)
	if err != nil {
		return "", false
	}
	return "Tool calls must be emitted as plain text because this backend does not support native OpenAI tool calling. When a tool is needed, respond with exactly one tool call and no markdown:\n" +
		"<tool_call>\n<function=TOOL_NAME>\n<parameter=PARAMETER_NAME>JSON_VALUE</parameter>\n</function>\n</tool_call>\n" +
		"Use valid JSON for each parameter value and follow the supplied parameter schema. Available tools JSON: " + string(encoded), true
}

func anyItems(value any) ([]any, bool) {
	switch v := value.(type) {
	case []any:
		return v, true
	default:
		encoded, err := json.Marshal(v)
		if err != nil {
			return nil, false
		}
		var out []any
		if err := json.Unmarshal(encoded, &out); err != nil {
			return nil, false
		}
		return out, true
	}
}

func completeEvent(runID, finishReason string, usage *runtime.UsageStats, outputTokens int, toolCalls []any, textToolFallback bool) runtime.RuntimeEvent {
	if usage == nil {
		usage = &runtime.UsageStats{OutputTokens: outputTokens}
	}
	var metadata map[string]string
	if finishReason != "" {
		metadata = map[string]string{"finish_reason": finishReason}
	}
	if textToolFallback {
		if metadata == nil {
			metadata = make(map[string]string, 2)
		}
		metadata[runtimeMetadataOpenAITextToolFallback] = "true"
	}
	if len(toolCalls) > 0 {
		if metadata == nil {
			metadata = make(map[string]string, 2)
		}
		if finishReason == "" {
			metadata["finish_reason"] = "tool_calls"
		}
		if encoded, err := json.Marshal(toolCalls); err == nil {
			metadata[runtimeMetadataOpenAIToolCalls] = string(encoded)
		}
	}
	return runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeComplete,
		Message:   "openai_compat chat complete",
		Usage:     usage,
		Metadata:  metadata,
		Timestamp: time.Now(),
	}
}

// messagesFromInput extracts chat messages from the execution input.
func messagesFromInput(input map[string]any) []chatMessage {
	if raw, ok := input["messages"].([]any); ok {
		out := make([]chatMessage, 0, len(raw))
		for _, item := range raw {
			m, ok := item.(map[string]any)
			if !ok {
				continue
			}
			role, _ := m["role"].(string)
			content, _ := m["content"].(string)
			toolCallID, _ := m["tool_call_id"].(string)
			toolCalls := anySlice(m["tool_calls"])
			role = strings.TrimSpace(role)
			content = strings.TrimSpace(content)
			toolCallID = strings.TrimSpace(toolCallID)
			if role == "" || (content == "" && len(toolCalls) == 0) {
				continue
			}
			out = append(out, chatMessage{
				Role:       role,
				Content:    content,
				ToolCalls:  toolCalls,
				ToolCallID: toolCallID,
			})
		}
		if len(out) > 0 {
			return out
		}
	}
	if prompt := strings.TrimSpace(stringInput(input, "prompt")); prompt != "" {
		return []chatMessage{{Role: "user", Content: prompt}}
	}
	return nil
}

func emitError(ctx context.Context, sink runtime.EventSink, runID, msg string) error {
	return sink.Emit(ctx, runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeError,
		Error:     msg,
		Timestamp: time.Now(),
	})
}

func stringInput(input map[string]any, key string) string {
	if input == nil {
		return ""
	}
	if v, ok := input[key].(string); ok {
		return v
	}
	return ""
}

func anySlice(v any) []any {
	if items, ok := v.([]any); ok {
		return items
	}
	return nil
}
