package httpapi

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"

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

const kelingOmniCompatibilityMarker = "keling_omni_v1"

type kelingCompatRequestIDKey struct{}

type KelingOmniVideoRequest struct {
	ModelName     string                   `json:"model_name" example:"kling-v3-omni"`
	Prompt        string                   `json:"prompt" example:"A quiet street in the rain with natural ambient sound"`
	MultiShot     bool                     `json:"multi_shot" example:"false"`
	ShotType      string                   `json:"shot_type,omitempty" example:"customize"`
	MultiPrompt   []KelingOmniMultiPrompt  `json:"multi_prompt,omitempty"`
	ImageList     []KelingOmniImageInput   `json:"image_list,omitempty"`
	ElementList   []KelingOmniElementInput `json:"element_list,omitempty"`
	VideoList     []KelingOmniVideoInput   `json:"video_list,omitempty"`
	Sound         string                   `json:"sound" enums:"on,off" example:"on"`
	Mode          string                   `json:"mode" enums:"std,pro,4k" example:"pro"`
	AspectRatio   string                   `json:"aspect_ratio" enums:"16:9,9:16,1:1" example:"9:16"`
	Duration      any                      `json:"duration" swaggertype:"string" example:"5"`
	WatermarkInfo KelingOmniWatermarkInfo  `json:"watermark_info,omitempty"`
	CallbackURL   string                   `json:"callback_url,omitempty"`
	ExternalTask  string                   `json:"external_task_id,omitempty"`
}

type KelingOmniMultiPrompt struct {
	Index    int    `json:"index" example:"1"`
	Prompt   string `json:"prompt" example:"A wide establishing shot"`
	Duration any    `json:"duration" swaggertype:"string" example:"3"`
}

type KelingOmniImageInput struct {
	ImageURL string `json:"image_url"`
	Type     string `json:"type,omitempty" enums:"first_frame,end_frame"`
}

type KelingOmniElementInput struct {
	ElementID any `json:"element_id"`
}

type KelingOmniVideoInput struct {
	VideoURL          string `json:"video_url"`
	ReferType         string `json:"refer_type,omitempty" enums:"base,feature"`
	KeepOriginalSound string `json:"keep_original_sound,omitempty" enums:"yes,no"`
}

type KelingOmniWatermarkInfo struct {
	Enabled bool `json:"enabled" example:"false"`
}

type KelingCompatibleEnvelope struct {
	Code      int                `json:"code" example:"0"`
	Message   string             `json:"message" example:"SUCCEED"`
	RequestID string             `json:"request_id"`
	Data      any                `json:"data,omitempty"`
	Error     *publicerror.Error `json:"error,omitempty"`
}

type kelingCompatError struct {
	HTTPStatus  int
	Code        int
	Message     string
	RequestID   string
	PublicError *publicerror.Error
}

func (e *kelingCompatError) Error() string {
	if e == nil {
		return "keling compatibility error"
	}
	return e.Message
}

func newKelingCompatError(status int, code int, message string) *kelingCompatError {
	return &kelingCompatError{HTTPStatus: status, Code: code, Message: message}
}

func (s *Server) requireKelingAPIKey(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestID := newKelingCompatRequestID(r)
		r = r.WithContext(context.WithValue(r.Context(), kelingCompatRequestIDKey{}, requestID))
		user, err := s.auth.Authenticate(r)
		if err != nil {
			code := 1002
			message := "Authorization is invalid"
			if strings.TrimSpace(r.Header.Get("Authorization")) == "" {
				code = 1001
				message = "Authorization is required"
			}
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusUnauthorized, code, message))
			return
		}
		if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusUnauthorized, 1002, "a Gateway API Key is required"))
			return
		}
		if !apiKeyScopeAllowed(user, "videos.generations") {
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusForbidden, 1103, "API Key scope does not allow video generation"))
			return
		}
		next.ServeHTTP(w, r.WithContext(auth.WithUser(r.Context(), user)))
	})
}

// createKelingOmniVideo godoc
// @Summary 创建 Kling Omni 视频任务
// @Description 兼容 Kling 旧版 /v1/videos/omni-video；Bearer token 必须为 Gateway API Key。任务固定异步执行，返回的 task_id 为网关任务 ID。
// @Tags kling-compatible
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body KelingOmniVideoRequest true "Kling Omni 官方兼容请求"
// @Success 200 {object} KelingCompatibleEnvelope
// @Failure 400 {object} KelingCompatibleEnvelope
// @Failure 401 {object} KelingCompatibleEnvelope
// @Failure 403 {object} KelingCompatibleEnvelope
// @Failure 429 {object} KelingCompatibleEnvelope
// @Failure 500 {object} KelingCompatibleEnvelope
// @Failure 503 {object} KelingCompatibleEnvelope
// @Router /api/v1/videos/omni-video [post]
func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
	requestID := kelingCompatRequestID(r)
	user, ok := auth.UserFromContext(r.Context())
	if !ok || user == nil {
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusUnauthorized, 1002, "Authorization is invalid"))
		return
	}
	body, err := s.decodeTaskRequestBody(r.Context(), w, r, "videos.generations")
	if err != nil {
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusBadRequest, 1201, err.Error()))
		return
	}
	normalized, compatErr := normalizeKelingOmniRequest(body)
	if compatErr != nil {
		writeKelingCompatError(w, requestID, compatErr)
		return
	}
	model := strings.TrimSpace(stringFromKelingCompat(normalized["model"]))
	if normalized["resolution"] == "2160p" {
		candidates, candidateErr := s.store.ListModelCandidates(r.Context(), model, "omni_video", user)
		if candidateErr != nil {
			writeKelingCompatError(w, requestID, kelingCompatGatewayError(candidateErr))
			return
		}
		if !kelingCompatCandidatesSupport4K(candidates) {
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusBadRequest, 1201, "mode=4k is not enabled by the selected model capabilities"))
			return
		}
	}
	task, createErr := s.prepareAndCreateGatewayTask(
		r.Context(),
		r,
		user,
		"videos.generations",
		model,
		normalized,
		true,
	)
	if createErr != nil {
		var staged *gatewayTaskCreationError
		if errors.As(createErr, &staged) && staged.Stage == gatewayTaskCreationPrepare {
			writeKelingCompatError(w, requestID, kelingCompatGatewayError(staged.Err))
			return
		}
		if errors.Is(createErr, store.ErrTaskRequestBinaryNotMaterialized) {
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusBadRequest, 1201, clients.ErrorCode(createErr)))
			return
		}
		s.logger.Error("create Kling-compatible task failed", "error", createErr)
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusInternalServerError, 5000, "create task failed"))
		return
	}
	if err := s.store.SetTaskCompatibilitySubmission(r.Context(), task.ID, store.CompatibilitySubmission{
		TargetProtocol: clients.ProtocolKlingV1Omni,
		PublicID:       task.ID,
	}); err != nil {
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusInternalServerError, 5000, "create compatibility metadata failed"))
		return
	}
	if err := s.runner.SubmitAsyncTask(r.Context(), task); err != nil {
		s.logger.Error("enqueue Kling-compatible task failed", "taskId", task.ID, "error", err)
		applyRunErrorHeaders(w, err)
		writeKelingCompatError(w, requestID, kelingCompatGatewayError(err))
		return
	}
	task, createErr = s.waitForCompatibilitySubmission(r, task)
	if createErr != nil {
		writeKelingCompatError(w, requestID, kelingCompatGatewayError(createErr))
		return
	}
	writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
		Code:      0,
		Message:   "SUCCEED",
		RequestID: requestID,
		Data:      kelingCompatSubmissionData(task),
	})
}

// getKelingOmniVideo godoc
// @Summary 查询 Kling Omni 视频任务
// @Description 按创建接口返回的网关 task_id 查询任务；仅允许创建任务的 Gateway 用户访问。
// @Tags kling-compatible
// @Produce json
// @Security BearerAuth
// @Param taskID path string true "网关任务 ID"
// @Success 200 {object} KelingCompatibleEnvelope
// @Failure 401 {object} KelingCompatibleEnvelope
// @Failure 403 {object} KelingCompatibleEnvelope
// @Failure 404 {object} KelingCompatibleEnvelope
// @Failure 410 {object} KelingCompatibleEnvelope
// @Failure 500 {object} KelingCompatibleEnvelope
// @Router /api/v1/videos/omni-video/{taskID} [get]
func (s *Server) getKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
	requestID := kelingCompatRequestID(r)
	user, ok := auth.UserFromContext(r.Context())
	if !ok || user == nil {
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusUnauthorized, 1002, "Authorization is invalid"))
		return
	}
	task, err := s.store.GetCompatibilityTask(r.Context(), clients.ProtocolKlingV1Omni, strings.TrimSpace(r.PathValue("taskID")))
	if err != nil && store.IsNotFound(err) {
		task, err = s.store.GetTask(r.Context(), strings.TrimSpace(r.PathValue("taskID")))
	}
	if err != nil {
		if store.IsNotFound(err) {
			writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusNotFound, 1203, "task not found"))
			return
		}
		s.logger.Error("get Kling-compatible task failed", "error", err)
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusInternalServerError, 5000, "query task failed"))
		return
	}
	if !kelingCompatTaskOwnedBy(task, user) || !isKelingCompatTask(task) {
		writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusNotFound, 1203, "task not found"))
		return
	}
	task, err = s.hydrateTaskResult(r.Context(), task)
	if err != nil {
		writeKelingCompatError(w, requestID, newKelingCompatError(statusFromRunError(err), 5000, clients.ErrorCode(err)))
		return
	}
	writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
		Code:      0,
		Message:   "SUCCEED",
		RequestID: requestID,
		Data:      kelingCompatTaskData(task),
	})
}

func normalizeKelingOmniRequest(input map[string]any) (map[string]any, *kelingCompatError) {
	if input == nil {
		input = map[string]any{}
	}
	if callbackURL := strings.TrimSpace(stringFromKelingCompat(input["callback_url"])); callbackURL != "" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "callback_url is not supported by this Gateway endpoint")
	}
	requestedModel := strings.TrimSpace(stringFromKelingCompat(input["model_name"]))
	if requestedModel == "" {
		requestedModel = "kling-video-o1"
	}
	model, maxDuration, ok := kelingCompatModel(requestedModel)
	if !ok {
		return nil, newKelingCompatError(http.StatusNotFound, 1203, "unsupported model_name: "+requestedModel)
	}

	mode := strings.ToLower(strings.TrimSpace(stringFromKelingCompat(input["mode"])))
	if mode == "" {
		mode = "pro"
	}
	resolutionByMode := map[string]string{"std": "720p", "pro": "1080p", "4k": "2160p"}
	resolution := resolutionByMode[mode]
	if resolution == "" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "mode must be std, pro, or 4k")
	}

	sound := strings.ToLower(strings.TrimSpace(stringFromKelingCompat(input["sound"])))
	if sound == "" {
		sound = "off"
	}
	if sound != "on" && sound != "off" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "sound must be on or off")
	}
	if model == klingO1Model && sound == "on" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "kling-video-o1 does not support generated audio; sound must be off")
	}

	content := make([]any, 0)
	prompt := strings.TrimSpace(stringFromKelingCompat(input["prompt"]))
	images, hasFirstFrame, imageErr := normalizeKelingImageList(input["image_list"])
	if imageErr != nil {
		return nil, imageErr
	}
	content = append(content, images...)
	elements, elementErr := normalizeKelingElementList(input["element_list"])
	if elementErr != nil {
		return nil, elementErr
	}
	content = append(content, elements...)
	videos, hasBaseVideo, hasVideo, videoErr := normalizeKelingVideoList(input["video_list"])
	if videoErr != nil {
		return nil, videoErr
	}
	content = append(content, videos...)
	if hasVideo && sound == "on" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "sound must be off when video_list is provided")
	}

	multiShot, multiShotPresent, boolErr := kelingCompatOptionalBool(input, "multi_shot")
	if boolErr != nil {
		return nil, boolErr
	}
	shotType := strings.ToLower(strings.TrimSpace(stringFromKelingCompat(input["shot_type"])))
	multiPrompts, shotDuration, multiPromptErr := normalizeKelingMultiPrompts(input["multi_prompt"])
	if multiPromptErr != nil {
		return nil, multiPromptErr
	}
	if !multiShotPresent {
		multiShot = false
	}
	if multiShot {
		if shotType != "customize" && shotType != "intelligence" {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "shot_type must be customize or intelligence when multi_shot is true")
		}
		if shotType == "customize" && len(multiPrompts) == 0 {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "multi_prompt is required for customized multi-shot generation")
		}
		if shotType == "intelligence" && len(multiPrompts) > 0 {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "multi_prompt is only supported when shot_type is customize")
		}
	} else if len(multiPrompts) > 0 || shotType != "" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "shot_type and multi_prompt require multi_shot=true")
	}
	if (len(multiPrompts) == 0 || shotType == "intelligence") && prompt == "" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "prompt is required")
	}
	if prompt != "" {
		content = append([]any{map[string]any{"type": "text", "text": prompt}}, content...)
	}
	content = append(content, multiPrompts...)

	duration, durationProvided, durationErr := kelingCompatOptionalInt(input, "duration")
	if durationErr != nil {
		return nil, durationErr
	}
	if hasBaseVideo {
		if durationProvided {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "duration is not supported for base video editing")
		}
	} else {
		if len(multiPrompts) > 0 {
			if durationProvided && duration != shotDuration {
				return nil, newKelingCompatError(http.StatusBadRequest, 1201, "duration must equal the sum of multi_prompt durations")
			}
			duration = shotDuration
		} else if !durationProvided {
			duration = 5
		}
		if duration < 3 || duration > maxDuration {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("duration for %s must be an integer between 3 and %d seconds", requestedModel, maxDuration))
		}
		if model == klingO1Model && (len(images) == 0 || hasFirstFrame) && duration != 5 && duration != 10 {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "kling-video-o1 text-to-video and first-frame generation only support 5 or 10 seconds")
		}
	}

	aspectRatio := strings.TrimSpace(stringFromKelingCompat(input["aspect_ratio"]))
	if aspectRatio != "" && aspectRatio != "16:9" && aspectRatio != "9:16" && aspectRatio != "1:1" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "aspect_ratio must be 16:9, 9:16, or 1:1")
	}
	if hasFirstFrame || hasBaseVideo {
		if aspectRatio != "" {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, "aspect_ratio is not supported with a first frame or base video")
		}
	} else if aspectRatio == "" {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, "aspect_ratio is required when no first frame or base video is provided")
	}

	watermarkEnabled, watermarkErr := kelingCompatWatermarkEnabled(input["watermark_info"])
	if watermarkErr != nil {
		return nil, watermarkErr
	}
	externalTaskID := strings.TrimSpace(stringFromKelingCompat(input["external_task_id"]))
	normalized := map[string]any{
		"model":                    model,
		"model_name":               requestedModel,
		"modelType":                "omni_video",
		"runMode":                  "real",
		"content":                  content,
		"resolution":               resolution,
		"mode":                     mode,
		"sound":                    sound,
		"audio":                    sound == "on",
		"multi_shot":               multiShot,
		"watermark":                watermarkEnabled,
		"watermark_info":           map[string]any{"enabled": watermarkEnabled},
		"external_task_id":         externalTaskID,
		"_gateway_compatibility":   kelingOmniCompatibilityMarker,
		"_gateway_target_protocol": clients.ProtocolKlingV1Omni,
	}
	if prompt != "" {
		normalized["prompt"] = prompt
	}
	if shotType != "" {
		normalized["shot_type"] = shotType
	}
	if !hasBaseVideo {
		normalized["duration"] = duration
	}
	if aspectRatio != "" {
		normalized["aspect_ratio"] = aspectRatio
	}
	return normalized, nil
}

func normalizeKelingImageList(value any) ([]any, bool, *kelingCompatError) {
	items, err := kelingCompatObjectList(value, "image_list")
	if err != nil {
		return nil, false, err
	}
	out := make([]any, 0, len(items))
	hasFirstFrame := false
	hasEndFrame := false
	for index, item := range items {
		url := strings.TrimSpace(stringFromKelingCompat(item["image_url"]))
		if url == "" {
			return nil, false, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("image_list[%d].image_url is required", index))
		}
		frameType := strings.TrimSpace(stringFromKelingCompat(item["type"]))
		role := "reference_image"
		switch frameType {
		case "":
		case "first_frame":
			role = "first_frame"
			hasFirstFrame = true
		case "end_frame":
			role = "last_frame"
			hasEndFrame = true
		default:
			return nil, false, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("image_list[%d].type must be first_frame or end_frame", index))
		}
		out = append(out, map[string]any{
			"type":      "image_url",
			"role":      role,
			"image_url": map[string]any{"url": url},
		})
	}
	if hasEndFrame && !hasFirstFrame {
		return nil, false, newKelingCompatError(http.StatusBadRequest, 1201, "end_frame requires first_frame")
	}
	return out, hasFirstFrame, nil
}

func normalizeKelingElementList(value any) ([]any, *kelingCompatError) {
	items, err := kelingCompatObjectList(value, "element_list")
	if err != nil {
		return nil, err
	}
	out := make([]any, 0, len(items))
	for index, item := range items {
		id := item["element_id"]
		if strings.TrimSpace(stringFromKelingCompat(id)) == "" {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("element_list[%d].element_id is required", index))
		}
		out = append(out, map[string]any{"type": "element", "element": map[string]any{"element_id": id}})
	}
	return out, nil
}

func normalizeKelingVideoList(value any) ([]any, bool, bool, *kelingCompatError) {
	items, err := kelingCompatObjectList(value, "video_list")
	if err != nil {
		return nil, false, false, err
	}
	if len(items) > 1 {
		return nil, false, false, newKelingCompatError(http.StatusBadRequest, 1201, "video_list supports at most one video")
	}
	out := make([]any, 0, len(items))
	hasBase := false
	for index, item := range items {
		url := strings.TrimSpace(stringFromKelingCompat(item["video_url"]))
		if url == "" {
			return nil, false, false, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("video_list[%d].video_url is required", index))
		}
		referType := strings.ToLower(strings.TrimSpace(stringFromKelingCompat(item["refer_type"])))
		if referType == "" {
			referType = "base"
		}
		if referType != "base" && referType != "feature" {
			return nil, false, false, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("video_list[%d].refer_type must be base or feature", index))
		}
		keepSound := strings.ToLower(strings.TrimSpace(stringFromKelingCompat(item["keep_original_sound"])))
		if keepSound != "" && keepSound != "yes" && keepSound != "no" {
			return nil, false, false, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("video_list[%d].keep_original_sound must be yes or no", index))
		}
		nested := map[string]any{"url": url, "refer_type": referType}
		if keepSound != "" {
			nested["keep_original_sound"] = keepSound
		}
		role := "video_feature"
		if referType == "base" {
			role = "video_base"
			hasBase = true
		}
		out = append(out, map[string]any{"type": "video_url", "role": role, "video_url": nested})
	}
	return out, hasBase, len(items) > 0, nil
}

func normalizeKelingMultiPrompts(value any) ([]any, int, *kelingCompatError) {
	items, err := kelingCompatObjectList(value, "multi_prompt")
	if err != nil {
		return nil, 0, err
	}
	if len(items) > 6 {
		return nil, 0, newKelingCompatError(http.StatusBadRequest, 1201, "multi_prompt supports at most six shots")
	}
	out := make([]any, 0, len(items))
	seen := map[int]bool{}
	total := 0
	for index, item := range items {
		shotIndex, ok := kelingCompatInt(item["index"])
		if !ok || shotIndex < 1 || shotIndex > 6 || seen[shotIndex] {
			return nil, 0, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("multi_prompt[%d].index must be a unique integer from 1 to 6", index))
		}
		seen[shotIndex] = true
		prompt := strings.TrimSpace(stringFromKelingCompat(item["prompt"]))
		if prompt == "" {
			return nil, 0, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("multi_prompt[%d].prompt is required", index))
		}
		duration, ok := kelingCompatInt(item["duration"])
		if !ok || duration < 1 {
			return nil, 0, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("multi_prompt[%d].duration must be an integer of at least 1 second", index))
		}
		total += duration
		out = append(out, map[string]any{
			"type":       "text",
			"role":       "shot_prompt",
			"shot_index": shotIndex,
			"text":       prompt,
			"duration":   duration,
		})
	}
	return out, total, nil
}

func kelingCompatModel(value string) (string, int, bool) {
	model, ok := canonicalKlingOmniModel(value)
	if !ok {
		return "", 0, false
	}
	if model == klingO1Model {
		return model, 10, true
	}
	return model, 15, true
}

func kelingCompatObjectList(value any, field string) ([]map[string]any, *kelingCompatError) {
	if value == nil {
		return nil, nil
	}
	raw, ok := value.([]any)
	if !ok {
		return nil, newKelingCompatError(http.StatusBadRequest, 1201, field+" must be an array")
	}
	out := make([]map[string]any, 0, len(raw))
	for index, item := range raw {
		object, ok := item.(map[string]any)
		if !ok {
			return nil, newKelingCompatError(http.StatusBadRequest, 1201, fmt.Sprintf("%s[%d] must be an object", field, index))
		}
		out = append(out, object)
	}
	return out, nil
}

func kelingCompatOptionalInt(body map[string]any, key string) (int, bool, *kelingCompatError) {
	value, present := body[key]
	if !present || value == nil || strings.TrimSpace(stringFromKelingCompat(value)) == "" {
		return 0, false, nil
	}
	parsed, ok := kelingCompatInt(value)
	if !ok {
		return 0, true, newKelingCompatError(http.StatusBadRequest, 1201, key+" must be an integer")
	}
	return parsed, true, nil
}

func kelingCompatOptionalBool(body map[string]any, key string) (bool, bool, *kelingCompatError) {
	value, present := body[key]
	if !present || value == nil {
		return false, false, nil
	}
	parsed, ok := value.(bool)
	if !ok {
		return false, true, newKelingCompatError(http.StatusBadRequest, 1201, key+" must be a boolean")
	}
	return parsed, true, nil
}

func kelingCompatWatermarkEnabled(value any) (bool, *kelingCompatError) {
	if value == nil {
		return false, nil
	}
	object, ok := value.(map[string]any)
	if !ok {
		return false, newKelingCompatError(http.StatusBadRequest, 1201, "watermark_info must be an object")
	}
	enabled, present := object["enabled"]
	if !present {
		return false, nil
	}
	result, ok := enabled.(bool)
	if !ok {
		return false, newKelingCompatError(http.StatusBadRequest, 1201, "watermark_info.enabled must be a boolean")
	}
	return result, nil
}

func kelingCompatInt(value any) (int, bool) {
	switch typed := value.(type) {
	case int:
		return typed, true
	case int64:
		return int(typed), true
	case float64:
		if math.Abs(typed-math.Round(typed)) > 1e-9 {
			return 0, false
		}
		return int(math.Round(typed)), true
	case json.Number:
		parsed, err := strconv.Atoi(typed.String())
		return parsed, err == nil
	case string:
		parsed, err := strconv.Atoi(strings.TrimSpace(typed))
		return parsed, err == nil
	default:
		return 0, false
	}
}

func stringFromKelingCompat(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	case json.Number:
		return typed.String()
	case float64:
		if math.Abs(typed-math.Round(typed)) < 1e-9 {
			return strconv.FormatInt(int64(math.Round(typed)), 10)
		}
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case int:
		return strconv.Itoa(typed)
	case int64:
		return strconv.FormatInt(typed, 10)
	default:
		return ""
	}
}

func kelingCompatTaskData(task store.GatewayTask) map[string]any {
	data := map[string]any{
		"task_id":     kelingCompatPublicID(task),
		"task_status": kelingCompatTaskStatus(task.Status),
		"task_info": map[string]any{
			"external_task_id": strings.TrimSpace(stringFromKelingCompat(task.Request["external_task_id"])),
		},
		"created_at": task.CreatedAt.UnixMilli(),
		"updated_at": task.UpdatedAt.UnixMilli(),
		"watermark_info": map[string]any{
			"enabled": kelingCompatTaskWatermark(task.Request),
		},
	}
	if kelingCompatTaskStatus(task.Status) == "failed" {
		standard := publicTaskError(task)
		data["task_status_msg"] = standard.Message
		data["task_status_code"] = kelingCompatBusinessCode(standard.Code, standard.Message)
		data["error"] = standard
	} else if message := kelingCompatTaskMessage(task); message != "" {
		data["task_status_msg"] = message
	}
	if videos := kelingCompatTaskVideos(task.Result); len(videos) > 0 {
		data["task_result"] = map[string]any{"videos": videos}
	}
	return data
}

func kelingCompatSubmissionData(task store.GatewayTask) map[string]any {
	data := kelingCompatTaskData(task)
	data["task_status"] = "submitted"
	delete(data, "task_status_code")
	delete(data, "task_status_msg")
	delete(data, "task_result")
	return data
}

func kelingCompatPublicID(task store.GatewayTask) string {
	if task.CompatibilityProtocol == task.CompatibilitySourceProtocol {
		if remoteTaskID := strings.TrimSpace(task.RemoteTaskID); remoteTaskID != "" {
			return remoteTaskID
		}
	}
	return task.ID
}

func kelingCompatTaskStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "succeeded", "success", "completed":
		return "succeed"
	case "failed", "cancelled", "canceled":
		return "failed"
	case "running", "processing":
		return "processing"
	default:
		return "submitted"
	}
}

func kelingCompatTaskVideos(result map[string]any) []any {
	raw, _ := result["data"].([]any)
	out := make([]any, 0, len(raw))
	for _, itemValue := range raw {
		item, _ := itemValue.(map[string]any)
		if item == nil {
			continue
		}
		url := strings.TrimSpace(stringFromKelingCompat(firstKelingCompatValue(item["url"], item["video_url"])))
		if url == "" {
			continue
		}
		video := map[string]any{"url": url}
		if id := strings.TrimSpace(stringFromKelingCompat(item["id"])); id != "" {
			video["id"] = id
		}
		if watermarkURL := strings.TrimSpace(stringFromKelingCompat(item["watermark_url"])); watermarkURL != "" {
			video["watermark_url"] = watermarkURL
		}
		if duration := strings.TrimSpace(stringFromKelingCompat(item["duration"])); duration != "" {
			video["duration"] = duration
		}
		out = append(out, video)
	}
	return out
}

func firstKelingCompatValue(values ...any) any {
	for _, value := range values {
		if strings.TrimSpace(stringFromKelingCompat(value)) != "" {
			return value
		}
	}
	return nil
}

func kelingCompatTaskMessage(task store.GatewayTask) string {
	if task.Status == "failed" || task.Status == "cancelled" || task.ErrorCode != "" || task.ErrorMessage != "" || task.Error != "" {
		return publicTaskError(task).Message
	}
	return strings.TrimSpace(firstNonEmpty(task.ErrorMessage, task.Error, task.Message))
}

func kelingCompatTaskWatermark(request map[string]any) bool {
	if value, ok := request["watermark"].(bool); ok {
		return value
	}
	if info, ok := request["watermark_info"].(map[string]any); ok {
		value, _ := info["enabled"].(bool)
		return value
	}
	return false
}

func kelingCompatCandidatesSupport4K(candidates []store.RuntimeModelCandidate) bool {
	for _, candidate := range candidates {
		if strings.ToLower(strings.TrimSpace(candidate.Provider)) != "keling" {
			continue
		}
		capability, _ := candidate.Capabilities["omni_video"].(map[string]any)
		if capability == nil {
			capability, _ = candidate.Capabilities["omni"].(map[string]any)
		}
		for _, resolution := range kelingCompatStringList(capability["output_resolutions"]) {
			switch strings.ToLower(strings.TrimSpace(resolution)) {
			case "2160p", "4k":
				return true
			}
		}
	}
	return false
}

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

func kelingCompatGatewayError(err error) *kelingCompatError {
	if err == nil {
		return newKelingCompatError(http.StatusInternalServerError, 5000, "unknown gateway error")
	}
	codeText := clients.ErrorCode(err)
	status := statusFromRunError(err)
	standard := publicerror.FromFields(codeText, err.Error(), status, clients.IsRetryable(err))
	publicerror.Observe(standard)
	businessCode := kelingCompatBusinessCode(standard.Code, standard.Message)
	status = standard.HTTPStatus
	switch businessCode {
	case 1101:
		status = http.StatusPaymentRequired
	case 1103:
		status = http.StatusForbidden
	case 1201:
		status = http.StatusBadRequest
	case 1203:
		status = http.StatusNotFound
	case 1302, 1303:
		status = http.StatusTooManyRequests
	case 5001:
		status = http.StatusBadGateway
	}
	result := newKelingCompatError(status, businessCode, standard.Message)
	result.PublicError = &standard
	return result
}

func kelingCompatBusinessCode(errorCode string, message string) int {
	combined := strings.ToLower(strings.TrimSpace(errorCode + " " + message))
	containsAny := func(values ...string) bool {
		for _, value := range values {
			if strings.Contains(combined, value) {
				return true
			}
		}
		return false
	}
	switch {
	case containsAny("insufficient_balance", "insufficient balance", "balance_not_enough", "wallet balance", "余额不足", "欠费", "quota exceeded"):
		return 1101
	case containsAny("permission_denied", "permission denied", "forbidden", "access denied", "scope does not allow"):
		return 1103
	case containsAny("concurrent", "concurrency"):
		return 1303
	case containsAny("rate_limit", "rate limit", "too many requests", "rpm", "tpm"):
		return 1302
	case containsAny("no_model_candidate", "no model candidate", "model_not_found", "unsupported model", "resource not found"):
		return 1203
	case containsAny("invalid_parameter", "invalid parameter", "bad_request", "parameter_preprocessing", "duration", "aspect_ratio"):
		return 1201
	case containsAny("upload_", "request_asset_", "network", "timeout", "upstream", "service unavailable", "bad gateway"):
		return 5001
	default:
		return 5000
	}
}

func isKelingCompatTask(task store.GatewayTask) bool {
	return task.Kind == "videos.generations" && strings.TrimSpace(stringFromKelingCompat(task.Request["_gateway_compatibility"])) == kelingOmniCompatibilityMarker
}

func kelingCompatTaskOwnedBy(task store.GatewayTask, user *auth.User) bool {
	if user == nil {
		return false
	}
	taskOwner := strings.TrimSpace(firstNonEmpty(task.GatewayUserID, task.UserID))
	requestOwner := strings.TrimSpace(firstNonEmpty(user.GatewayUserID, user.ID))
	return taskOwner != "" && requestOwner != "" && taskOwner == requestOwner
}

func newKelingCompatRequestID(r *http.Request) string {
	if r != nil {
		if value := strings.TrimSpace(firstNonEmpty(r.Header.Get("X-Request-ID"), r.Header.Get("X-Request-Id"))); value != "" {
			return value
		}
	}
	random := make([]byte, 16)
	if _, err := rand.Read(random); err == nil {
		return hex.EncodeToString(random)
	}
	return strconv.FormatInt(time.Now().UnixNano(), 36)
}

func kelingCompatRequestID(r *http.Request) string {
	if r != nil {
		if value, ok := r.Context().Value(kelingCompatRequestIDKey{}).(string); ok && strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return newKelingCompatRequestID(r)
}

func writeKelingCompatError(w http.ResponseWriter, requestID string, err *kelingCompatError) {
	if err == nil {
		err = newKelingCompatError(http.StatusInternalServerError, 5000, "internal error")
	}
	if err.RequestID != "" {
		requestID = err.RequestID
	}
	if requestID == "" {
		requestID = newKelingCompatRequestID(nil)
	}
	status := err.HTTPStatus
	if status == 0 {
		status = http.StatusInternalServerError
	}
	standard := err.PublicError
	if standard == nil {
		value := publicerror.FromFields("", err.Message, status, status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500)
		value = publicerror.WithIDs(value, requestID, "")
		publicerror.Observe(value)
		standard = &value
	}
	status = standard.HTTPStatus
	writeJSON(w, status, KelingCompatibleEnvelope{
		Code:      err.Code,
		Message:   standard.Message,
		RequestID: requestID,
		Error:     standard,
	})
}
