package httpapi

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"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/identity"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/modelaccess"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/netproxy"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const (
	postgresReadinessTimeout  = 2 * time.Second
	localLoginStoreTimeout    = 5 * time.Second
	errorCodePostgresDown     = "POSTGRES_UNAVAILABLE"
	errorCodeAuthStoreDown    = "AUTH_STORE_UNAVAILABLE"
	authStoreUnavailableError = "authentication service temporarily unavailable"
)

// health godoc
// @Summary 健康检查
// @Description 返回服务进程、运行环境和身份模式，供负载均衡或人工排障使用。
// @Tags system
// @Produce json
// @Success 200 {object} HealthResponse
// @Router /api/v1/healthz [get]
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]any{
		"ok":           true,
		"service":      "easyai-ai-gateway",
		"env":          s.cfg.AppEnv,
		"identityMode": s.cfg.IdentityMode,
	})
}

// ready godoc
// @Summary 就绪检查
// @Description 检查 Postgres 是否可用；数据库不可用时返回 503。
// @Tags system
// @Produce json
// @Success 200 {object} ReadyResponse
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/readyz [get]
func (s *Server) ready(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), postgresReadinessTimeout)
	defer cancel()
	readinessStore := s.coordinationStore
	if readinessStore == nil {
		readinessStore = s.store
	}
	if err := readinessStore.Ping(ctx); err != nil {
		s.logPostgresUnavailable("postgres readiness check failed")
		writeError(w, http.StatusServiceUnavailable, "postgres unavailable", errorCodePostgresDown)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

// me godoc
// @Summary 获取当前用户
// @Description 返回鉴权中解析出的用户、租户、用户组和 API Key 上下文。
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} auth.User
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/me [get]
func (s *Server) me(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	writeJSON(w, http.StatusOK, user)
}

// register godoc
// @Summary 本地注册
// @Description 在 standalone 或 hybrid 身份模式下创建本地用户，并返回 24 小时 JWT。
// @Tags auth
// @Accept json
// @Produce json
// @Param input body store.LocalRegisterInput true "注册请求，password 至少 8 位，invitationCode 取决于部署策略"
// @Success 201 {object} AuthResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/auth/register [post]
func (s *Server) register(w http.ResponseWriter, r *http.Request) {
	if !s.localRegistrationEnabled() {
		writeError(w, http.StatusForbidden, "统一认证启用后，本地注册已关闭", "LOCAL_REGISTRATION_DISABLED")
		return
	}
	var input store.LocalRegisterInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	user, err := s.store.RegisterLocalUser(r.Context(), input)
	if err != nil {
		if errors.Is(err, store.ErrWeakPassword) {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		if errors.Is(err, store.ErrInvalidInvitation) {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		if errors.Is(err, store.ErrUserAlreadyExists) {
			writeError(w, http.StatusConflict, err.Error())
			return
		}
		s.logger.Error("register local user failed", "error", err)
		writeError(w, http.StatusInternalServerError, "register local user failed")
		return
	}
	s.writeAuthResponse(w, http.StatusCreated, user)
}

// login godoc
// @Summary 本地登录
// @Description 使用用户名或邮箱登录本地账号，并返回 24 小时 JWT。非本地身份模式只允许本地应急 Manager 登录。
// @Tags auth
// @Accept json
// @Produce json
// @Param input body store.LocalLoginInput true "登录请求，account 可为用户名或邮箱"
// @Success 200 {object} AuthResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/auth/login [post]
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
	var input store.LocalLoginInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), localLoginStoreTimeout)
	defer cancel()
	user, err := s.store.AuthenticateLocalUser(ctx, input)
	if err != nil {
		if errors.Is(err, store.ErrInvalidCredentials) {
			writeError(w, http.StatusUnauthorized, "invalid account or password")
			return
		}
		if store.IsPostgresUnavailable(err) {
			s.logPostgresUnavailable("login authentication store unavailable")
			writeError(w, http.StatusServiceUnavailable, authStoreUnavailableError, errorCodeAuthStoreDown)
			return
		}
		s.logger.Error("login local user failed", "error", err)
		writeError(w, http.StatusInternalServerError, "login failed")
		return
	}
	if !s.localLoginAllowed(user) {
		writeError(w, http.StatusForbidden, "local login is disabled except for break-glass managers")
		return
	}
	s.writeAuthResponse(w, http.StatusOK, user)
}

func (s *Server) logPostgresUnavailable(message string) {
	if s.logger == nil || s.store == nil || s.store.Pool() == nil {
		return
	}
	statistics := s.store.Pool().Stat()
	s.logger.Error(message,
		"error_category", "postgres_unavailable",
		"postgres_pool_max_connections", statistics.MaxConns(),
		"postgres_pool_total_connections", statistics.TotalConns(),
		"postgres_pool_acquired_connections", statistics.AcquiredConns(),
		"postgres_pool_idle_connections", statistics.IdleConns(),
		"postgres_pool_empty_acquire_count", statistics.EmptyAcquireCount(),
		"postgres_pool_canceled_acquire_count", statistics.CanceledAcquireCount(),
		"postgres_critical_pool", s.criticalPostgresPoolLogFields(),
		"postgres_river_pool", s.riverPostgresPoolLogFields(),
	)
}

func (s *Server) criticalPostgresPoolLogFields() map[string]any {
	if s.coordinationStore == nil || s.coordinationStore == s.store ||
		s.coordinationStore.Pool() == nil {
		return map[string]any{"separate": false}
	}
	statistics := s.coordinationStore.Pool().Stat()
	return map[string]any{
		"separate":               true,
		"max_connections":        statistics.MaxConns(),
		"total_connections":      statistics.TotalConns(),
		"acquired_connections":   statistics.AcquiredConns(),
		"idle_connections":       statistics.IdleConns(),
		"empty_acquire_count":    statistics.EmptyAcquireCount(),
		"canceled_acquire_count": statistics.CanceledAcquireCount(),
	}
}

func (s *Server) riverPostgresPoolLogFields() map[string]any {
	if s.riverStore == nil || s.riverStore == s.store ||
		s.riverStore.Pool() == nil {
		return map[string]any{"separate": false}
	}
	statistics := s.riverStore.Pool().Stat()
	return map[string]any{
		"separate":               true,
		"max_connections":        statistics.MaxConns(),
		"total_connections":      statistics.TotalConns(),
		"acquired_connections":   statistics.AcquiredConns(),
		"idle_connections":       statistics.IdleConns(),
		"empty_acquire_count":    statistics.EmptyAcquireCount(),
		"canceled_acquire_count": statistics.CanceledAcquireCount(),
	}
}

func (s *Server) localIdentityEnabled() bool {
	mode := strings.ToLower(strings.TrimSpace(s.cfg.IdentityMode))
	return mode == "" || mode == "standalone" || mode == "hybrid"
}

func (s *Server) localRegistrationEnabled() bool {
	runtime := s.currentIdentityRuntime()
	return (runtime == nil || runtime.Revision.State != identity.RevisionActive) &&
		s.localIdentityEnabled() && s.ordinaryLocalJWTEnabled()
}

func (s *Server) localLoginAllowed(user store.GatewayUser) bool {
	for _, role := range user.Roles {
		if role == "manager" || role == "admin" {
			return true
		}
	}
	return s.localIdentityEnabled() && s.ordinaryLocalJWTEnabled()
}

func (s *Server) ordinaryLocalJWTEnabled() bool {
	return s.identityRuntime == nil || s.identityRuntime.LegacyJWTEnabled()
}

func (s *Server) writeAuthResponse(w http.ResponseWriter, status int, user store.GatewayUser) {
	authUser := authUserFromGatewayUser(user)
	const ttl = 24 * time.Hour
	token, err := s.auth.SignJWT(authUser, ttl)
	if err != nil {
		s.logger.Error("sign local jwt failed", "error", err)
		writeError(w, http.StatusInternalServerError, "token sign failed")
		return
	}
	writeJSON(w, status, map[string]any{
		"accessToken": token,
		"tokenType":   "Bearer",
		"expiresIn":   int(ttl.Seconds()),
		"user":        authUser,
	})
}

func authUserFromGatewayUser(user store.GatewayUser) *auth.User {
	roles := user.Roles
	if len(roles) == 0 {
		roles = []string{"user"}
	}
	tenantID := user.TenantID
	if tenantID == "" {
		tenantID = user.TenantKey
	}
	return &auth.User{
		ID:              user.ID,
		Username:        user.Username,
		Roles:           roles,
		TenantID:        tenantID,
		GatewayTenantID: user.GatewayTenantID,
		TenantKey:       user.TenantKey,
		Source:          "gateway",
		GatewayUserID:   user.ID,
		UserGroupID:     user.DefaultUserGroupID,
	}
}

// listPlatforms godoc
// @Summary 列出平台
// @Description 管理端返回所有接入平台及其优先级、定价和运行策略摘要。
// @Tags platforms
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PlatformListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms [get]
func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
	platforms, err := s.store.ListPlatforms(r.Context())
	if err != nil {
		s.logger.Error("list platforms failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list platforms failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": platforms})
}

// listPlayablePlatforms godoc
// @Summary 列出可用平台
// @Description 按当前用户可访问模型过滤平台，仅返回启用且存在可访问模型的平台。
// @Tags playground
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PlatformListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/platforms [get]
func (s *Server) listPlayablePlatforms(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
	if err != nil {
		s.logger.Error("list playable platform models failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list playable platforms failed")
		return
	}
	allowedPlatformIDs := map[string]bool{}
	for _, model := range models {
		allowedPlatformIDs[model.PlatformID] = true
	}
	platforms, err := s.store.ListPlatforms(r.Context())
	if err != nil {
		s.logger.Error("list platforms failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list playable platforms failed")
		return
	}
	filtered := platforms[:0]
	for _, platform := range platforms {
		if platform.Status == "enabled" && allowedPlatformIDs[platform.ID] {
			filtered = append(filtered, platform)
		}
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": filtered})
}

// createPlatform godoc
// @Summary 创建平台
// @Description 新增模型供应商平台配置；credentials 会被服务端保存并在返回值中脱敏。
// @Tags platforms
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.CreatePlatformInput true "平台配置请求"
// @Success 201 {object} store.Platform
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms [post]
func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
	var input store.CreatePlatformInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	input.Provider = strings.TrimSpace(input.Provider)
	input.Name = strings.TrimSpace(input.Name)
	input.InternalName = strings.TrimSpace(input.InternalName)
	input.Status = strings.TrimSpace(input.Status)
	if input.Provider == "" || input.Name == "" {
		writeError(w, http.StatusBadRequest, "provider and name are required")
		return
	}
	if input.Status != "" && input.Status != "enabled" && input.Status != "disabled" {
		writeError(w, http.StatusBadRequest, "status must be enabled or disabled")
		return
	}
	if input.AuthType == "" {
		input.AuthType = "bearer"
	}
	config, err := netproxy.NormalizePlatformConfig(input.Config)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	input.Config = config
	platform, err := s.store.CreatePlatform(r.Context(), input)
	if err != nil {
		if store.IsInvalidRateLimitPolicy(err) {
			writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
			return
		}
		s.logger.Error("create platform failed", "error", err)
		writeError(w, http.StatusInternalServerError, "create platform failed")
		return
	}
	writeJSON(w, http.StatusCreated, platform)
}

// updatePlatform godoc
// @Summary 更新平台
// @Description 覆盖指定平台的基础配置、凭证、优先级、定价和运行策略。
// @Tags platforms
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param platformID path string true "平台 ID"
// @Param input body store.CreatePlatformInput true "平台配置请求"
// @Success 200 {object} store.Platform
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms/{platformID} [patch]
func (s *Server) updatePlatform(w http.ResponseWriter, r *http.Request) {
	var input store.CreatePlatformInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	input.Provider = strings.TrimSpace(input.Provider)
	input.Name = strings.TrimSpace(input.Name)
	input.InternalName = strings.TrimSpace(input.InternalName)
	input.Status = strings.TrimSpace(input.Status)
	if input.Provider == "" || input.Name == "" {
		writeError(w, http.StatusBadRequest, "provider and name are required")
		return
	}
	if input.Status != "" && input.Status != "enabled" && input.Status != "disabled" {
		writeError(w, http.StatusBadRequest, "status must be enabled or disabled")
		return
	}
	if input.AuthType == "" {
		input.AuthType = "bearer"
	}
	config, err := netproxy.NormalizePlatformConfig(input.Config)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	input.Config = config
	platform, err := s.store.UpdatePlatform(r.Context(), r.PathValue("platformID"), input)
	if err != nil {
		if store.IsInvalidRateLimitPolicy(err) {
			writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
			return
		}
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "platform not found")
			return
		}
		if store.IsUniqueViolation(err) {
			writeError(w, http.StatusConflict, "platform key already exists")
			return
		}
		s.logger.Error("update platform failed", "error", err)
		writeError(w, http.StatusInternalServerError, "update platform failed")
		return
	}
	writeJSON(w, http.StatusOK, platform)
}

// deletePlatform godoc
// @Summary 删除平台
// @Description 删除指定平台及关联配置；不存在时返回 404。
// @Tags platforms
// @Produce json
// @Security BearerAuth
// @Param platformID path string true "平台 ID"
// @Success 204 "No Content"
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms/{platformID} [delete]
func (s *Server) deletePlatform(w http.ResponseWriter, r *http.Request) {
	if err := s.store.DeletePlatform(r.Context(), r.PathValue("platformID")); err != nil {
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "platform not found")
			return
		}
		s.logger.Error("delete platform failed", "error", err)
		writeError(w, http.StatusInternalServerError, "delete platform failed")
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// createPlatformModel godoc
// @Summary 创建平台模型
// @Description 为平台新增一个可路由模型；路径中的 platformID 会覆盖请求体 platformId。
// @Tags platform-models
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param platformID path string true "平台 ID，使用 /api/admin/platforms/{platformID}/models 时由路径提供"
// @Param input body store.CreatePlatformModelInput true "平台模型配置请求"
// @Success 201 {object} store.PlatformModel
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms/{platformID}/models [post]
// @Router /api/admin/platform-models [post]
func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
	var input store.CreatePlatformModelInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	if pathPlatformID := r.PathValue("platformID"); pathPlatformID != "" {
		input.PlatformID = pathPlatformID
	}
	if input.PlatformID == "" {
		writeError(w, http.StatusBadRequest, "platformId is required")
		return
	}
	model, err := s.store.CreatePlatformModel(r.Context(), input)
	if err != nil {
		if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) || store.IsInvalidRateLimitPolicy(err) {
			writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
			return
		}
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "base model not found")
			return
		}
		s.logger.Error("create platform model failed", "error", err)
		writeError(w, http.StatusInternalServerError, "create platform model failed")
		return
	}
	writeJSON(w, http.StatusCreated, s.platformModelResponse(r.Context(), model))
}

// replacePlatformModels godoc
// @Summary 替换平台模型
// @Description 用请求体中的 models 列表整体替换指定平台下的模型配置。
// @Tags platform-models
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param platformID path string true "平台 ID"
// @Param input body ReplacePlatformModelsRequest true "模型列表请求"
// @Success 200 {object} PlatformModelListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platforms/{platformID}/models [put]
func (s *Server) replacePlatformModels(w http.ResponseWriter, r *http.Request) {
	platformID := r.PathValue("platformID")
	if platformID == "" {
		writeError(w, http.StatusBadRequest, "platformId is required")
		return
	}

	var input struct {
		Models []store.CreatePlatformModelInput `json:"models"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}

	models, err := s.store.ReplacePlatformModels(r.Context(), platformID, input.Models)
	if err != nil {
		if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) || store.IsInvalidRateLimitPolicy(err) {
			writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
			return
		}
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "base model not found")
			return
		}
		s.logger.Error("replace platform models failed", "error", err)
		writeError(w, http.StatusInternalServerError, "replace platform models failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": s.platformModelResponses(r.Context(), models)})
}

// deletePlatformModel godoc
// @Summary 删除平台模型
// @Description 删除指定平台模型路由配置。
// @Tags platform-models
// @Produce json
// @Security BearerAuth
// @Param modelID path string true "平台模型 ID"
// @Success 204 "No Content"
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/platform-models/{modelID} [delete]
func (s *Server) deletePlatformModel(w http.ResponseWriter, r *http.Request) {
	if err := s.store.DeletePlatformModel(r.Context(), r.PathValue("modelID")); err != nil {
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "platform model not found")
			return
		}
		s.logger.Error("delete platform model failed", "error", err)
		writeError(w, http.StatusInternalServerError, "delete platform model failed")
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// listModels godoc
// @Summary 列出平台模型
// @Description 管理端返回所有平台模型，并补齐有效计费配置。
// @Tags platform-models
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PlatformModelListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/models [get]
func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
	models, err := s.store.ListModels(r.Context())
	if err != nil {
		s.logger.Error("list models failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list models failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": s.platformModelResponses(r.Context(), models)})
}

// listPlayableModels godoc
// @Summary 列出可调用模型
// @Description 按全局启用、租户、用户组、用户、当前 API Key 分层白名单及 scope 的交集返回可用于 Playground 或 API 调用的平台来源；其他主体规则不参与求值。
// @Tags playground
// @Produce json
// @Security BearerAuth
// @Param usage_scene query string false "模型可选场景；不传时保持原有行为" Enums(canvas_model_node,desktop)
// @Success 200 {object} PlatformModelListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/platform-models [get]
// @Router /api/v1/playground/models [get]
func (s *Server) listPlayableModels(w http.ResponseWriter, r *http.Request) {
	usageScene, err := parseModelUsageSceneQuery(r.URL.Query())
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	user, _ := auth.UserFromContext(r.Context())
	models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
	if err != nil {
		s.logger.Error("list playable models failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list playable models failed")
		return
	}
	if usageScene != "" {
		availableModelIDs, sceneErr := s.listServerMainUsageSceneModelIDs(r.Context(), usageScene)
		if sceneErr != nil {
			s.logger.Error("filter playable models by usage scene failed", "usage_scene", usageScene, "error", sceneErr)
			writeError(w, http.StatusBadGateway, "model usage scene catalog is unavailable")
			return
		}
		models = filterPlatformModelsByUsageSceneIDs(models, availableModelIDs)
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": s.platformModelResponses(r.Context(), models)})
}

// listPricingRules godoc
// @Summary 列出定价规则
// @Description 返回所有定价规则明细，便于管理端排查有效价格。
// @Tags pricing
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PricingRuleListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/pricing/rules [get]
func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListPricingRules(r.Context())
	if err != nil {
		s.logger.Error("list pricing rules failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list pricing rules failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listTenants godoc
// @Summary 列出租户
// @Description 管理端返回网关租户列表。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Success 200 {object} TenantListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/tenants [get]
func (s *Server) listTenants(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListTenants(r.Context())
	if err != nil {
		s.logger.Error("list tenants failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list tenants failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listUsers godoc
// @Summary 列出用户
// @Description 管理端返回网关用户列表及钱包摘要。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Success 200 {object} UserListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/users [get]
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListUsers(r.Context())
	if err != nil {
		s.logger.Error("list users failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list users failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listUserGroups godoc
// @Summary 列出用户组
// @Description 管理端返回用户组及其计费、限流和配额策略。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Success 200 {object} UserGroupListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/user-groups [get]
func (s *Server) listUserGroups(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListUserGroups(r.Context())
	if err != nil {
		s.logger.Error("list user groups failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list user groups failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listCurrentUserGroups godoc
// @Summary 获取当前用户组策略
// @Description 返回当前用户关联的用户组及策略摘要，供个人中心使用。
// @Tags workspace
// @Produce json
// @Security BearerAuth
// @Success 200 {object} UserGroupListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/user-groups [get]
func (s *Server) listCurrentUserGroups(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	if user == nil {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	groupIDs := compactAuthStrings(user.UserGroupID)
	groupKeys := compactAuthStrings(user.UserGroupKey)
	groupKeys = append(groupKeys, compactAuthStrings(user.UserGroupKeys...)...)
	items, err := s.store.ListUserGroupsBySubject(r.Context(), groupIDs, groupKeys)
	if err != nil {
		s.logger.Error("list current user groups failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list current user groups failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

func compactAuthStrings(values ...string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			out = append(out, value)
		}
	}
	return out
}

// listAPIKeys godoc
// @Summary 列出 API Key
// @Description 返回当前用户创建的 API Key 元数据，secret 只在创建时返回。
// @Tags api-keys
// @Produce json
// @Security BearerAuth
// @Success 200 {object} APIKeyListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys [get]
func (s *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	items, err := s.store.ListAPIKeys(r.Context(), user)
	if err != nil {
		s.logger.Error("list api keys failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list api keys failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listPlayableAPIKeys godoc
// @Summary 列出 Playground API Key
// @Description 返回当前本地用户可在 Playground 中直接使用的 API Key 和 secret。
// @Tags playground
// @Produce json
// @Security BearerAuth
// @Success 200 {object} PlayableAPIKeyListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/playground/api-keys [get]
func (s *Server) listPlayableAPIKeys(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	items, err := s.store.ListPlayableAPIKeys(r.Context(), user)
	if err != nil {
		if errors.Is(err, store.ErrLocalUserRequired) {
			writeLocalUserRequired(w)
			return
		}
		s.logger.Error("list playable api keys failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list playable api keys failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// createAPIKey godoc
// @Summary 创建 API Key
// @Description 为当前本地用户创建 API Key；secret 仅在本次响应中返回。
// @Tags api-keys
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.CreateAPIKeyInput true "API Key 创建请求"
// @Success 201 {object} store.CreatedAPIKey
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys [post]
func (s *Server) createAPIKey(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	var input store.CreateAPIKeyInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	created, err := s.store.CreateAPIKey(r.Context(), input, user)
	if err != nil {
		if errors.Is(err, store.ErrLocalUserRequired) {
			writeLocalUserRequired(w)
			return
		}
		s.logger.Error("create api key failed", "error", err)
		writeError(w, http.StatusInternalServerError, "create api key failed")
		return
	}
	writeJSON(w, http.StatusCreated, created)
}

// updateAPIKeyScopes godoc
// @Summary 更新 API Key 能力范围
// @Description 更新当前用户拥有的 API Key scopes，至少需要保留一个能力范围。
// @Tags api-keys
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param apiKeyID path string true "API Key ID"
// @Param input body store.UpdateAPIKeyScopesInput true "API Key scope 更新请求"
// @Success 200 {object} store.APIKey
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys/{apiKeyID}/scopes [patch]
func (s *Server) updateAPIKeyScopes(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	var input store.UpdateAPIKeyScopesInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	item, err := s.store.UpdateAPIKeyScopes(r.Context(), r.PathValue("apiKeyID"), input, user)
	if err == nil {
		writeJSON(w, http.StatusOK, item)
		return
	}
	if errors.Is(err, store.ErrLocalUserRequired) {
		writeLocalUserRequired(w)
		return
	}
	if errors.Is(err, store.ErrInvalidAPIKeyScopes) {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	if store.IsNotFound(err) {
		writeError(w, http.StatusNotFound, "api key not found")
		return
	}
	s.logger.Error("update api key scopes failed", "error", err)
	writeError(w, http.StatusInternalServerError, "update api key scopes failed")
}

// disableAPIKey godoc
// @Summary 禁用 API Key
// @Description 禁用当前用户拥有的 API Key，保留记录但不再允许调用。
// @Tags api-keys
// @Produce json
// @Security BearerAuth
// @Param apiKeyID path string true "API Key ID"
// @Success 200 {object} store.APIKey
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys/{apiKeyID}/disable [patch]
func (s *Server) disableAPIKey(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	item, err := s.store.DisableAPIKey(r.Context(), r.PathValue("apiKeyID"), user)
	if err == nil {
		writeJSON(w, http.StatusOK, item)
		return
	}
	if errors.Is(err, store.ErrLocalUserRequired) {
		writeLocalUserRequired(w)
		return
	}
	if store.IsNotFound(err) {
		writeError(w, http.StatusNotFound, "api key not found")
		return
	}
	s.logger.Error("disable api key failed", "error", err)
	writeError(w, http.StatusInternalServerError, "disable api key failed")
}

// deleteAPIKey godoc
// @Summary 删除 API Key
// @Description 删除当前用户拥有的 API Key。
// @Tags api-keys
// @Produce json
// @Security BearerAuth
// @Param apiKeyID path string true "API Key ID"
// @Success 204 "No Content"
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys/{apiKeyID} [delete]
func (s *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	err := s.store.DeleteAPIKey(r.Context(), r.PathValue("apiKeyID"), user)
	if err == nil {
		w.WriteHeader(http.StatusNoContent)
		return
	}
	if errors.Is(err, store.ErrLocalUserRequired) {
		writeLocalUserRequired(w)
		return
	}
	if store.IsNotFound(err) {
		writeError(w, http.StatusNotFound, "api key not found")
		return
	}
	s.logger.Error("delete api key failed", "error", err)
	writeError(w, http.StatusInternalServerError, "delete api key failed")
}

// estimatePricing godoc
// @Summary 估算请求价格
// @Description 按当前用户、模型候选、任务类型和请求参数估算计费条目。
// @Tags pricing
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body PricingEstimateRequest true "计费估算请求，kind 默认为 chat.completions"
// @Success 200 {object} PricingEstimateResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 429 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/pricing/estimate [post]
func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) {
	user, _ := auth.UserFromContext(r.Context())
	var body map[string]any
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	model := requestModelName(body)
	kind, _ := body["kind"].(string)
	if kind == "" {
		kind = "chat.completions"
	}
	if model == "" {
		writeError(w, http.StatusBadRequest, "model is required")
		return
	}
	if !apiKeyScopeAllowed(user, kind) {
		writeError(w, http.StatusForbidden, "api key scope does not allow this capability")
		return
	}
	estimate, err := s.runner.Estimate(r.Context(), kind, model, body, user)
	if err != nil {
		if s.billingMetrics != nil {
			s.billingMetrics.ObserveBillingEvent("estimate_failed")
		}
		if runner.IsPricingUnavailable(err) {
			if s.billingMetrics != nil {
				s.billingMetrics.ObserveBillingEvent("pricing_unavailable")
			}
			writeErrorWithDetails(w, http.StatusServiceUnavailable, runErrorMessage(err), runErrorDetails(err), "pricing_unavailable")
			return
		}
		if errors.Is(err, store.ErrNoModelCandidate) {
			applyRunErrorHeaders(w, err)
			writeErrorWithDetails(w, statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), store.ModelCandidateErrorCode(err))
			return
		}
		if code := clients.ErrorCode(err); code == "bad_request" || code == "invalid_parameter" {
			writeErrorWithDetails(w, http.StatusBadRequest, runErrorMessage(err), runErrorDetails(err), code)
			return
		}
		s.logger.Error("estimate pricing failed", "error", err)
		writeError(w, http.StatusInternalServerError, "estimate pricing failed")
		return
	}
	writeJSON(w, http.StatusOK, estimate)
}

// listRateLimitWindows godoc
// @Summary 列出限流窗口
// @Description 管理端查看当前运行时限流窗口状态。
// @Tags runtime
// @Produce json
// @Security BearerAuth
// @Success 200 {object} RateLimitWindowListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/runtime/rate-limit-windows [get]
func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListRateLimitWindows(r.Context())
	if err != nil {
		s.logger.Error("list rate limit windows failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list rate limit windows failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// listModelRateLimitStatuses godoc
// @Summary 列出模型限流状态
// @Description 管理端查看平台模型维度的限流和冷却状态。
// @Tags runtime
// @Produce json
// @Security BearerAuth
// @Success 200 {object} ModelRateLimitStatusListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/runtime/model-rate-limits [get]
func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Request) {
	items, err := s.store.ListModelRateLimitStatuses(r.Context())
	if err != nil {
		s.logger.Error("list model rate limit statuses failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list model rate limit statuses failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": items})
}

// createTask godoc
// @Summary 创建或执行 AI 任务
// @Description 统一公开入口按 model 选择平台模型并默认同步返回兼容响应；设置 X-Async=true 时异步创建任务并返回 202。
// @Tags tasks
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param X-Async header bool false "true 时异步创建任务并返回 202"
// @Param Idempotency-Key header string false "可选请求幂等键；同一用户范围内唯一"
// @Param input body TaskRequest true "AI 任务请求，字段随任务类型变化"
// @Success 200 {object} CompatibleResponse
// @Success 202 {object} TaskAcceptedResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 402 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 429 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Router /api/v1/reranks [post]
func (s *Server) createTask(kind string, compatible bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var releaseAsyncMediaRequest func()
		if easyAIAsyncMediaRequest(kind, r) {
			var err error
			releaseAsyncMediaRequest, err = s.acquireMediaRequestBodySlot(r.Context())
			if err != nil {
				return
			}
			defer releaseAsyncMediaRequest()
		}
		targetProtocol := targetProtocolForTaskRequest(kind, r)
		writeTaskError := func(status int, message string, details map[string]any, codes ...string) {
			if easyAIAsyncMediaRequest(kind, r) {
				writeEasyAIAsyncError(w, status, message, details, codes...)
				return
			}
			if targetProtocol == "" {
				writeErrorWithDetails(w, status, message, details, codes...)
				return
			}
			code := ""
			if len(codes) > 0 {
				code = codes[0]
			}
			writeProtocolError(w, targetProtocol, status, message, details, code)
		}
		user, ok := auth.UserFromContext(r.Context())
		if !ok {
			writeTaskError(http.StatusUnauthorized, "unauthorized", nil)
			return
		}
		admission, err := s.authorizeTaskTraffic(r, user)
		if err != nil {
			writeTaskTrafficError(w, err, targetProtocol)
			return
		}

		body, err := s.decodeTaskRequestBody(r.Context(), w, r, kind)
		if err != nil {
			status := http.StatusBadRequest
			if code := clients.ErrorCode(err); strings.HasPrefix(code, "upload_") || strings.HasPrefix(code, "storage_") || code == "request_asset_upload_failed" || code == "request_asset_public_url_required" {
				status = http.StatusBadGateway
			}
			writeTaskError(status, err.Error(), nil, clients.ErrorCode(err))
			return
		}
		if kind == "chat.completions" || kind == "responses" {
			if err := clients.ValidateOpenAIRequestParameters(kind, body); err != nil {
				writeTaskError(http.StatusBadRequest, err.Error(), map[string]any{"param": clients.ErrorParam(err)}, clients.ErrorCode(err))
				return
			}
		}
		if err := validateMediaResponseFormat(kind, body); err != nil {
			writeTaskError(http.StatusBadRequest, err.Error(), map[string]any{"param": "response_format"}, "invalid_parameter")
			return
		}
		requestedModel := requestModelName(body)
		model := canonicalTaskModelName(kind, requestedModel)
		if model == "" {
			writeTaskError(http.StatusBadRequest, "model is required", nil, "invalid_parameter")
			return
		}
		if model != requestedModel {
			body["model"] = model
		}
		if !apiKeyScopeAllowed(user, kind) {
			writeTaskError(http.StatusForbidden, "api key scope does not allow this capability", nil, "permission_denied")
			return
		}
		responsePlan := planTaskResponse(kind, compatible, body, r)
		if targetProtocol != "" && !easyAIAsyncMediaRequest(kind, r) {
			// OpenAI compatibility endpoints are synchronous APIs. Gateway async
			// task envelopes belong only to native Gateway routes, except for the
			// explicit EasyAI media X-Async extension.
			responsePlan.asyncMode = false
		}
		idempotencyKey, hasIdempotencyKey, err := optionalTaskIdempotencyKey(r)
		if err != nil {
			writeTaskError(http.StatusBadRequest, "Idempotency-Key must contain one non-empty value", nil, "invalid_idempotency_key")
			return
		}
		prepared, err := s.prepareTaskRequest(r.Context(), r, user, body)
		if err != nil {
			s.logger.Warn("prepare task request failed", "kind", kind, "error", err)
			status := http.StatusBadRequest
			if code := clients.ErrorCode(err); strings.HasPrefix(code, "upload_") || strings.HasPrefix(code, "storage_") || code == "request_asset_upload_failed" {
				status = http.StatusBadGateway
			}
			writeTaskError(status, err.Error(), nil, clients.ErrorCode(err))
			return
		}
		if err := s.runner.ValidateModelAccess(r.Context(), kind, model, prepared.Body, user); err != nil {
			if responsePlan.asyncMode && store.ModelCandidateRetryAfter(err) > 0 {
				// Persist recoverable asynchronous requests so the shared queue can
				// wake them when the earliest candidate cooldown expires.
			} else if errors.Is(err, store.ErrNoModelCandidate) {
				applyRunErrorHeaders(w, err)
				writeTaskError(statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), runErrorCode(err))
				return
			} else {
				s.logger.Error("validate model access failed", "kind", kind, "error_category", "model_access_validation_failed", "error", err)
				writeTaskError(http.StatusInternalServerError, "validate model access failed", nil, "model_access_validation_failed")
				return
			}
		}

		runMode, err := s.admittedTaskRunMode(admission, prepared.Body)
		if err != nil {
			writeTaskTrafficError(w, err, targetProtocol)
			return
		}
		createInput := store.CreateTaskInput{
			Kind:            kind,
			Model:           model,
			RunMode:         runMode,
			AcceptanceRunID: admission.AcceptanceRunID,
			Async:           responsePlan.asyncMode,
			Request:         prepared.Body,
			ConversationID:  prepared.ConversationID,
			NewMessageCount: prepared.NewMessageCount,
			MessageRefs:     prepared.MessageRefs,
		}
		if hasIdempotencyKey {
			createInput.IdempotencyKeyHash = taskIdempotencyKeyHash(idempotencyKey)
			createInput.IdempotencyRequestHash = taskIdempotencyRequestHash(kind, responsePlan.asyncMode, responsePlan.streamMode, prepared.Body)
		}
		created, err := s.store.CreateTaskIdempotent(r.Context(), createInput, user)
		if err != nil {
			if errors.Is(err, store.ErrRateLimited) {
				applyRunErrorHeaders(w, err)
				writeTaskError(statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), runErrorCode(err))
				return
			}
			if errors.Is(err, store.ErrIdempotencyKeyReused) {
				writeTaskError(http.StatusConflict, "Idempotency-Key was reused for a different request", nil, "idempotency_key_reused")
				return
			}
			if errors.Is(err, store.ErrTaskRequestBinaryNotMaterialized) {
				writeTaskError(http.StatusBadRequest, err.Error(), nil, clients.ErrorCode(err))
				return
			}
			s.logger.Error("create task failed", "kind", kind, "error_category", "task_create_failed", "error", err)
			writeTaskError(http.StatusInternalServerError, "create task failed", nil, "task_create_failed")
			return
		}
		task := created.Task
		if created.Replayed {
			if s.billingMetrics != nil {
				s.billingMetrics.ObserveBillingEvent("idempotent_replay")
			}
			if targetProtocol == "" || gatewayAPIV1Request(r) {
				w.Header().Set("Idempotent-Replayed", "true")
				w.Header().Set("X-Gateway-Task-Id", task.ID)
			}
			if responsePlan.streamMode {
				writeTaskError(http.StatusConflict, "streaming idempotent replay is not supported", nil, "idempotency_stream_replay_unsupported")
				return
			}
			if task.Status == "succeeded" && !responsePlan.asyncMode && (targetProtocol != "" || responsePlan.compatibleMode) {
				task, err = s.hydrateTaskResult(r.Context(), task)
				if err != nil {
					if targetProtocol != "" {
						writeProtocolError(w, targetProtocol, statusFromRunError(err), err.Error(), nil, clients.ErrorCode(err))
					} else {
						writeStoredBinaryResultError(w, err)
					}
					return
				}
			}
			if targetProtocol != "" && !responsePlan.asyncMode {
				if task.Status == "succeeded" {
					writeJSON(w, http.StatusOK, task.Result)
					return
				}
				status := http.StatusConflict
				if task.Status == "failed" || task.Status == "cancelled" {
					status = taskErrorHTTPStatus(task)
				}
				writeTaskError(status, firstNonEmpty(task.ErrorMessage, task.Error, task.Message, "task is not complete"), nil, firstNonEmpty(task.ErrorCode, task.Status))
				return
			}
			writeIdempotentTaskReplay(w, task, responsePlan.compatibleMode)
			return
		}
		if responsePlan.asyncMode {
			if err := s.runner.SubmitAsyncTask(r.Context(), task); err != nil {
				applyRunErrorHeaders(w, err)
				writeTaskError(statusFromRunError(err), runErrorMessage(err), runErrorDetails(err), runErrorCode(err))
				return
			}
			if releaseAsyncMediaRequest != nil {
				releaseAsyncMediaRequest()
				releaseAsyncMediaRequest = nil
			}
			writeTaskAccepted(w, task)
			return
		}
		runCtx, cancelRun := s.requestExecutionContext(r)
		defer cancelRun()
		if responsePlan.compatibleMode {
			writeProtocolCompatibleTaskResponse(runCtx, w, r, s.runner, kind, model, targetProtocol, task, user, responsePlan.streamMode, streamIncludeUsage(body), s.billingMetrics)
			return
		}
		result, runErr := s.runner.Execute(runCtx, task, user)
		if runErr != nil {
			s.logger.Warn("task completed with failure", "kind", kind, "taskId", task.ID, "error", runErr)
		}

		if !requestStillConnected(r) {
			return
		}
		writeTaskAccepted(w, result.Task)
	})
}

// createAPIV1ChatCompletions godoc
// @Summary 创建 Chat Completions
// @Description /api/v1/chat/completions 同步执行：除 Gateway 路由字段外透明转发当前及未来 OpenAI 字段与嵌套结构，完整保留 function/custom tools、tool_choice、tool_calls 和旧版 function_call；stream=true 返回 text/event-stream SSE，stream=false 或未传返回兼容 JSON；该接口忽略 X-Async。
// @Tags tasks
// @Accept json
// @Produce json
// @Produce text/event-stream
// @Security BearerAuth
// @Param X-Async header bool false "该接口忽略此参数"
// @Param input body ChatCompletionRequest true "Chat Completions 请求"
// @Success 200 {object} ChatCompletionCompatibleResponse
// @Failure 400 {object} OpenAIErrorEnvelope
// @Failure 401 {object} OpenAIErrorEnvelope
// @Failure 402 {object} OpenAIErrorEnvelope
// @Failure 403 {object} OpenAIErrorEnvelope
// @Failure 404 {object} OpenAIErrorEnvelope
// @Failure 429 {object} OpenAIErrorEnvelope
// @Failure 502 {object} OpenAIErrorEnvelope
// @Router /api/v1/chat/completions [post]
func (s *Server) createAPIV1ChatCompletions() http.Handler {
	return s.createTask("chat.completions", false)
}

// openAIChatCompletionsDoc godoc
// @Summary 创建 OpenAI Chat Completions
// @Description OpenAI-compatible Chat Completions 入口。除 Gateway 路由字段外，原生请求会透明转发当前及未来 OpenAI 字段和嵌套结构；Gateway 仅覆盖上游模型名等受控字段。function/custom tools、tool_choice、tool_calls 与旧版 function_call 均保持标准语义。
// @Tags chat
// @Accept json
// @Produce json
// @Produce text/event-stream
// @Security BearerAuth
// @Param input body ChatCompletionRequest true "Chat Completions 请求"
// @Success 200 {object} ChatCompletionCompatibleResponse
// @Failure 400 {object} OpenAIErrorEnvelope "invalid_parameter"
// @Failure 401 {object} OpenAIErrorEnvelope
// @Failure 402 {object} OpenAIErrorEnvelope
// @Failure 403 {object} OpenAIErrorEnvelope
// @Failure 429 {object} OpenAIErrorEnvelope
// @Failure 502 {object} OpenAIErrorEnvelope
func openAIChatCompletionsDoc() {}

// openAIResponsesDoc godoc
// @Summary 创建 OpenAI Responses
// @Description 公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时除 Gateway 路由字段外透明转发当前及未来 OpenAI 字段；否则仅对具有等价 Chat Completions 语义的请求执行转换。background、conversation、prompt template、context management、max_tool_calls、自动 truncation、reasoning summary、内置工具、MCP 和 namespace 等原生专属能力不会静默丢弃：路由会排除 Chat 候选，固定 Chat 链路则返回带参数路径的 unsupported_response_parameter。store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议，链路不可用时不跨平台续接；未提供 previous_response_id 时调用方历史权威，Gateway 不追加本地历史。
// @Tags responses
// @Accept json
// @Produce json
// @Produce text/event-stream
// @Security BearerAuth
// @Param input body ResponsesRequest true "Responses 请求；Chat 回退支持 function/custom tools 及所有可等价能力"
// @Success 200 {object} ResponsesCompatibleResponse
// @Failure 400 {object} OpenAIErrorEnvelope "invalid_previous_response_id / unsupported_response_parameter"
// @Failure 401 {object} OpenAIErrorEnvelope
// @Failure 402 {object} OpenAIErrorEnvelope
// @Failure 403 {object} OpenAIErrorEnvelope
// @Failure 503 {object} OpenAIErrorEnvelope "response_chain_unavailable"
// @Router /api/v1/responses [post]
func openAIResponsesDoc() {}

// openAIEmbeddingsDoc godoc
// @Summary 创建 OpenAI Embeddings
// @Tags embeddings
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body TaskRequest true "OpenAI Embeddings 请求"
// @Success 200 {object} CompatibleResponse
// @Failure 400 {object} OpenAIErrorEnvelope
// @Failure 401 {object} OpenAIErrorEnvelope
// @Failure 429 {object} OpenAIErrorEnvelope
// @Failure 502 {object} OpenAIErrorEnvelope
// @Router /api/v1/embeddings [post]
func openAIEmbeddingsDoc() {}

// openAIImagesDoc godoc
// @Summary 创建或编辑 OpenAI Images
// @Description 默认同步返回 OpenAI-compatible Images 响应；非文本模型并发饱和且配置队列时等待准入，超过最长等待返回 504 queue_timeout；设置 X-Async=true 时异步创建任务并返回 202。
// @Tags images
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body TaskRequest true "OpenAI Images 请求"
// @Param X-Async header bool false "true 时异步创建任务并返回 202"
// @Success 200 {object} OpenAIImagesResponse
// @Success 202 {object} TaskAcceptedResponse
// @Failure 400 {object} OpenAIErrorEnvelope
// @Failure 401 {object} OpenAIErrorEnvelope
// @Failure 429 {object} OpenAIErrorEnvelope
// @Failure 504 {object} OpenAIErrorEnvelope
// @Failure 502 {object} OpenAIErrorEnvelope
// @Router /api/v1/images/generations [post]
// @Router /api/v1/images/edits [post]
func openAIImagesDoc() {}

// easyAIMediaTasksDoc godoc
// @Summary 创建 EasyAI 媒体任务
// @Description 默认同步返回 EasyAI GeneratedResponse；非文本模型并发饱和且配置队列时等待准入，超过最长等待返回 504 queue_timeout；设置 X-Async=true 时返回同时兼容 Gateway 与 server-main EasyAIClient 的异步提交结构。
// @Tags media
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param X-Async header bool false "true 时异步创建任务并返回 202"
// @Param Idempotency-Key header string false "可选请求幂等键；同一用户范围内唯一"
// @Param input body TaskRequest true "媒体任务请求，字段随任务类型变化"
// @Success 200 {object} EasyAIGeneratedResponse
// @Success 202 {object} TaskAcceptedResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 402 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 429 {object} ErrorEnvelope
// @Failure 504 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/videos/generations [post]
// @Router /api/v1/song/generations [post]
// @Router /api/v1/music/generations [post]
// @Router /api/v1/speech/generations [post]
// @Router /api/v1/voice_clone [post]
func easyAIMediaTasksDoc() {}

func (s *Server) requestExecutionContext(r *http.Request) (context.Context, context.CancelFunc) {
	if s.ctx == nil {
		return runner.NewRequestExecutionContext(r.Context(), context.Background())
	}
	return runner.NewRequestExecutionContext(r.Context(), s.ctx)
}

func requestStillConnected(r *http.Request) bool {
	select {
	case <-r.Context().Done():
		return false
	default:
		return true
	}
}

type taskExecutor interface {
	Execute(context.Context, store.GatewayTask, *auth.User) (runner.Result, error)
	ExecuteStream(context.Context, store.GatewayTask, *auth.User, clients.StreamDelta) (runner.Result, error)
}

func writeCompatibleTaskResponse(runCtx context.Context, w http.ResponseWriter, r *http.Request, executor taskExecutor, kind string, model string, task store.GatewayTask, user *auth.User, streamMode bool, includeUsage bool) {
	writeProtocolCompatibleTaskResponse(runCtx, w, r, executor, kind, model, "", task, user, streamMode, includeUsage, nil)
}

func writeProtocolCompatibleTaskResponse(runCtx context.Context, w http.ResponseWriter, r *http.Request, executor taskExecutor, kind string, model string, targetProtocol string, task store.GatewayTask, user *auth.User, streamMode bool, includeUsage bool, base64Observer interface{ ObserveSynchronousBase64(string) }) {
	if targetProtocol == "" || gatewayAPIV1Request(r) {
		w.Header().Set("X-Gateway-Task-Id", task.ID)
	}
	if streamMode {
		flusher := prepareCompatibleStream(w)
		streamWriter := newCompatibleStreamWriter(kind, model, includeUsage)
		nativeStreamPassthrough := false
		result, runErr := executor.ExecuteStream(runCtx, task, user, func(delta clients.StreamDeltaEvent) error {
			if !requestStillConnected(r) {
				return nil
			}
			if targetProtocol != "" && delta.Event != nil && delta.WireProtocol == targetProtocol && !delta.WireConverted {
				for name, values := range delta.WireHeaders {
					for _, value := range values {
						if strings.EqualFold(name, "Content-Type") {
							w.Header().Set(name, value)
						} else {
							w.Header().Add(name, value)
						}
					}
				}
				nativeStreamPassthrough = true
				if kind == "responses" {
					eventType, _ := delta.Event["type"].(string)
					sendSSE(w, eventType, delta.Event)
				} else {
					raw, _ := json.Marshal(delta.Event)
					_, _ = fmt.Fprintf(w, "data: %s\n\n", raw)
				}
				if flusher != nil {
					flusher.Flush()
				}
				return nil
			}
			streamWriter.writeDelta(w, delta)
			if flusher != nil {
				flusher.Flush()
			}
			return nil
		})
		if runErr != nil {
			if !requestStillConnected(r) {
				return
			}
			applyRunErrorHeaders(w, runErr)
			errorPayload := map[string]any{"message": runErrorMessage(runErr), "type": "server_error", "param": nil, "code": runErrorCode(runErr)}
			if status := statusFromRunError(runErr); status >= 400 && status < 500 {
				errorPayload["type"] = "invalid_request_error"
			}
			if param := runErrorDetails(runErr)["param"]; param != nil {
				errorPayload["param"] = param
			}
			sendSSE(w, "error", map[string]any{"error": errorPayload})
			if flusher != nil {
				flusher.Flush()
			}
			return
		}
		if !requestStillConnected(r) {
			return
		}
		if nativeStreamPassthrough {
			if kind != "responses" {
				_, _ = fmt.Fprint(w, "data: [DONE]\n\n")
			}
			if flusher != nil {
				flusher.Flush()
			}
			return
		}
		streamWriter.writeDone(w, result.Output)
		if flusher != nil {
			flusher.Flush()
		}
		return
	}

	releaseInlineResponse, limitErr := acquireSynchronousInlineResponseSlot(runCtx, kind, task.Request)
	if limitErr != nil {
		observeSynchronousBase64(base64Observer, "capacity_timeout")
		writeProtocolError(w, targetProtocol, statusFromRunError(limitErr), runErrorMessage(limitErr), runErrorDetails(limitErr), runErrorCode(limitErr))
		return
	}
	defer releaseInlineResponse()
	result, runErr := executor.Execute(runCtx, task, user)
	if runErr != nil {
		if clients.ErrorCode(runErr) == "response_format_too_large" {
			observeSynchronousBase64(base64Observer, "too_large")
		}
		if !requestStillConnected(r) {
			return
		}
		applyRunErrorHeaders(w, runErr)
		if targetProtocol != "" {
			writeProtocolError(w, targetProtocol, statusFromRunError(runErr), runErrorMessage(runErr), runErrorDetails(runErr), runErrorCode(runErr))
			return
		}
		writeErrorWithDetails(w, statusFromRunError(runErr), runErrorMessage(runErr), runErrorDetails(runErr), runErrorCode(runErr))
		return
	}
	if !requestStillConnected(r) {
		return
	}
	if synchronousInlineResponseRequested(kind, task.Request) {
		rawBytes := inlineMediaDecodedSize(result.Output)
		if rawBytes > maxSynchronousInlineResponseBytes {
			observeSynchronousBase64(base64Observer, "too_large")
			writeProtocolError(w, targetProtocol, http.StatusRequestEntityTooLarge, "synchronous Base64 response exceeds the 20 MiB limit", map[string]any{
				"task_id":         result.Task.ID,
				"query_url":       "/api/v1/ai/result/" + result.Task.ID,
				"limit_bytes":     maxSynchronousInlineResponseBytes,
				"actual_bytes":    rawBytes,
				"response_format": "url",
			}, "response_format_too_large")
			return
		}
		if rawBytes > 0 {
			observeSynchronousBase64(base64Observer, "success")
			w.Header().Set("X-Gateway-Response-Format", "b64_json")
		} else {
			w.Header().Set("X-Gateway-Response-Format", "url")
		}
	} else if mediaResultKind(kind) {
		w.Header().Set("X-Gateway-Response-Format", "url")
	}
	if !mediaResultKind(kind) && wireResponseMatches(result.Wire, targetProtocol) {
		writeWireResponse(w, result.Wire)
		return
	}
	writeJSON(w, http.StatusOK, easyAISynchronousTaskResponse(result.Task, result.Output))
}

const maxSynchronousInlineResponseBytes = runner.MaxSynchronousInlineResponseBytes

var synchronousInlineResponseSlots = make(chan struct{}, 2)

func acquireSynchronousInlineResponseSlot(ctx context.Context, kind string, request map[string]any) (func(), error) {
	if !synchronousInlineResponseRequested(kind, request) {
		return func() {}, nil
	}
	select {
	case synchronousInlineResponseSlots <- struct{}{}:
		return func() { <-synchronousInlineResponseSlots }, nil
	case <-ctx.Done():
		return func() {}, &clients.ClientError{Code: "response_format_capacity_timeout", Message: "synchronous Base64 response capacity wait timed out", StatusCode: http.StatusServiceUnavailable, Retryable: true}
	}
}

func observeSynchronousBase64(observer interface{ ObserveSynchronousBase64(string) }, outcome string) {
	if observer != nil {
		observer.ObserveSynchronousBase64(outcome)
	}
}

func synchronousInlineResponseRequested(kind string, request map[string]any) bool {
	if !mediaResultKind(kind) {
		return false
	}
	value, _ := request["response_format"].(string)
	return strings.EqualFold(strings.TrimSpace(value), "b64_json")
}

func mediaResultKind(kind string) bool {
	return strings.HasPrefix(kind, "images.") || strings.HasPrefix(kind, "videos.") ||
		kind == "song.generations" || kind == "music.generations" || kind == "speech.generations"
}

func inlineMediaDecodedSize(value any) int64 {
	switch typed := value.(type) {
	case map[string]any:
		var total int64
		for key, item := range typed {
			normalized := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(key), "-", "_"))
			if raw, ok := item.(string); ok && (normalized == "b64_json" || strings.Contains(normalized, "base64")) {
				total += base64DecodedSize(raw)
				continue
			}
			total += inlineMediaDecodedSize(item)
		}
		return total
	case []any:
		var total int64
		for _, item := range typed {
			total += inlineMediaDecodedSize(item)
		}
		return total
	case string:
		raw := strings.TrimSpace(typed)
		if strings.HasPrefix(strings.ToLower(raw), "data:") {
			if comma := strings.IndexByte(raw, ','); comma >= 0 {
				return base64DecodedSize(raw[comma+1:])
			}
		}
	}
	return 0
}

func base64DecodedSize(value string) int64 {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0
	}
	padding := 0
	if strings.HasSuffix(value, "=") {
		padding++
	}
	if strings.HasSuffix(value, "==") {
		padding++
	}
	return int64(base64.StdEncoding.DecodedLen(len(value)) - padding)
}

func gatewayAPIV1Request(r *http.Request) bool {
	return r != nil && strings.HasPrefix(r.URL.Path, "/api/v1/")
}

func streamIncludeUsage(body map[string]any) bool {
	streamOptions, _ := body["stream_options"].(map[string]any)
	includeUsage, _ := streamOptions["include_usage"].(bool)
	return includeUsage
}

func validateMediaResponseFormat(kind string, body map[string]any) error {
	if !strings.HasPrefix(kind, "images.") {
		return nil
	}
	value, exists := body["response_format"]
	if !exists || value == nil {
		return nil
	}
	format, ok := value.(string)
	if !ok {
		return errors.New("response_format must be url or b64_json")
	}
	switch strings.ToLower(strings.TrimSpace(format)) {
	case "url", "b64_json":
		body["response_format"] = strings.ToLower(strings.TrimSpace(format))
		return nil
	default:
		return errors.New("response_format must be url or b64_json")
	}
}

func asyncRequest(r *http.Request) bool {
	value := strings.TrimSpace(strings.ToLower(r.Header.Get("x-async")))
	return value == "1" || value == "true" || value == "yes" || value == "on"
}

type taskResponsePlan struct {
	asyncMode      bool
	compatibleMode bool
	streamMode     bool
}

func planTaskResponse(kind string, compatible bool, body map[string]any, r *http.Request) taskResponsePlan {
	asyncMode := asyncRequest(r)
	compatibleMode := compatible
	if synchronousCompatibleKind(kind) && !compatible {
		asyncMode = false
		compatibleMode = true
	}
	return taskResponsePlan{
		asyncMode:      asyncMode,
		compatibleMode: compatibleMode,
		streamMode:     streamCompatibleKind(kind) && boolValue(body, "stream"),
	}
}

func synchronousCompatibleKind(kind string) bool {
	return kind == "chat.completions" || kind == "embeddings" || kind == "reranks"
}

func streamCompatibleKind(kind string) bool {
	return kind == "chat.completions" || kind == "responses"
}

func writeTaskAccepted(w http.ResponseWriter, task store.GatewayTask) {
	writeJSON(w, http.StatusAccepted, easyAITaskAcceptedResponse(task))
}

func apiKeyScopeAllowed(user *auth.User, kind string) bool {
	if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
		return true
	}
	return modelaccess.ScopeAllowsTask(user.APIKeyScopes, kind)
}

func requestModelName(body map[string]any) string {
	if body == nil {
		return ""
	}
	return modelNameFromValue(body["model"])
}

func canonicalTaskModelName(kind string, model string) string {
	model = strings.TrimSpace(model)
	if model == "" {
		switch kind {
		case "images.vectorize":
			return "easy-image-vectorizer-1"
		case "videos.upscales":
			return "easy-proteus-standard-4"
		}
	}
	if kind != "videos.generations" {
		return model
	}
	if canonical, ok := canonicalKlingOmniModel(model); ok {
		return canonical
	}
	return model
}

func modelNameFromValue(value any) string {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case map[string]any:
		for _, key := range []string{"modelId", "model_id", "modelName", "model_name", "id", "name"} {
			if model := modelNameFromValue(typed[key]); model != "" {
				return model
			}
		}
	}
	return ""
}

func statusFromRunError(err error) int {
	switch {
	case clients.ErrorCode(err) == "storage_write_failed" || clients.ErrorCode(err) == "storage_read_failed" || clients.ErrorCode(err) == "storage_config_invalid" || clients.ErrorCode(err) == "storage_auth_failed":
		return http.StatusServiceUnavailable
	case clients.ErrorCode(err) == "binary_result_expired":
		return http.StatusGone
	case clients.ErrorCode(err) == "binary_result_corrupted" || clients.ErrorCode(err) == "result_binary_not_materialized":
		return http.StatusInternalServerError
	case clients.ErrorCode(err) == "local_result_storage_unavailable":
		return http.StatusServiceUnavailable
	case clients.ErrorCode(err) == "billing_hold":
		return http.StatusServiceUnavailable
	case runner.IsPricingUnavailable(err):
		return http.StatusServiceUnavailable
	case clients.ErrorCode(err) == "invalid_previous_response_id" || clients.ErrorCode(err) == "response_chain_too_deep" || clients.ErrorCode(err) == "unsupported_model_protocol" || clients.ErrorCode(err) == "unsupported_response_tool" || clients.ErrorCode(err) == "unsupported_response_parameter":
		return http.StatusBadRequest
	case clients.ErrorCode(err) == "response_chain_unavailable":
		return http.StatusServiceUnavailable
	case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "invalid_parameter" || clients.ErrorCode(err) == "unsupported_kind" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable" || clients.ErrorCode(err) == "cloned_voice_platform_unavailable" || clients.ErrorCode(err) == "unsupported_operation" || clients.ErrorCode(err) == "invalid_proxy":
		return http.StatusBadRequest
	case store.ModelCandidateErrorCode(err) == "invalid_parameter":
		return http.StatusBadRequest
	case store.ModelCandidateErrorCode(err) == "model_capability_configuration_error":
		return http.StatusInternalServerError
	case clients.ErrorCode(err) == "cloned_voice_not_found":
		return http.StatusNotFound
	case store.ModelCandidateErrorCode(err) == "platform_cooling_down" || store.ModelCandidateErrorCode(err) == "model_cooling_down":
		return http.StatusTooManyRequests
	case errors.Is(err, store.ErrQueueTimeout) || clients.ErrorCode(err) == "queue_timeout":
		return http.StatusGatewayTimeout
	case errors.Is(err, store.ErrNoModelCandidate):
		return http.StatusNotFound
	case errors.Is(err, store.ErrRateLimited):
		return http.StatusTooManyRequests
	case clients.ErrorCode(err) == "rate_limit":
		return http.StatusTooManyRequests
	case errors.Is(err, store.ErrInsufficientWalletBalance):
		return http.StatusPaymentRequired
	default:
		if status := clients.ErrorResponseMetadata(err).StatusCode; status >= 400 && status <= 599 {
			return status
		}
		return http.StatusBadGateway
	}
}

func runErrorCode(err error) string {
	if runner.IsPricingUnavailable(err) {
		return "pricing_unavailable"
	}
	if errors.Is(err, store.ErrNoModelCandidate) {
		return store.ModelCandidateErrorCode(err)
	}
	return clients.ErrorCode(err)
}

func runErrorMessage(err error) string {
	if err == nil {
		return ""
	}
	if summary := rateLimitErrorSummary(err); summary != "" {
		return err.Error() + "；" + summary
	}
	return err.Error()
}

func runErrorDetails(err error) map[string]any {
	if detail := runner.PricingUnavailableDetails(err); len(detail) > 0 {
		return map[string]any{"pricing": detail}
	}
	if detail := rateLimitErrorDetail(err); len(detail) > 0 {
		return map[string]any{"rateLimit": detail}
	}
	if detail := store.ModelCandidateErrorDetails(err); len(detail) > 0 {
		return detail
	}
	if detail := clients.ErrorDetails(err); len(detail) > 0 {
		return detail
	}
	return nil
}

func applyRunErrorHeaders(w http.ResponseWriter, err error) {
	retryAfter := store.ModelCandidateRetryAfter(err)
	if limitRetryAfter := store.RateLimitRetryAfter(err); limitRetryAfter > 0 {
		retryAfter = limitRetryAfter
	}
	if retryAfter <= 0 {
		if seconds := retryAfterSecondsFromDetails(clients.ErrorDetails(err)); seconds > 0 {
			retryAfter = time.Duration(seconds) * time.Second
		}
	}
	if retryAfter > 0 {
		seconds := int((retryAfter + time.Second - 1) / time.Second)
		if seconds < 1 {
			seconds = 1
		}
		w.Header().Set("Retry-After", strconv.Itoa(seconds))
	}
}

func retryAfterSecondsFromDetails(details map[string]any) int {
	if details == nil {
		return 0
	}
	switch value := details["retryAfterSeconds"].(type) {
	case int:
		return value
	case int32:
		return int(value)
	case int64:
		return int(value)
	case float64:
		return int(value)
	default:
		return 0
	}
}

func rateLimitErrorSummary(err error) string {
	var limitErr *store.RateLimitExceededError
	if !errors.As(err, &limitErr) {
		return ""
	}
	scopeLabel := "限流对象"
	switch limitErr.ScopeType {
	case "user_group":
		scopeLabel = "用户组"
	case "platform_model":
		scopeLabel = "平台模型"
	}
	scopeName := strings.TrimSpace(limitErr.ScopeName)
	if scopeName == "" {
		scopeName = strings.TrimSpace(limitErr.ScopeKey)
	}
	if groupKey := stringValue(limitErr.ScopeMetadata["groupKey"]); limitErr.ScopeType == "user_group" && groupKey != "" && groupKey != scopeName {
		scopeName = fmt.Sprintf("%s(%s)", scopeName, groupKey)
	}
	projected := limitErr.Projected
	if projected <= 0 {
		projected = limitErr.Current + limitErr.Amount
	}
	parts := []string{
		fmt.Sprintf("限流摘要：%s %s 的 %s 超限", scopeLabel, scopeName, limitErr.Metric),
		fmt.Sprintf("当前 %s，本次 %s，预计 %s，限制 %s", formatRateLimitValue(limitErr.Current), formatRateLimitValue(limitErr.Amount), formatRateLimitValue(projected), formatRateLimitValue(limitErr.Limit)),
	}
	if limitErr.WindowSeconds > 0 {
		parts = append(parts, fmt.Sprintf("窗口 %d 秒", limitErr.WindowSeconds))
	}
	if limitErr.RetryAfter > 0 {
		parts = append(parts, fmt.Sprintf("约%s后可重试", formatRateLimitDuration(limitErr.RetryAfter)))
	} else if !limitErr.Retryable {
		parts = append(parts, "该请求超过单次限额，不能排队重试")
	}
	return strings.Join(parts, "，")
}

func rateLimitErrorDetail(err error) map[string]any {
	var limitErr *store.RateLimitExceededError
	if !errors.As(err, &limitErr) {
		return nil
	}
	detail := map[string]any{
		"scopeType":     limitErr.ScopeType,
		"scopeKey":      limitErr.ScopeKey,
		"scopeName":     limitErr.ScopeName,
		"metric":        limitErr.Metric,
		"limit":         limitErr.Limit,
		"amount":        limitErr.Amount,
		"current":       limitErr.Current,
		"used":          limitErr.Used,
		"reserved":      limitErr.Reserved,
		"projected":     limitErr.Projected,
		"windowSeconds": limitErr.WindowSeconds,
		"retryable":     limitErr.Retryable,
		"exceeded": map[string]any{
			"metric":    limitErr.Metric,
			"current":   limitErr.Current,
			"amount":    limitErr.Amount,
			"projected": limitErr.Projected,
			"limit":     limitErr.Limit,
		},
	}
	if limitErr.Reason != "" {
		detail["reason"] = limitErr.Reason
	}
	if limitErr.QueueLimit > 0 {
		detail["queueDepth"] = limitErr.QueueDepth
		detail["queueLimit"] = limitErr.QueueLimit
	}
	if limitErr.RetryAfter > 0 {
		detail["retryAfterMs"] = limitErr.RetryAfter.Milliseconds()
	}
	if !limitErr.ResetAt.IsZero() {
		detail["resetAt"] = limitErr.ResetAt.UTC().Format(time.RFC3339Nano)
	}
	if len(limitErr.Policy) > 0 {
		detail["rateLimitPolicy"] = limitErr.Policy
		if matchedRule := matchedRateLimitRule(limitErr.Policy, limitErr.Metric); len(matchedRule) > 0 {
			detail["matchedRule"] = matchedRule
		}
	}
	if len(limitErr.ScopeMetadata) > 0 {
		detail["scopeMetadata"] = limitErr.ScopeMetadata
	}
	if limitErr.ScopeType == "user_group" {
		userGroup := map[string]any{
			"id":   limitErr.ScopeKey,
			"name": limitErr.ScopeName,
		}
		if groupKey := stringValue(limitErr.ScopeMetadata["groupKey"]); groupKey != "" {
			userGroup["groupKey"] = groupKey
		}
		detail["userGroup"] = userGroup
	}
	return detail
}

func formatRateLimitValue(value float64) string {
	return strconv.FormatFloat(value, 'f', -1, 64)
}

func formatRateLimitDuration(duration time.Duration) string {
	if duration < time.Second {
		return strconv.FormatInt(duration.Milliseconds(), 10) + "毫秒"
	}
	seconds := duration.Seconds()
	return strconv.FormatFloat(seconds, 'f', -1, 64) + "秒"
}

func matchedRateLimitRule(policy map[string]any, metric string) map[string]any {
	rules, _ := policy["rules"].([]any)
	for _, rawRule := range rules {
		rule, _ := rawRule.(map[string]any)
		if stringValue(rule["metric"]) == metric {
			return rule
		}
	}
	return nil
}

// listTasks godoc
// @Summary 列出任务
// @Description 按当前用户列出任务，支持关键字、模型类型、时间范围和分页过滤。
// @Tags tasks
// @Produce json
// @Security BearerAuth
// @Param q query string false "搜索关键字，别名 query"
// @Param modelType query string false "模型类型，别名 type"
// @Param createdFrom query string false "创建时间起点，支持 RFC3339 或日期格式，别名 from"
// @Param createdTo query string false "创建时间终点，支持 RFC3339 或日期格式，别名 to"
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页数量，别名 limit" default(50)
// @Success 200 {object} TaskListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/tasks [get]
// @Router /api/v1/tasks [get]
func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	query := r.URL.Query()
	page, err := positiveQueryInt(query.Get("page"), 1)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid page")
		return
	}
	pageSizeRaw := query.Get("pageSize")
	if pageSizeRaw == "" {
		pageSizeRaw = query.Get("limit")
	}
	pageSize, err := positiveQueryInt(pageSizeRaw, 50)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid pageSize")
		return
	}
	createdFrom, err := parseTaskListTime(query.Get("createdFrom"), query.Get("from"))
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid createdFrom")
		return
	}
	createdTo, err := parseTaskListTime(query.Get("createdTo"), query.Get("to"))
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid createdTo")
		return
	}
	result, err := s.store.ListTasks(r.Context(), user, store.TaskListFilter{
		Query:       firstNonEmpty(query.Get("q"), query.Get("query")),
		ModelType:   firstNonEmpty(query.Get("modelType"), query.Get("type")),
		CreatedFrom: createdFrom,
		CreatedTo:   createdTo,
		Page:        page,
		PageSize:    pageSize,
	})
	if err != nil {
		s.logger.Error("list tasks failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list tasks failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"items":    publicTaskList(result.Items),
		"total":    result.Total,
		"page":     result.Page,
		"pageSize": result.PageSize,
	})
}

func positiveQueryInt(raw string, fallback int) (int, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return fallback, nil
	}
	value, err := strconv.Atoi(raw)
	if err != nil || value <= 0 {
		return 0, fmt.Errorf("invalid positive integer")
	}
	return value, nil
}

func parseTaskListTime(values ...string) (*time.Time, error) {
	raw := strings.TrimSpace(firstNonEmpty(values...))
	if raw == "" {
		return nil, nil
	}
	layouts := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04", "2006-01-02 15:04:05", "2006-01-02"}
	var lastErr error
	for _, layout := range layouts {
		parsed, err := time.ParseInLocation(layout, raw, time.Local)
		if err == nil {
			return &parsed, nil
		}
		lastErr = err
	}
	return nil, lastErr
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func boolValue(body map[string]any, key string) bool {
	value, _ := body[key].(bool)
	return value
}

// getTask godoc
// @Summary 获取任务详情
// @Description 返回指定任务的请求、状态、输出和执行摘要。
// @Tags tasks
// @Produce json
// @Security BearerAuth
// @Param taskID path string true "任务 ID"
// @Success 200 {object} store.GatewayTask
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 410 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/tasks/{taskID} [get]
// @Router /api/v1/tasks/{taskID} [get]
func (s *Server) getTask(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
	if err == nil {
		if !runner.TaskAccessibleToUser(task, user) {
			writeError(w, http.StatusNotFound, "task not found")
			return
		}
		cancelState := runner.DescribeTaskCancellation(task)
		task.Cancellable = &cancelState.Cancellable
		task.Submitted = &cancelState.Submitted
		task.Message = cancelState.Message
		task, err = s.hydrateTaskResult(r.Context(), task)
		if err != nil {
			writeStoredBinaryResultError(w, err)
			return
		}
		writeJSON(w, http.StatusOK, publicGatewayTask(task))
		return
	}
	if store.IsNotFound(err) {
		writeError(w, http.StatusNotFound, "task not found")
		return
	}
	s.logger.Error("get task failed", "error", err)
	writeError(w, http.StatusInternalServerError, "get task failed")
}

// cancelTask godoc
// @Summary 取消异步任务
// @Description 仅取消仍在网关本地排队且尚未提交上游的任务；任务已运行、已提交上游或已结束时返回不可取消，客户端应继续查询结果。
// @Tags tasks
// @Produce json
// @Security BearerAuth
// @Param taskID path string true "任务 ID"
// @Success 200 {object} TaskCancelResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/tasks/{taskID}/cancel [post]
// @Router /api/v1/tasks/{taskID}/cancel [post]
func (s *Server) cancelTask(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	result, err := s.runner.CancelTask(r.Context(), r.PathValue("taskID"), user)
	if err == nil {
		writeJSON(w, http.StatusOK, result)
		return
	}
	if store.IsNotFound(err) {
		writeError(w, http.StatusNotFound, "task not found")
		return
	}
	if errors.Is(err, runner.ErrTaskAccessDenied) {
		writeError(w, http.StatusForbidden, "task access denied")
		return
	}
	s.logger.Error("cancel task failed", "error", err)
	writeError(w, http.StatusInternalServerError, "cancel task failed")
}

// taskParamPreprocessing godoc
// @Summary 获取任务参数预处理日志
// @Description 返回指定任务在执行前的参数改写、校验或模板处理日志。
// @Tags tasks
// @Produce json
// @Security BearerAuth
// @Param taskID path string true "任务 ID"
// @Success 200 {object} TaskParamPreprocessingLogListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/tasks/{taskID}/param-preprocessing [get]
// @Router /api/v1/tasks/{taskID}/param-preprocessing [get]
func (s *Server) taskParamPreprocessing(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
	if err != nil {
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "task not found")
			return
		}
		s.logger.Error("get task failed", "error", err)
		writeError(w, http.StatusInternalServerError, "get task failed")
		return
	}
	if !runner.TaskAccessibleToUser(task, user) {
		writeError(w, http.StatusNotFound, "task not found")
		return
	}
	logs, err := s.store.ListTaskParamPreprocessingLogs(r.Context(), task.ID)
	if err != nil {
		s.logger.Error("list task parameter preprocessing logs failed", "taskID", task.ID, "error", err)
		writeError(w, http.StatusInternalServerError, "list task parameter preprocessing logs failed")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"items": logs})
}

// taskEvents godoc
// @Summary 订阅任务事件
// @Description 以 text/event-stream 返回指定任务的历史事件；无事件时返回 task.accepted 占位事件。
// @Tags tasks
// @Produce text/event-stream
// @Security BearerAuth
// @Param taskID path string true "任务 ID"
// @Success 200 {string} string "Server-Sent Events，data 为 store.TaskEvent 或 TaskAcceptedEvent"
// @Failure 401 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/workspace/tasks/{taskID}/events [get]
// @Router /api/v1/tasks/{taskID}/events [get]
func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
	if err != nil {
		if store.IsNotFound(err) {
			writeError(w, http.StatusNotFound, "task not found")
			return
		}
		writeError(w, http.StatusInternalServerError, "get task failed")
		return
	}
	if !runner.TaskAccessibleToUser(task, user) {
		writeError(w, http.StatusNotFound, "task not found")
		return
	}

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	events, err := s.store.ListTaskEvents(r.Context(), task.ID)
	if err != nil {
		s.logger.Error("list task events failed", "error", err)
		return
	}
	for _, event := range events {
		sendSSE(w, event.EventType, event)
		if flusher, ok := w.(http.Flusher); ok {
			flusher.Flush()
		}
	}
	if len(events) == 0 {
		sendSSE(w, "task.accepted", map[string]any{
			"taskId": task.ID,
			"status": task.Status,
		})
	}
}

func runModeFromRequest(body map[string]any) string {
	if value, ok := body["runMode"].(string); ok {
		return value
	}
	if value, ok := body["mode"].(string); ok {
		return value
	}
	if value, ok := body["simulation"].(bool); ok && value {
		return "simulation"
	}
	if value, ok := body["testMode"].(bool); ok && value {
		return "simulation"
	}
	return ""
}
