package llm

import (
	"bufio"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/apr/types"

	"github.com/gin-gonic/gin"
)

func proxyOpenAIAsAnthropic(ctx *gin.Context, provider types.OpenAICompatible, model string, requestBody []byte, observation *llmUsageObservation, handler *LLMHandler) {
	rewrittenBody, err := anthropicRequestToOpenAI(requestBody, model)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "bad_request")
		return
	}
	resp, err := provider.ChatCompletion(ctx.Request.Context(), ctx.Request.Header, rewrittenBody)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "upstream messages fallback failed"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, classifyUpstreamTransportError(err))
		return
	}
	defer resp.Body.Close()
	if isStreamingResponse(resp.Header.Get("Content-Type")) {
		usage := copyChatStreamAsAnthropic(ctx, resp.Body, model)
		handler.recordObservation(ctx, observation, usage, classifyHTTPErrorType(resp.StatusCode, nil))
		return
	}
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "failed to read upstream response"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "network")
		return
	}
	converted, err := openAIResponseToAnthropic(raw, model)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "failed to convert upstream response"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "internal")
		return
	}
	ctx.Header("Content-Type", "application/json")
	ctx.Status(resp.StatusCode)
	_, _ = ctx.Writer.Write(converted)
	handler.recordObservation(ctx, observation, extractUsageFromJSON(converted), classifyHTTPErrorType(resp.StatusCode, converted))
}

func proxyOpenAIAsResponses(ctx *gin.Context, provider types.OpenAICompatible, model string, requestBody []byte, observation *llmUsageObservation, handler *LLMHandler) {
	rewrittenBody, err := responsesRequestToChat(requestBody, model)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "bad_request")
		return
	}
	providerName := "unknown"
	if namedProvider, ok := any(provider).(types.LLMProvider); ok && namedProvider != nil {
		providerName = namedProvider.Name()
	}
	if handler.logger != nil {
		handler.logger.Info("llm responses fallback to chat",
			"provider", providerName,
			"model", model,
			"request", summarizeChatFallbackRequest(rewrittenBody),
		)
	}
	resp, err := provider.ChatCompletion(ctx.Request.Context(), ctx.Request.Header, rewrittenBody)
	if err != nil {
		if handler.logger != nil {
			handler.logger.Error("llm responses fallback chat failed",
				"provider", providerName,
				"model", model,
				"request", summarizeChatFallbackRequest(rewrittenBody),
				"error", err,
			)
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "upstream responses fallback failed"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, classifyUpstreamTransportError(err))
		return
	}
	defer resp.Body.Close()
	if isStreamingResponse(resp.Header.Get("Content-Type")) {
		usage := copyChatStreamAsResponses(ctx, resp.Body, model)
		handler.recordObservation(ctx, observation, usage, classifyHTTPErrorType(resp.StatusCode, nil))
		return
	}
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "failed to read upstream response"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "network")
		return
	}
	converted, err := chatResponseToResponses(raw, model)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"error": "failed to convert upstream response"})
		handler.recordObservation(ctx, observation, llmUsageTotals{}, "internal")
		return
	}
	ctx.Header("Content-Type", "application/json")
	ctx.Status(resp.StatusCode)
	_, _ = ctx.Writer.Write(converted)
	handler.recordObservation(ctx, observation, extractUsageFromJSON(converted), classifyHTTPErrorType(resp.StatusCode, converted))
}

func anthropicRequestToOpenAI(raw []byte, model string) ([]byte, error) {
	var payload map[string]any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, fmt.Errorf("invalid anthropic request body")
	}
	out := map[string]any{
		"model": model,
	}
	messages := make([]any, 0)
	if system := anthropicSystemPrompt(payload["system"]); system != "" {
		messages = append(messages, map[string]any{
			"role":    "system",
			"content": system,
		})
	}
	if items, ok := payload["messages"].([]any); ok {
		for _, rawMessage := range items {
			message, ok := rawMessage.(map[string]any)
			if !ok {
				continue
			}
			role, _ := message["role"].(string)
			converted := map[string]any{"role": role}
			converted["content"] = anthropicContentToOpenAI(message["content"])
			messages = append(messages, converted)
		}
	}
	if len(messages) == 0 {
		return nil, fmt.Errorf("messages cannot be empty")
	}
	out["messages"] = messages
	copyIfPresent(payload, out, "stream", "temperature", "top_p")
	if stop, ok := payload["stop_sequences"]; ok {
		out["stop"] = stop
	}
	if maxTokens, ok := payload["max_tokens"]; ok {
		out["max_tokens"] = maxTokens
	}
	return json.Marshal(out)
}

func anthropicSystemPrompt(value any) string {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case []any:
		var parts []string
		for _, item := range typed {
			if block, ok := item.(map[string]any); ok {
				if text, _ := block["text"].(string); strings.TrimSpace(text) != "" {
					parts = append(parts, text)
				}
			}
		}
		return strings.Join(parts, "\n")
	default:
		return ""
	}
}

func anthropicContentToOpenAI(value any) any {
	switch typed := value.(type) {
	case string:
		return typed
	case []any:
		parts := make([]any, 0, len(typed))
		plainText := make([]string, 0, len(typed))
		textOnly := true
		for _, item := range typed {
			block, ok := item.(map[string]any)
			if !ok {
				continue
			}
			switch strings.TrimSpace(fmt.Sprint(block["type"])) {
			case "text":
				text, _ := block["text"].(string)
				if strings.TrimSpace(text) == "" {
					continue
				}
				plainText = append(plainText, text)
				parts = append(parts, map[string]any{"type": "text", "text": text})
			case "image":
				textOnly = false
				source, _ := block["source"].(map[string]any)
				url := strings.TrimSpace(fmt.Sprint(source["data"]))
				if url == "" {
					continue
				}
				parts = append(parts, map[string]any{
					"type": "image_url",
					"image_url": map[string]any{
						"url": url,
					},
				})
			}
		}
		if textOnly {
			return strings.Join(plainText, "\n")
		}
		return parts
	default:
		return ""
	}
}

func openAIResponseToAnthropic(raw []byte, model string) ([]byte, error) {
	var payload map[string]any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, err
	}
	content := ""
	if choices, ok := payload["choices"].([]any); ok && len(choices) > 0 {
		if choice, ok := choices[0].(map[string]any); ok {
			if message, ok := choice["message"].(map[string]any); ok {
				content = openAIMessageText(message["content"])
			}
		}
	}
	usage := anthropicUsageFromOpenAI(payload["usage"])
	out := map[string]any{
		"id":            valueOrRandomString(payload["id"], "msg"),
		"type":          "message",
		"role":          "assistant",
		"model":         model,
		"content":       []map[string]any{{"type": "text", "text": content}},
		"stop_reason":   "end_turn",
		"stop_sequence": nil,
		"usage":         usage,
	}
	return json.Marshal(out)
}

func openAIMessageText(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	case []any:
		parts := make([]string, 0, len(typed))
		for _, item := range typed {
			block, ok := item.(map[string]any)
			if !ok {
				continue
			}
			if text, _ := block["text"].(string); strings.TrimSpace(text) != "" {
				parts = append(parts, text)
			}
		}
		return strings.Join(parts, "\n")
	default:
		return ""
	}
}

func anthropicUsageFromOpenAI(value any) map[string]any {
	usage, _ := value.(map[string]any)
	inputTokens := intValue(usage["prompt_tokens"])
	outputTokens := intValue(usage["completion_tokens"])
	result := map[string]any{
		"input_tokens":  inputTokens,
		"output_tokens": outputTokens,
	}
	if details, ok := usage["prompt_tokens_details"].(map[string]any); ok {
		if cached := intValue(details["cached_tokens"]); cached > 0 {
			result["cache_read_input_tokens"] = cached
		}
	}
	return result
}

func responsesRequestToChat(raw []byte, model string) ([]byte, error) {
	var req types.ResponseCreateParams
	if err := json.Unmarshal(raw, &req); err != nil {
		return nil, fmt.Errorf("invalid responses request body")
	}
	messages, systemPrompt := convertResponseInputToMessages(req.Input)
	if req.Instructions != nil && strings.TrimSpace(*req.Instructions) != "" {
		systemPrompt = strings.TrimSpace(*req.Instructions)
	}
	if len(messages) == 0 {
		return nil, fmt.Errorf("input cannot be empty")
	}
	chatReq := types.ChatCompletionRequest{
		Model:            model,
		Messages:         messages,
		Temperature:      req.Temperature,
		TopP:             req.TopP,
		MaxTokens:        req.MaxOutputTokens,
		Stream:           req.Stream,
		ToolChoice:       req.ToolChoice,
		ResponseFormat:   req.ResponseFormat,
		FrequencyPenalty: req.FrequencyPenalty,
		PresencePenalty:  req.PresencePenalty,
		Seed:             req.Seed,
		User:             "",
	}
	if systemPrompt != "" {
		chatReq.Messages = append([]types.Message{{Role: "system", Content: systemPrompt}}, chatReq.Messages...)
	}
	if len(req.Tools) > 0 {
		chatReq.Tools = make([]types.Tool, 0, len(req.Tools))
		for _, tool := range req.Tools {
			chatReq.Tools = append(chatReq.Tools, types.Tool{
				Type: "function",
				Function: types.ToolFunction{
					Name:        tool.Name,
					Description: stringValue(tool.Description),
					Parameters:  tool.Parameters,
				},
			})
		}
	}
	if req.ParallelToolCalls != nil {
		chatReq.ParallelToolCalls = req.ParallelToolCalls
	}
	if req.Stream != nil && *req.Stream {
		includeUsage := true
		chatReq.StreamOptions = &types.StreamOptions{IncludeUsage: &includeUsage}
	}
	return json.Marshal(chatReq)
}

func convertResponseInputToMessages(input any) ([]types.Message, string) {
	if input == nil {
		return nil, ""
	}
	switch typed := input.(type) {
	case string:
		if typed == "" {
			return nil, ""
		}
		return []types.Message{{Role: "user", Content: typed}}, ""
	case json.RawMessage:
		return convertResponseInputJSON(typed)
	default:
		jsonBytes, err := json.Marshal(typed)
		if err != nil {
			return nil, ""
		}
		return convertResponseInputJSON(jsonBytes)
	}
}

func convertResponseInputJSON(data []byte) ([]types.Message, string) {
	var str string
	if err := json.Unmarshal(data, &str); err == nil {
		if str == "" {
			return nil, ""
		}
		return []types.Message{{Role: "user", Content: str}}, ""
	}

	var items []json.RawMessage
	if err := json.Unmarshal(data, &items); err == nil {
		var messages []types.Message
		var systemPrompt string
		for _, rawItem := range items {
			var item struct {
				Role    string          `json:"role"`
				Content json.RawMessage `json:"content"`
				Type    string          `json:"type"`
			}
			if err := json.Unmarshal(rawItem, &item); err != nil {
				continue
			}

			switch {
			case item.Role == "system", item.Role == "developer":
				var content string
				_ = json.Unmarshal(item.Content, &content)
				systemPrompt = content
			case item.Role == "user":
				messages = append(messages, convertInputContentToUserMessages(item.Content)...)
			case item.Role == "assistant":
				messages = append(messages, convertInputContentToAssistantMessage(item.Content))
			case item.Type == "function_call_output":
				var toolResult struct {
					CallID string `json:"call_id"`
					Output string `json:"output"`
				}
				_ = json.Unmarshal(rawItem, &toolResult)
				messages = append(messages, types.Message{
					Role:       "tool",
					ToolCallID: toolResult.CallID,
					Content:    toolResult.Output,
				})
			}
		}
		return messages, systemPrompt
	}

	return nil, ""
}

func convertInputContentToUserMessages(content json.RawMessage) []types.Message {
	if content == nil {
		return []types.Message{{Role: "user", Content: ""}}
	}

	var str string
	if err := json.Unmarshal(content, &str); err == nil {
		return []types.Message{{Role: "user", Content: str}}
	}

	var parts []json.RawMessage
	if err := json.Unmarshal(content, &parts); err != nil {
		return []types.Message{{Role: "user", Content: string(content)}}
	}

	var contentParts []types.ContentPart
	for _, rawPart := range parts {
		var part struct {
			Type     string `json:"type"`
			Text     string `json:"text"`
			ImageURL struct {
				URL string `json:"url"`
			} `json:"image_url,omitempty"`
		}
		if err := json.Unmarshal(rawPart, &part); err != nil {
			continue
		}

		switch part.Type {
		case "input_text", "output_text", "text":
			contentParts = append(contentParts, types.ContentPart{
				Type: "text",
				Text: part.Text,
			})
		case "input_image", "image_url":
			contentParts = append(contentParts, types.ContentPart{
				Type:     "image_url",
				ImageURL: &types.ImageURL{URL: part.ImageURL.URL},
			})
		}
	}

	if len(contentParts) == 1 && contentParts[0].Type == "text" {
		return []types.Message{{Role: "user", Content: contentParts[0].Text}}
	}
	return []types.Message{{Role: "user", Content: contentParts}}
}

func convertInputContentToAssistantMessage(content json.RawMessage) types.Message {
	var str string
	if err := json.Unmarshal(content, &str); err == nil {
		return types.Message{Role: "assistant", Content: str}
	}

	var parts []json.RawMessage
	if err := json.Unmarshal(content, &parts); err != nil {
		return types.Message{Role: "assistant", Content: string(content)}
	}

	var toolCalls []types.ToolCall
	var textParts []string
	for _, rawPart := range parts {
		var part struct {
			Type      string `json:"type"`
			Text      string `json:"text"`
			CallID    string `json:"call_id"`
			Name      string `json:"name"`
			Arguments string `json:"arguments"`
		}
		if err := json.Unmarshal(rawPart, &part); err != nil {
			continue
		}

		switch part.Type {
		case "output_text", "text":
			textParts = append(textParts, part.Text)
		case "function_call":
			toolCalls = append(toolCalls, types.ToolCall{
				ID:   part.CallID,
				Type: "function",
				Function: types.Function{
					Name:      part.Name,
					Arguments: part.Arguments,
				},
			})
		}
	}

	message := types.Message{Role: "assistant"}
	if len(toolCalls) > 0 {
		message.ToolCalls = toolCalls
		if len(textParts) > 0 {
			message.Content = strings.Join(textParts, "\n")
		}
		return message
	}
	message.Content = strings.Join(textParts, "\n")
	return message
}

func stringValue(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func summarizeChatFallbackRequest(raw []byte) map[string]any {
	var payload struct {
		Model    string `json:"model"`
		Stream   bool   `json:"stream"`
		Messages []struct {
			Role    string `json:"role"`
			Content any    `json:"content"`
		} `json:"messages"`
		Tools []any `json:"tools"`
	}
	if err := json.Unmarshal(raw, &payload); err != nil {
		return map[string]any{"invalid_json": true}
	}
	roles := make([]string, 0, len(payload.Messages))
	for _, message := range payload.Messages {
		roles = append(roles, strings.TrimSpace(message.Role))
	}
	return map[string]any{
		"model":         payload.Model,
		"stream":        payload.Stream,
		"message_count": len(payload.Messages),
		"roles":         roles,
		"tool_count":    len(payload.Tools),
	}
}

func chatResponseToResponses(raw []byte, model string) ([]byte, error) {
	var payload map[string]any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, err
	}
	content := ""
	if choices, ok := payload["choices"].([]any); ok && len(choices) > 0 {
		if choice, ok := choices[0].(map[string]any); ok {
			if message, ok := choice["message"].(map[string]any); ok {
				content = openAIMessageText(message["content"])
			}
		}
	}
	usage, _ := payload["usage"].(map[string]any)
	inputTokens := intValue(usage["prompt_tokens"])
	outputTokens := intValue(usage["completion_tokens"])
	cachedTokens := 0
	if details, ok := usage["prompt_tokens_details"].(map[string]any); ok {
		cachedTokens = intValue(details["cached_tokens"])
	}
	out := map[string]any{
		"id":      valueOrRandomString(payload["id"], "resp"),
		"object":  "response",
		"created": time.Now().Unix(),
		"model":   model,
		"status":  "completed",
		"output": []any{
			map[string]any{
				"type":   "message",
				"id":     randomID("msg"),
				"status": "completed",
				"role":   "assistant",
				"content": []any{
					map[string]any{
						"type": "output_text",
						"text": content,
					},
				},
			},
		},
		"usage": map[string]any{
			"input_tokens":  inputTokens,
			"output_tokens": outputTokens,
			"total_tokens":  inputTokens + outputTokens,
			"input_tokens_details": map[string]any{
				"cached_tokens": cachedTokens,
			},
		},
	}
	return json.Marshal(out)
}

func copyChatStreamAsAnthropic(ctx *gin.Context, reader io.Reader, model string) llmUsageTotals {
	ctx.Header("Content-Type", "text/event-stream")
	ctx.Header("Cache-Control", "no-cache")
	ctx.Header("Connection", "keep-alive")
	start := map[string]any{
		"type": "message_start",
		"message": map[string]any{
			"id":            randomID("msg"),
			"type":          "message",
			"role":          "assistant",
			"model":         model,
			"content":       []any{},
			"stop_reason":   nil,
			"stop_sequence": nil,
			"usage":         map[string]any{"input_tokens": 0, "output_tokens": 0},
		},
	}
	writeAnthropicEvent(ctx, "message_start", start)
	writeAnthropicEvent(ctx, "content_block_start", map[string]any{
		"type":          "content_block_start",
		"index":         0,
		"content_block": map[string]any{"type": "text", "text": ""},
	})
	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	inputTokens, outputTokens, cachedTokens := 0, 0, 0
	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if data == "" {
			continue
		}
		if data == "[DONE]" {
			break
		}
		var chunk map[string]any
		if json.Unmarshal([]byte(data), &chunk) != nil {
			continue
		}
		if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 {
			if choice, ok := choices[0].(map[string]any); ok {
				if delta, ok := choice["delta"].(map[string]any); ok {
					if text, _ := delta["content"].(string); text != "" {
						writeAnthropicEvent(ctx, "content_block_delta", map[string]any{
							"type":  "content_block_delta",
							"index": 0,
							"delta": map[string]any{"type": "text_delta", "text": text},
						})
					}
				}
			}
		}
		if usage, ok := chunk["usage"].(map[string]any); ok {
			inputTokens = intValue(usage["prompt_tokens"])
			outputTokens = intValue(usage["completion_tokens"])
			if details, ok := usage["prompt_tokens_details"].(map[string]any); ok {
				cachedTokens = intValue(details["cached_tokens"])
			}
		}
	}
	writeAnthropicEvent(ctx, "content_block_stop", map[string]any{"type": "content_block_stop", "index": 0})
	usage := map[string]any{"input_tokens": inputTokens, "output_tokens": outputTokens}
	if cachedTokens > 0 {
		usage["cache_read_input_tokens"] = cachedTokens
	}
	writeAnthropicEvent(ctx, "message_delta", map[string]any{
		"type":  "message_delta",
		"delta": map[string]any{"stop_reason": "end_turn", "stop_sequence": nil},
		"usage": usage,
	})
	writeAnthropicEvent(ctx, "message_stop", map[string]any{"type": "message_stop"})
	return llmUsageTotals{
		InputTokens:       uint64(maxInt(inputTokens, 0)),
		CachedInputTokens: uint64(maxInt(cachedTokens, 0)),
		OutputTokens:      uint64(maxInt(outputTokens, 0)),
	}
}

func writeAnthropicEvent(ctx *gin.Context, event string, payload map[string]any) {
	raw, _ := json.Marshal(payload)
	_, _ = ctx.Writer.Write([]byte("event: " + event + "\n"))
	_, _ = ctx.Writer.Write([]byte("data: " + string(raw) + "\n\n"))
	if flusher, ok := ctx.Writer.(http.Flusher); ok {
		flusher.Flush()
	}
}

func copyChatStreamAsResponses(ctx *gin.Context, reader io.Reader, model string) llmUsageTotals {
	ctx.Header("Content-Type", "text/event-stream")
	ctx.Header("Cache-Control", "no-cache")
	ctx.Header("Connection", "keep-alive")
	responseID := randomID("resp")
	writeResponsesEvent(ctx, map[string]any{
		"type":     "response.created",
		"response": map[string]any{"id": responseID, "object": "response", "model": model, "status": "in_progress"},
	})
	scanner := bufio.NewScanner(reader)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	inputTokens, outputTokens, cachedTokens := 0, 0, 0
	for scanner.Scan() {
		line := scanner.Text()
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if data == "" {
			continue
		}
		if data == "[DONE]" {
			break
		}
		var chunk map[string]any
		if json.Unmarshal([]byte(data), &chunk) != nil {
			continue
		}
		if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 {
			if choice, ok := choices[0].(map[string]any); ok {
				if delta, ok := choice["delta"].(map[string]any); ok {
					if text, _ := delta["content"].(string); text != "" {
						writeResponsesEvent(ctx, map[string]any{
							"type":  "response.output_text.delta",
							"delta": text,
						})
					}
				}
			}
		}
		if usage, ok := chunk["usage"].(map[string]any); ok {
			inputTokens = intValue(usage["prompt_tokens"])
			outputTokens = intValue(usage["completion_tokens"])
			if details, ok := usage["prompt_tokens_details"].(map[string]any); ok {
				cachedTokens = intValue(details["cached_tokens"])
			}
		}
	}
	writeResponsesEvent(ctx, map[string]any{
		"type": "response.completed",
		"response": map[string]any{
			"id":      responseID,
			"object":  "response",
			"model":   model,
			"status":  "completed",
			"created": time.Now().Unix(),
			"usage": map[string]any{
				"input_tokens":  inputTokens,
				"output_tokens": outputTokens,
				"total_tokens":  inputTokens + outputTokens,
				"input_tokens_details": map[string]any{
					"cached_tokens": cachedTokens,
				},
			},
		},
	})
	return llmUsageTotals{
		InputTokens:       uint64(maxInt(inputTokens, 0)),
		CachedInputTokens: uint64(maxInt(cachedTokens, 0)),
		OutputTokens:      uint64(maxInt(outputTokens, 0)),
	}
}

func writeResponsesEvent(ctx *gin.Context, payload map[string]any) {
	raw, _ := json.Marshal(payload)
	_, _ = ctx.Writer.Write([]byte("data: " + string(raw) + "\n\n"))
	if flusher, ok := ctx.Writer.(http.Flusher); ok {
		flusher.Flush()
	}
}

func copyIfPresent(in, out map[string]any, keys ...string) {
	for _, key := range keys {
		if value, ok := in[key]; ok {
			out[key] = value
		}
	}
}

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

func valueOrRandomString(value any, prefix string) string {
	if text, _ := value.(string); strings.TrimSpace(text) != "" {
		return text
	}
	return randomID(prefix)
}

func randomID(prefix string) string {
	raw := make([]byte, 6)
	if _, err := rand.Read(raw); err != nil {
		return prefix + "_" + fmt.Sprint(time.Now().UnixNano())
	}
	return prefix + "_" + hex.EncodeToString(raw)
}
