package httpapi

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"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 (
	klingCompatProvider = "kling"
	klingO1Model        = "kling-video-o1"
	klingV3OmniModel    = "kling-v3-omni"
)

func (s *Server) registerKlingCompatibilityRoutes(mux *http.ServeMux) {
	handler := func(next http.HandlerFunc) http.Handler {
		return s.requireProtocolUser(clients.ProtocolKlingV1Omni, http.HandlerFunc(next))
	}
	// /api/v1 is the canonical public prefix. The historical /kling paths
	// remain registered below so existing clients can migrate without downtime.
	mux.Handle("POST /api/v1/kling/v1/videos/omni-video", handler(s.klingV1CreateOmniVideo))
	mux.Handle("GET /api/v1/kling/v1/videos/omni-video", handler(s.klingV1ListOmniVideos))
	mux.Handle("GET /api/v1/kling/v1/videos/omni-video/{taskID}", handler(s.klingV1GetOmniVideo))
	mux.Handle("POST /api/v1/kling/v2/omni-video/{model}", handler(s.klingV2CreateOmniVideo))
	mux.Handle("GET /api/v1/kling/v2/tasks", handler(s.klingV2GetTasks))
	mux.Handle("POST /api/v1/kling/v2/tasks", handler(s.klingV2ListTasks))

	mux.Handle("POST /kling/v1/videos/omni-video", handler(s.klingV1CreateOmniVideo))
	mux.Handle("GET /kling/v1/videos/omni-video", handler(s.klingV1ListOmniVideos))
	mux.Handle("GET /kling/v1/videos/omni-video/{taskID}", handler(s.klingV1GetOmniVideo))

	// Kling API 2.0 uses model-specific paths and a shared /tasks resource.
	mux.Handle("POST /kling/omni-video/{model}", handler(s.klingV2CreateOmniVideo))
	mux.Handle("GET /kling/tasks", handler(s.klingV2GetTasks))
	mux.Handle("POST /kling/tasks", handler(s.klingV2ListTasks))
	// Versioned aliases help clients that keep the protocol version in their base path.
	mux.Handle("POST /kling/v2/omni-video/{model}", handler(s.klingV2CreateOmniVideo))
	mux.Handle("GET /kling/v2/tasks", handler(s.klingV2GetTasks))
	mux.Handle("POST /kling/v2/tasks", handler(s.klingV2ListTasks))
}

// klingV1CreateOmniVideo godoc
// @Summary 创建可灵 V1 Omni 视频任务
// @Description 兼容中国区可灵 V1 /v1/videos/omni-video；用户使用网关 API Key，网关在服务端使用 AK/SK 调用上游。
// @Tags kling-compatible
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body map[string]interface{} true "可灵 V1 Omni 请求"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 401 {object} KlingErrorEnvelope
// @Router /api/v1/kling/v1/videos/omni-video [post]
func (s *Server) klingV1CreateOmniVideo(w http.ResponseWriter, r *http.Request) {
	var native map[string]any
	if err := decodeKlingJSON(r, &native); err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid json body", "invalid_json_body")
		return
	}
	model := strings.TrimSpace(stringFromRequestAny(native["model_name"]))
	if model == "" {
		model = klingO1Model
	}
	s.createKlingCompatTask(w, r, "v1", model, native)
}

// klingV2CreateOmniVideo godoc
// @Summary 创建可灵 API 2.0 Omni 视频任务
// @Description 兼容可灵 API 2.0 的模型路径；调用方使用网关 API Key，网关转换并使用中国区 V1 AK/SK 上游。
// @Tags kling-compatible
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param model path string true "模型路径（kling-o1 或 kling-v3-omni）"
// @Param input body map[string]interface{} true "可灵 API 2.0 请求"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 401 {object} KlingErrorEnvelope
// @Router /api/v1/kling/v2/omni-video/{model} [post]
func (s *Server) klingV2CreateOmniVideo(w http.ResponseWriter, r *http.Request) {
	model, ok := klingV2ProviderModel(r.PathValue("model"))
	if !ok {
		writeKlingCompatError(w, http.StatusNotFound, "unsupported Kling Omni model", "model_not_found")
		return
	}
	var native map[string]any
	if err := decodeKlingJSON(r, &native); err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid json body", "invalid_json_body")
		return
	}
	s.createKlingCompatTask(w, r, "v2", model, native)
}

func (s *Server) createKlingCompatTask(w http.ResponseWriter, r *http.Request, version string, model string, native map[string]any) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeKlingCompatError(w, http.StatusUnauthorized, "unauthorized", "unauthorized")
		return
	}
	if !apiKeyScopeAllowed(user, "videos.generations") {
		writeKlingCompatError(w, http.StatusForbidden, "api key scope does not allow video generation", "permission_denied")
		return
	}
	admission, err := s.authorizeTaskTraffic(r, user)
	if err != nil {
		targetProtocol := clients.ProtocolKlingV1Omni
		if version == "v2" {
			targetProtocol = clients.ProtocolKlingV2Omni
		}
		writeTaskTrafficError(w, err, targetProtocol)
		return
	}
	body, externalTaskID, err := klingCompatTaskBody(version, model, native)
	if err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, err.Error(), clients.ErrorCode(err))
		return
	}
	prepared, err := s.prepareTaskRequest(r.Context(), r, user, body)
	if err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, err.Error(), clients.ErrorCode(err))
		return
	}
	runMode, err := s.admittedTaskRunMode(admission, prepared.Body)
	if err != nil {
		targetProtocol := clients.ProtocolKlingV1Omni
		if version == "v2" {
			targetProtocol = clients.ProtocolKlingV2Omni
		}
		writeTaskTrafficError(w, err, targetProtocol)
		return
	}
	createInput := store.CreateTaskInput{
		Kind:            "videos.generations",
		Model:           model,
		ExternalTaskID:  externalTaskID,
		RunMode:         runMode,
		AcceptanceRunID: admission.AcceptanceRunID,
		Async:           true,
		Request:         prepared.Body,
	}
	if idempotencyKey, hasKey, keyErr := optionalTaskIdempotencyKey(r); keyErr != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid Idempotency-Key", "invalid_idempotency_key")
		return
	} else if hasKey {
		createInput.IdempotencyKeyHash = taskIdempotencyKeyHash(idempotencyKey)
		createInput.IdempotencyRequestHash = taskIdempotencyRequestHash(createInput.Kind, true, false, prepared.Body)
	}
	created, err := s.store.CreateTaskIdempotent(r.Context(), createInput, user)
	if err != nil {
		switch {
		case errors.Is(err, store.ErrRateLimited):
			applyRunErrorHeaders(w, err)
			writeKlingCompatError(w, statusFromRunError(err), runErrorMessage(err), runErrorCode(err))
		case errors.Is(err, store.ErrIdempotencyKeyReused):
			writeKlingCompatError(w, http.StatusConflict, err.Error(), "idempotency_key_reused")
		case errors.Is(err, store.ErrExternalTaskIDReused):
			writeKlingCompatError(w, http.StatusConflict, err.Error(), "external_task_id_reused")
		case errors.Is(err, store.ErrTaskRequestBinaryNotMaterialized):
			writeKlingCompatError(w, http.StatusBadRequest, err.Error(), clients.ErrorCode(err))
		default:
			s.logger.Error("create Kling compatibility task failed", "version", version, "model", model, "error", err)
			writeKlingCompatError(w, http.StatusInternalServerError, "create task failed", "task_create_failed")
		}
		return
	}
	task := created.Task
	targetProtocol := clients.ProtocolKlingV1Omni
	if version == "v2" {
		targetProtocol = clients.ProtocolKlingV2Omni
	}
	if !created.Replayed {
		if err := s.store.SetTaskCompatibilitySubmission(r.Context(), task.ID, store.CompatibilitySubmission{TargetProtocol: targetProtocol, PublicID: task.ID}); err != nil {
			writeKlingCompatError(w, http.StatusInternalServerError, "create compatibility metadata failed", "task_create_failed")
			return
		}
		if err := s.runner.SubmitAsyncTask(r.Context(), task); err != nil {
			applyRunErrorHeaders(w, err)
			writeKlingCompatError(w, statusFromRunError(err), runErrorMessage(err), runErrorCode(err))
			return
		}
		task, err = s.waitForCompatibilitySubmission(r, task)
		if err != nil {
			writeKlingCompatError(w, statusFromRunError(err), err.Error(), clients.ErrorCode(err))
			return
		}
	} else {
		w.Header().Set("Idempotent-Replayed", "true")
	}
	if version == "v2" {
		writeJSON(w, http.StatusOK, klingV2SubmissionEnvelope(task))
		return
	}
	writeJSON(w, http.StatusOK, klingV1SubmissionEnvelope(task))
}

func klingCompatTaskBody(version string, model string, native map[string]any) (map[string]any, string, error) {
	if model != klingO1Model && model != klingV3OmniModel {
		return nil, "", &clients.ClientError{Code: "invalid_parameter", Message: "model_name must be kling-video-o1 or kling-v3-omni", StatusCode: 400, Retryable: false}
	}
	if native == nil {
		native = map[string]any{}
	}
	body := cloneMap(native)
	if version == "v2" {
		body = klingV2ToLegacyBody(native)
	}
	body["model"] = model
	body["modelType"] = "omni_video"
	body["_compat_provider"] = klingCompatProvider
	body["_kling_compat_version"] = version
	if version == "v2" {
		body["_gateway_target_protocol"] = clients.ProtocolKlingV2Omni
	} else {
		body["_gateway_target_protocol"] = clients.ProtocolKlingV1Omni
	}
	body["content"] = klingLegacyContent(body)
	mode := strings.TrimSpace(stringFromRequestAny(body["mode"]))
	if mode == "" && version == "v1" {
		// The legacy Omni API defaults to professional (1080p) mode.
		mode = "pro"
		body["mode"] = mode
	}
	if mode != "" {
		resolution, ok := klingResolutionFromMode(mode)
		if !ok {
			return nil, "", &clients.ClientError{Code: "invalid_parameter", Message: "mode must be std, pro, or 4k", StatusCode: 400, Retryable: false}
		}
		body["resolution"] = resolution
	}
	if strings.EqualFold(strings.TrimSpace(stringFromRequestAny(body["sound"])), "on") {
		body["audio"] = true
	}
	externalTaskID := strings.TrimSpace(stringFromRequestAny(body["external_task_id"]))
	if err := validateKlingCompatBody(model, body); err != nil {
		return nil, "", err
	}
	return body, externalTaskID, nil
}

func klingV2ToLegacyBody(native map[string]any) map[string]any {
	body := map[string]any{}
	for _, key := range []string{"runMode", "simulation", "simulationDurationMs", "simulationProfile"} {
		if value, ok := native[key]; ok {
			body[key] = value
		}
	}
	settings, _ := native["settings"].(map[string]any)
	options, _ := native["options"].(map[string]any)
	if settings == nil {
		settings = map[string]any{}
	}
	if options == nil {
		options = map[string]any{}
	}
	if resolution := strings.TrimSpace(stringFromRequestAny(settings["resolution"])); resolution != "" {
		switch strings.ToLower(resolution) {
		case "720p":
			body["mode"] = "std"
		case "1080p":
			body["mode"] = "pro"
		case "4k", "2160p":
			body["mode"] = "4k"
		default:
			body["mode"] = resolution
		}
	}
	for _, key := range []string{"aspect_ratio", "duration", "multi_shot", "shot_type", "multi_prompt"} {
		if value, ok := settings[key]; ok {
			body[key] = value
		}
	}
	audio := strings.ToLower(strings.TrimSpace(stringFromRequestAny(settings["audio"])))
	if audio == "native" || audio == "on" {
		body["sound"] = "on"
	} else {
		body["sound"] = "off"
	}
	for _, key := range []string{"callback_url", "external_task_id", "watermark_info"} {
		if value, ok := options[key]; ok {
			body[key] = value
		}
	}
	contents, _ := native["contents"].([]any)
	imageList := make([]any, 0)
	videoList := make([]any, 0)
	elementList := make([]any, 0)
	for _, raw := range contents {
		item, _ := raw.(map[string]any)
		kind := strings.ToLower(strings.TrimSpace(stringFromRequestAny(item["type"])))
		switch kind {
		case "prompt":
			body["prompt"] = stringFromRequestAny(item["text"])
		case "first_frame", "last_frame", "refer_image", "reference_image":
			image := map[string]any{"image_url": firstNonEmptyRequestString(item, "url", "image_url")}
			if kind == "first_frame" {
				image["type"] = "first_frame"
			} else if kind == "last_frame" {
				image["type"] = "end_frame"
			}
			imageList = append(imageList, image)
		case "feature_video", "base_video", "refer_video", "reference_video":
			referType := "feature"
			if kind == "base_video" {
				referType = "base"
			}
			video := map[string]any{
				"video_url":           firstNonEmptyRequestString(item, "url", "video_url"),
				"refer_type":          referType,
				"keep_original_sound": firstNonEmptyRequestString(item, "keep_original_sound", "keepOriginalSound"),
			}
			if audio == "original" && video["keep_original_sound"] == "" {
				video["keep_original_sound"] = "yes"
			}
			videoList = append(videoList, video)
		case "element":
			elementList = append(elementList, map[string]any{"element_id": firstPresentRequest(item["element_id"], item["id"])})
		}
	}
	if len(imageList) > 0 {
		body["image_list"] = imageList
	}
	if len(videoList) > 0 {
		body["video_list"] = videoList
	}
	if len(elementList) > 0 {
		body["element_list"] = elementList
	}
	return body
}

func klingLegacyContent(body map[string]any) []any {
	content := make([]any, 0)
	if prompt := strings.TrimSpace(stringFromRequestAny(body["prompt"])); prompt != "" {
		content = append(content, map[string]any{"type": "text", "text": prompt})
	}
	for _, raw := range mapListFromRequest(body["image_list"]) {
		role := "reference_image"
		switch strings.TrimSpace(stringFromRequestAny(raw["type"])) {
		case "first_frame":
			role = "first_frame"
		case "end_frame", "last_frame":
			role = "last_frame"
		}
		content = append(content, map[string]any{
			"type": "image_url", "role": role,
			"image_url": map[string]any{"url": firstNonEmptyRequestString(raw, "image_url", "url")},
		})
	}
	for _, raw := range mapListFromRequest(body["video_list"]) {
		referType := firstNonEmptyRequestString(raw, "refer_type", "referType")
		role := "video_feature"
		if referType == "base" {
			role = "video_base"
		}
		content = append(content, map[string]any{
			"type": "video_url", "role": role,
			"video_url": map[string]any{
				"url":                 firstNonEmptyRequestString(raw, "video_url", "url"),
				"refer_type":          referType,
				"keep_original_sound": firstNonEmptyRequestString(raw, "keep_original_sound", "keepOriginalSound"),
			},
		})
	}
	for _, raw := range mapListFromRequest(body["element_list"]) {
		content = append(content, map[string]any{
			"type":    "element",
			"element": map[string]any{"element_id": firstPresentRequest(raw["element_id"], raw["id"])},
		})
	}
	return content
}

func validateKlingCompatBody(model string, body map[string]any) error {
	maxDuration := 10
	if model == klingV3OmniModel {
		maxDuration = 15
	}
	if duration, ok := klingCompatInt(body["duration"]); ok && (duration < 3 || duration > maxDuration) {
		return &clients.ClientError{Code: "invalid_parameter", Message: fmt.Sprintf("duration must be between 3 and %d seconds for %s", maxDuration, model), StatusCode: 400, Retryable: false}
	}
	if model == klingO1Model && boolFromRequestAny(body["multi_shot"]) {
		return &clients.ClientError{Code: "invalid_parameter", Message: "kling-video-o1 does not support multi_shot", StatusCode: 400, Retryable: false}
	}
	if model == klingO1Model && strings.EqualFold(strings.TrimSpace(stringFromRequestAny(body["mode"])), "4k") {
		return &clients.ClientError{Code: "invalid_parameter", Message: "kling-video-o1 does not support 4k mode", StatusCode: 400, Retryable: false}
	}
	if ratio := strings.TrimSpace(stringFromRequestAny(body["aspect_ratio"])); ratio != "" && ratio != "16:9" && ratio != "9:16" && ratio != "1:1" {
		return &clients.ClientError{Code: "invalid_parameter", Message: "aspect_ratio must be 16:9, 9:16, or 1:1", StatusCode: 400, Retryable: false}
	}
	if sound := strings.ToLower(strings.TrimSpace(stringFromRequestAny(body["sound"]))); sound != "" && sound != "on" && sound != "off" {
		return &clients.ClientError{Code: "invalid_parameter", Message: "sound must be on or off", StatusCode: 400, Retryable: false}
	}
	prompt := strings.TrimSpace(stringFromRequestAny(body["prompt"]))
	if utf8.RuneCountInString(prompt) > 2500 {
		return &clients.ClientError{Code: "invalid_parameter", Message: "prompt must not exceed 2500 characters", StatusCode: 400, Retryable: false}
	}
	images := mapListFromRequest(body["image_list"])
	videos := mapListFromRequest(body["video_list"])
	elements := mapListFromRequest(body["element_list"])
	for _, image := range images {
		if firstNonEmptyRequestString(image, "image_url", "url") == "" {
			return &clients.ClientError{Code: "invalid_parameter", Message: "every image_list item requires image_url", StatusCode: 400, Retryable: false}
		}
	}
	for _, video := range videos {
		if firstNonEmptyRequestString(video, "video_url", "url") == "" {
			return &clients.ClientError{Code: "invalid_parameter", Message: "every video_list item requires video_url", StatusCode: 400, Retryable: false}
		}
		if referType := strings.TrimSpace(firstNonEmptyRequestString(video, "refer_type", "referType")); referType != "" && referType != "base" && referType != "feature" {
			return &clients.ClientError{Code: "invalid_parameter", Message: "video refer_type must be base or feature", StatusCode: 400, Retryable: false}
		}
	}
	for _, element := range elements {
		if klingStringAny(firstPresentRequest(element["element_id"], element["id"])) == "" {
			return &clients.ClientError{Code: "invalid_parameter", Message: "every element_list item requires element_id", StatusCode: 400, Retryable: false}
		}
	}
	if model == klingO1Model && len(images) == 0 && len(videos) == 0 && len(elements) == 0 {
		if duration, ok := klingCompatInt(body["duration"]); ok && duration != 5 && duration != 10 {
			return &clients.ClientError{Code: "invalid_parameter", Message: "kling-video-o1 text-only generation supports duration 5 or 10 seconds", StatusCode: 400, Retryable: false}
		}
	}
	if len(videos) > 1 {
		return &clients.ClientError{Code: "invalid_parameter", Message: "video_list supports at most one video", StatusCode: 400, Retryable: false}
	}
	if len(images)+len(elements) > 7 {
		return &clients.ClientError{Code: "invalid_parameter", Message: "image_list and element_list support at most seven combined references", StatusCode: 400, Retryable: false}
	}
	if len(videos) > 0 && len(images)+len(elements) > 4 {
		return &clients.ClientError{Code: "invalid_parameter", Message: "requests with video input support at most four image and element references", StatusCode: 400, Retryable: false}
	}
	if len(videos) > 0 && strings.EqualFold(strings.TrimSpace(stringFromRequestAny(body["sound"])), "on") {
		return &clients.ClientError{Code: "invalid_parameter", Message: "sound must be off when video_list is provided", StatusCode: 400, Retryable: false}
	}
	if model == klingV3OmniModel && len(videos) > 0 {
		if duration, ok := klingCompatInt(body["duration"]); ok && duration > 10 {
			return &clients.ClientError{Code: "invalid_parameter", Message: "kling-v3-omni video-reference generation supports at most 10 seconds", StatusCode: 400, Retryable: false}
		}
	}
	multiShot := boolFromRequestAny(body["multi_shot"])
	shotType := strings.ToLower(strings.TrimSpace(stringFromRequestAny(body["shot_type"])))
	if shotType != "" && shotType != "customize" && shotType != "intelligence" {
		return &clients.ClientError{Code: "invalid_parameter", Message: "shot_type must be customize or intelligence", StatusCode: 400, Retryable: false}
	}
	if multiShot && shotType == "customize" {
		multiPrompt := mapListFromRequest(body["multi_prompt"])
		if len(multiPrompt) == 0 || len(multiPrompt) > 6 {
			return &clients.ClientError{Code: "invalid_parameter", Message: "customize multi-shot requires between one and six multi_prompt items", StatusCode: 400, Retryable: false}
		}
		totalDuration := 0
		for _, shot := range multiPrompt {
			shotPrompt := strings.TrimSpace(stringFromRequestAny(shot["prompt"]))
			shotDuration, ok := klingCompatInt(shot["duration"])
			if shotPrompt == "" || utf8.RuneCountInString(shotPrompt) > 2500 || !ok || shotDuration <= 0 {
				return &clients.ClientError{Code: "invalid_parameter", Message: "every multi_prompt item requires prompt and a positive integer duration", StatusCode: 400, Retryable: false}
			}
			totalDuration += shotDuration
		}
		if totalDuration < 3 || totalDuration > maxDuration {
			return &clients.ClientError{Code: "invalid_parameter", Message: fmt.Sprintf("multi_prompt duration must total between 3 and %d seconds", maxDuration), StatusCode: 400, Retryable: false}
		}
	}
	if (!multiShot || shotType == "intelligence" || shotType == "") && prompt == "" {
		return &clients.ClientError{Code: "invalid_parameter", Message: "prompt is required for single-shot and intelligence multi-shot generation", StatusCode: 400, Retryable: false}
	}
	return nil
}

// klingV1GetOmniVideo godoc
// @Summary 查询可灵 V1 Omni 视频任务
// @Tags kling-compatible
// @Produce json
// @Security BearerAuth
// @Param taskID path string true "任务 ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 410 {object} map[string]interface{}
// @Router /api/v1/kling/v1/videos/omni-video/{taskID} [get]
func (s *Server) klingV1GetOmniVideo(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	task, err := s.store.GetCompatTask(r.Context(), user, klingCompatProvider, "v1", r.PathValue("taskID"))
	if err != nil {
		if store.IsNotFound(err) {
			writeKlingCompatError(w, http.StatusNotFound, "task not found", "task_not_found")
			return
		}
		writeKlingCompatError(w, http.StatusInternalServerError, "get task failed", "task_query_failed")
		return
	}
	task, err = s.hydrateTaskResult(r.Context(), task)
	if err != nil {
		writeKlingCompatError(w, statusFromRunError(err), err.Error(), clients.ErrorCode(err))
		return
	}
	writeJSON(w, http.StatusOK, klingV1Envelope(task))
}

// klingV1ListOmniVideos godoc
// @Summary 分页查询可灵 V1 Omni 视频任务
// @Tags kling-compatible
// @Produce json
// @Security BearerAuth
// @Param pageNum query int false "页码" default(1)
// @Param pageSize query int false "每页数量" default(30)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/kling/v1/videos/omni-video [get]
func (s *Server) klingV1ListOmniVideos(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	page, err := positiveQueryInt(r.URL.Query().Get("pageNum"), 1)
	if err != nil || page > 1000 {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid pageNum", "invalid_parameter")
		return
	}
	pageSize, err := positiveQueryInt(r.URL.Query().Get("pageSize"), 30)
	if err != nil || pageSize > 500 {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid pageSize", "invalid_parameter")
		return
	}
	result, err := s.store.ListCompatTasks(r.Context(), user, store.CompatTaskListFilter{Provider: klingCompatProvider, Version: "v1", Page: page, PageSize: pageSize})
	if err != nil {
		writeKlingCompatError(w, http.StatusInternalServerError, "list tasks failed", "task_query_failed")
		return
	}
	data := make([]any, 0, len(result.Items))
	for _, task := range result.Items {
		data = append(data, klingV1TaskData(task))
	}
	writeJSON(w, http.StatusOK, map[string]any{"code": 0, "message": "success", "request_id": klingRequestID(result.Items), "data": data})
}

// klingV2GetTasks godoc
// @Summary 按 ID 查询可灵 API 2.0 任务
// @Tags kling-compatible
// @Produce json
// @Security BearerAuth
// @Param task_ids query string false "逗号分隔的任务 ID"
// @Param external_task_ids query string false "逗号分隔的外部任务 ID"
// @Success 200 {object} map[string]interface{}
// @Failure 410 {object} map[string]interface{}
// @Router /api/v1/kling/v2/tasks [get]
func (s *Server) klingV2GetTasks(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	taskIDs := splitKlingIDs(r.URL.Query().Get("task_ids"))
	externalIDs := splitKlingIDs(r.URL.Query().Get("external_task_ids"))
	if (len(taskIDs) == 0) == (len(externalIDs) == 0) {
		writeKlingCompatError(w, http.StatusBadRequest, "choose exactly one of task_ids or external_task_ids", "invalid_parameter")
		return
	}
	identifiers := taskIDs
	if len(externalIDs) > 0 {
		identifiers = externalIDs
	}
	data := make([]any, 0, len(identifiers))
	for _, identifier := range identifiers {
		task, err := s.store.GetCompatTask(r.Context(), user, klingCompatProvider, "v2", identifier)
		if err != nil {
			if store.IsNotFound(err) {
				continue
			}
			writeKlingCompatError(w, http.StatusInternalServerError, "get task failed", "task_query_failed")
			return
		}
		task, err = s.hydrateTaskResult(r.Context(), task)
		if err != nil {
			writeKlingCompatError(w, statusFromRunError(err), err.Error(), clients.ErrorCode(err))
			return
		}
		data = append(data, klingV2TaskData(task))
	}
	writeJSON(w, http.StatusOK, map[string]any{"code": 0, "message": "success", "request_id": klingRequestIDFromAny(data), "data": data})
}

// klingV2ListTasks godoc
// @Summary 分页查询可灵 API 2.0 任务
// @Tags kling-compatible
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body map[string]interface{} true "游标、数量、时间范围和筛选条件"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/kling/v2/tasks [post]
func (s *Server) klingV2ListTasks(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	var body map[string]any
	if err := decodeKlingJSON(r, &body); err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid json body", "invalid_json_body")
		return
	}
	page, err := klingCursorPage(stringFromRequestAny(body["cursor"]))
	if err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid cursor", "invalid_parameter")
		return
	}
	limit, ok := klingCompatInt(body["limit"])
	if !ok || limit <= 0 {
		limit = 100
	}
	if limit > 500 {
		writeKlingCompatError(w, http.StatusBadRequest, "limit must not exceed 500", "invalid_parameter")
		return
	}
	createdFrom, err := klingMillisTime(body["start_time"])
	if err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid start_time", "invalid_parameter")
		return
	}
	createdTo, err := klingMillisTime(body["end_time"])
	if err != nil {
		writeKlingCompatError(w, http.StatusBadRequest, "invalid end_time", "invalid_parameter")
		return
	}
	statuses := klingInternalStatuses(body["filters"])
	result, err := s.store.ListCompatTasks(r.Context(), user, store.CompatTaskListFilter{
		Provider: klingCompatProvider, Version: "v2", Statuses: statuses,
		CreatedFrom: createdFrom, CreatedTo: createdTo, Page: page, PageSize: limit,
	})
	if err != nil {
		writeKlingCompatError(w, http.StatusInternalServerError, "list tasks failed", "task_query_failed")
		return
	}
	items := make([]any, 0, len(result.Items))
	for _, task := range result.Items {
		items = append(items, klingV2TaskData(task))
	}
	hasMore := page*limit < result.Total
	nextCursor := ""
	if hasMore {
		nextCursor = base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(page + 1)))
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"code": 0, "message": "success", "request_id": klingRequestID(result.Items),
		"data": map[string]any{"result": items, "count": len(items), "next_cursor": nextCursor, "has_more": hasMore},
	})
}

func klingV1Envelope(task store.GatewayTask) map[string]any {
	return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV1TaskData(task)}
}

func klingV1SubmissionEnvelope(task store.GatewayTask) map[string]any {
	data := klingV1TaskData(task)
	data["task_status"] = "submitted"
	delete(data, "task_status_msg")
	delete(data, "task_result")
	return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": data}
}

func klingV1TaskData(task store.GatewayTask) map[string]any {
	data := map[string]any{
		"task_id": klingCompatibilityPublicID(task), "task_status": klingV1Status(task.Status),
		"task_info":  map[string]any{"external_task_id": task.ExternalTaskID},
		"created_at": task.CreatedAt.UnixMilli(), "updated_at": task.UpdatedAt.UnixMilli(),
	}
	if task.ErrorMessage != "" || task.Error != "" {
		standard := publicTaskError(task)
		data["task_status_msg"] = standard.Message
		data["error"] = standard
	}
	if watermarkInfo, ok := task.Request["watermark_info"].(map[string]any); ok {
		data["watermark_info"] = watermarkInfo
	}
	videos := klingTaskVideos(task)
	if len(videos) > 0 {
		data["task_result"] = map[string]any{"videos": videos}
	}
	return data
}

func klingV2Envelope(task store.GatewayTask) map[string]any {
	return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV2TaskData(task)}
}

func klingV2SubmissionEnvelope(task store.GatewayTask) map[string]any {
	data := klingV2TaskData(task)
	data["status"] = "submitted"
	delete(data, "message")
	delete(data, "outputs")
	return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": data}
}

func klingV2TaskData(task store.GatewayTask) map[string]any {
	data := map[string]any{
		"id": klingCompatibilityPublicID(task), "status": klingV2Status(task.Status),
		"create_time": task.CreatedAt.UnixMilli(), "update_time": task.UpdatedAt.UnixMilli(),
		"external_id": task.ExternalTaskID,
	}
	if message := firstNonEmpty(task.ErrorMessage, task.Error); message != "" {
		standard := publicTaskError(task)
		data["message"] = standard.Message
		data["error"] = standard
	}
	if outputs := klingV2Outputs(task); len(outputs) > 0 {
		data["outputs"] = outputs
	}
	return data
}

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

func klingMap(value any) map[string]any {
	result, _ := value.(map[string]any)
	return result
}

func klingTaskVideos(task store.GatewayTask) []any {
	items, _ := task.Result["data"].([]any)
	videos := make([]any, 0, len(items))
	for index, raw := range items {
		item, _ := raw.(map[string]any)
		url := firstNonEmptyRequestString(item, "url", "video_url")
		if url == "" {
			continue
		}
		video := map[string]any{"id": firstNonEmptyRequestString(item, "id")}
		if video["id"] == "" {
			video["id"] = fmt.Sprintf("%s-%d", task.ID, index+1)
		}
		video["url"] = url
		if watermarkURL := firstNonEmptyRequestString(item, "watermark_url"); watermarkURL != "" {
			video["watermark_url"] = watermarkURL
		}
		if duration := klingStringAny(item["duration"]); duration != "" {
			video["duration"] = duration
		}
		videos = append(videos, video)
	}
	return videos
}

func klingV2Outputs(task store.GatewayTask) []any {
	videos := klingTaskVideos(task)
	outputs := make([]any, 0, len(videos))
	for _, raw := range videos {
		video, _ := raw.(map[string]any)
		output := cloneMap(video)
		output["type"] = "video"
		outputs = append(outputs, output)
	}
	return outputs
}

func klingV1Status(status string) string {
	switch status {
	case "succeeded":
		return "succeed"
	case "failed", "cancelled":
		return "failed"
	case "running":
		return "processing"
	default:
		return "submitted"
	}
}

func klingV2Status(status string) string {
	switch status {
	case "succeeded":
		return "succeeded"
	case "failed", "cancelled":
		return "failed"
	case "running":
		return "processing"
	default:
		return "submitted"
	}
}

func klingV2ProviderModel(pathModel string) (string, bool) {
	return canonicalKlingOmniModel(pathModel)
}

func canonicalKlingOmniModel(value string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "kling-o1", "kling-video-o1":
		return klingO1Model, true
	case "kling-v3-omni", "kling-3.0-omni", "kling-3-omni":
		return klingV3OmniModel, true
	default:
		return "", false
	}
}

func klingResolutionFromMode(mode string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(mode)) {
	case "std":
		return "720p", true
	case "pro":
		return "1080p", true
	case "4k":
		return "2160p", true
	default:
		return "", false
	}
}

func decodeKlingJSON(r *http.Request, target any) error {
	decoder := json.NewDecoder(r.Body)
	decoder.UseNumber()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		if err != nil {
			return err
		}
		return errors.New("multiple json values")
	}
	return nil
}

func writeKlingCompatError(w http.ResponseWriter, status int, message string, code string) {
	standard := publicerror.FromFields(code, message, status, status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500)
	publicerror.Observe(standard)
	writeJSON(w, standard.HTTPStatus, map[string]any{
		"code":         klingCompatErrorCode(standard.HTTPStatus),
		"message":      standard.Message,
		"request_id":   "",
		"error":        standard.Code,
		"public_error": standard,
	})
}

func klingCompatErrorCode(status int) int {
	switch status {
	case http.StatusBadRequest:
		return 1001
	case http.StatusUnauthorized:
		return 1100
	case http.StatusForbidden:
		return 1302
	case http.StatusNotFound:
		return 1201
	case http.StatusConflict:
		return 1200
	case http.StatusTooManyRequests:
		return 1400
	default:
		return 5000
	}
}

func mapListFromRequest(value any) []map[string]any {
	items, _ := value.([]any)
	if len(items) == 0 {
		if typed, ok := value.([]map[string]any); ok {
			return typed
		}
	}
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		if mapped, ok := item.(map[string]any); ok {
			out = append(out, mapped)
		}
	}
	return out
}

func firstPresentRequest(values ...any) any {
	for _, value := range values {
		if value == nil {
			continue
		}
		if text, ok := value.(string); ok {
			if strings.TrimSpace(text) != "" {
				return value
			}
			continue
		}
		return value
	}
	return nil
}

func boolFromRequestAny(value any) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		parsed, _ := strconv.ParseBool(strings.TrimSpace(typed))
		return parsed
	default:
		return false
	}
}

func klingCompatInt(value any) (int, bool) {
	text := klingStringAny(value)
	if text == "" {
		return 0, false
	}
	number, err := strconv.Atoi(text)
	return number, err == nil
}

func splitKlingIDs(raw string) []string {
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		if part = strings.TrimSpace(part); part != "" {
			out = append(out, part)
		}
	}
	return out
}

func klingCursorPage(cursor string) (int, error) {
	cursor = strings.TrimSpace(cursor)
	if cursor == "" {
		return 1, nil
	}
	decoded, err := base64.RawURLEncoding.DecodeString(cursor)
	if err != nil {
		return 0, err
	}
	page, err := strconv.Atoi(string(decoded))
	if err != nil || page <= 0 {
		return 0, errors.New("invalid cursor")
	}
	return page, nil
}

func klingMillisTime(value any) (*time.Time, error) {
	text := klingStringAny(value)
	if text == "" {
		return nil, nil
	}
	millis, err := strconv.ParseInt(text, 10, 64)
	if err != nil || millis < 0 {
		return nil, errors.New("invalid millisecond timestamp")
	}
	parsed := time.UnixMilli(millis)
	return &parsed, nil
}

func klingInternalStatuses(filters any) []string {
	statuses := make([]string, 0)
	for _, filter := range mapListFromRequest(filters) {
		if stringFromRequestAny(filter["key"]) != "status" {
			continue
		}
		values, _ := filter["values"].([]any)
		for _, value := range values {
			switch strings.ToLower(strings.TrimSpace(stringFromRequestAny(value))) {
			case "submitted":
				statuses = append(statuses, "queued")
			case "processing":
				statuses = append(statuses, "running")
			case "succeeded":
				statuses = append(statuses, "succeeded")
			case "failed":
				statuses = append(statuses, "failed", "cancelled")
			}
		}
	}
	return statuses
}

func klingRequestID(tasks []store.GatewayTask) string {
	if len(tasks) == 0 {
		return ""
	}
	return firstNonEmpty(tasks[0].RequestID, tasks[0].ID)
}

func klingRequestIDFromAny(items []any) string {
	if len(items) == 0 {
		return ""
	}
	item, _ := items[0].(map[string]any)
	return stringFromRequestAny(item["id"])
}

func klingStringAny(value any) string {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case json.Number:
		return typed.String()
	case float64:
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case float32:
		return strconv.FormatFloat(float64(typed), 'f', -1, 32)
	case int:
		return strconv.Itoa(typed)
	case int64:
		return strconv.FormatInt(typed, 10)
	case int32:
		return strconv.FormatInt(int64(typed), 10)
	default:
		return ""
	}
}
