package httpapi

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"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/runner"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const seedancePortraitAssetCategory = "seedance_portrait_asset"

// getSeedancePortraitAssetCapability godoc
// @Summary 查询 Seedance 真人资产能力
// @Description 返回当前网关是否已配置可创建、同步和引用的火山 Seedance 真人资产平台。
// @Tags portrait-assets
// @Produce json
// @Security BearerAuth
// @Success 200 {object} runner.PortraitAssetCapability
// @Router /api/v1/resource/material/seedance-portrait-assets/capability [get]
func (s *Server) getSeedancePortraitAssetCapability(w http.ResponseWriter, r *http.Request) {
	capability, err := s.runner.PortraitAssetCapability(r.Context())
	if err != nil {
		s.logger.Error("get portrait asset capability failed", "error", err)
		writeError(w, http.StatusInternalServerError, "get portrait asset capability failed")
		return
	}
	writeJSON(w, http.StatusOK, capability)
}

// listSeedancePortraitAssets godoc
// @Summary 列出 Seedance 真人资产
// @Description 返回当前用户的真人资产；兼容 server-main material 列表响应字段。
// @Tags portrait-assets
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]any
// @Router /api/v1/resource/material/user/materials [get]
func (s *Server) listSeedancePortraitAssets(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok || user == nil {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	if category := strings.TrimSpace(r.URL.Query().Get("category")); category != seedancePortraitAssetCategory {
		writeError(w, http.StatusNotFound, "material category not found")
		return
	}
	items, err := s.store.ListPortraitAssets(r.Context(), user, store.PortraitAssetListFilter{
		Keyword:    r.URL.Query().Get("keyword"),
		SourceType: firstNonEmptyQuery(r, "fileType", "sourceType"),
		Page:       portraitAssetQueryInt(r, "pageNumber", "page"),
		PageSize:   portraitAssetQueryInt(r, "pageSize"),
	})
	if err != nil {
		s.logger.Error("list portrait assets failed", "error", err)
		writeError(w, http.StatusInternalServerError, "list portrait assets failed")
		return
	}
	responseItems := make([]any, 0, len(items.Items))
	for _, item := range items.Items {
		responseItems = append(responseItems, s.portraitAssetResponse(r, item))
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"data":     responseItems,
		"total":    items.Total,
		"page":     items.Page,
		"pageSize": items.PageSize,
	})
}

// createSeedancePortraitAsset godoc
// @Summary 上传并创建 Seedance 真人资产
// @Description 文件先写入网关文件存储；仅在 private_avatar_eligible=true 时登记到火山 Assets。创建后会立即触发一次状态同步。
// @Tags portrait-assets
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "真人资产源文件（图片、视频或音频）"
// @Param data formData string true "material JSON，category 必须是 seedance_portrait_asset"
// @Success 200 {object} map[string]any
// @Router /api/v1/resource/material [post]
func (s *Server) createSeedancePortraitAsset(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok || user == nil {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, maxGatewayUploadBytes)
	if err := r.ParseMultipartForm(multipartTaskMemoryBytes); err != nil {
		writeError(w, http.StatusBadRequest, "invalid multipart form-data body")
		return
	}
	if r.MultipartForm != nil {
		defer r.MultipartForm.RemoveAll()
	}
	var data map[string]any
	if err := json.Unmarshal([]byte(strings.TrimSpace(r.FormValue("data"))), &data); err != nil || data == nil {
		writeError(w, http.StatusBadRequest, "data must be a JSON object")
		return
	}
	if strings.TrimSpace(portraitAssetString(data["category"])) != seedancePortraitAssetCategory {
		writeError(w, http.StatusBadRequest, "category must be seedance_portrait_asset")
		return
	}
	privateEligible, _ := data["private_avatar_eligible"].(bool)
	if !privateEligible {
		writeError(w, http.StatusBadRequest, "private_avatar_eligible must be true after the user confirms authorization", "portrait_asset_authorization_required")
		return
	}
	file, header, err := r.FormFile("file")
	if err != nil {
		writeError(w, http.StatusBadRequest, "file is required")
		return
	}
	defer file.Close()
	payload, err := io.ReadAll(file)
	if err != nil {
		writeError(w, http.StatusBadRequest, "read portrait asset file failed")
		return
	}
	contentType := strings.TrimSpace(header.Header.Get("Content-Type"))
	if contentType == "" && len(payload) > 0 {
		contentType = http.DetectContentType(payload)
	}
	sourceType := strings.ToLower(strings.TrimSpace(firstNonEmpty(portraitAssetString(data["fileType"]), portraitAssetString(data["sourceType"]))))
	if !portraitAssetSourceMatchesContentType(sourceType, contentType) {
		writeError(w, http.StatusBadRequest, "fileType must be image, video, or audio and match the uploaded file", "portrait_asset_unsupported_type")
		return
	}
	upload, err := s.runner.UploadFile(r.Context(), runner.FileUploadPayload{
		Bytes: payload, ContentType: contentType, FileName: header.Filename, Source: "seedance-portrait-asset", Scene: store.FileStorageSceneUpload,
	})
	if err != nil {
		s.logger.Error("upload portrait asset failed", "error", err)
		writeError(w, http.StatusBadGateway, err.Error(), clients.ErrorCode(err))
		return
	}
	url := strings.TrimSpace(portraitAssetString(upload["url"]))
	if url == "" {
		writeError(w, http.StatusBadGateway, "portrait asset upload returned no URL", "portrait_asset_source_url_required")
		return
	}
	digest := sha256.Sum256(payload)
	asset, reused, err := s.runner.CreatePortraitAsset(r.Context(), user, runner.PortraitAssetCreateInput{
		Name:                  strings.TrimSpace(portraitAssetString(data["name"])),
		Description:           strings.TrimSpace(portraitAssetString(data["description"])),
		SourceType:            sourceType,
		URL:                   url,
		Preview:               firstNonEmpty(portraitAssetString(data["preview"]), url),
		MimeType:              contentType,
		ByteSize:              int64(len(payload)),
		SourceSHA256:          hex.EncodeToString(digest[:]),
		PrivateAvatarEligible: privateEligible,
		Metadata: map[string]any{
			"tags":               data["tags"],
			"materialGroupId":    data["material_group_id"],
			"uploadedFileName":   header.Filename,
			"uploadAssetStorage": upload["assetStorage"],
		},
	})
	if err != nil {
		writePortraitAssetError(w, err)
		return
	}
	_, _ = s.runner.SyncPortraitAssets(r.Context(), user, []string{asset.ID})
	asset, _, err = s.refreshPortraitAssetForResponse(r, user, asset.ID, asset)
	if err != nil {
		s.logger.Error("refresh portrait asset after create failed", "error", err)
		writeError(w, http.StatusInternalServerError, "refresh portrait asset failed")
		return
	}
	response := map[string]any{"asset": s.portraitAssetResponse(r, asset)}
	if reused {
		response["dedupe"] = map[string]any{"reused": true, "code": "PORTRAIT_ASSET_REUSED", "reason": "same_source", "message": "已复用相同源文件的真人资产，并触发状态刷新。"}
	}
	writeJSON(w, http.StatusOK, response)
}

// syncSeedancePortraitAssets godoc
// @Summary 同步 Seedance 真人资产状态
// @Description 调用火山 CreateAsset/GetAsset；多次调用可把 Processing 状态刷新为 Active 或 Failed。
// @Tags portrait-assets
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} runner.PortraitAssetSyncResponse
// @Router /api/v1/resource/material/seedance-portrait-assets/sync [post]
func (s *Server) syncSeedancePortraitAssets(w http.ResponseWriter, r *http.Request) {
	user, ok := auth.UserFromContext(r.Context())
	if !ok || user == nil {
		writeError(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	var request struct {
		IDs []string `json:"ids"`
	}
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json body")
		return
	}
	if len(request.IDs) == 0 {
		writeJSON(w, http.StatusOK, runner.PortraitAssetSyncResponse{SyncedIDs: []string{}, Skipped: []runner.PortraitAssetIssue{}, Failed: []runner.PortraitAssetIssue{}, Assets: []store.PortraitAsset{}})
		return
	}
	response, err := s.runner.SyncPortraitAssets(r.Context(), user, request.IDs)
	if err != nil {
		s.logger.Error("sync portrait assets failed", "error", err)
		writePortraitAssetError(w, err)
		return
	}
	assets := make([]any, 0, len(response.Assets))
	for _, asset := range response.Assets {
		assets = append(assets, s.portraitAssetResponse(r, asset))
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"requested": response.Requested, "accepted": response.Accepted, "syncedIds": response.SyncedIDs,
		"skipped": response.Skipped, "failed": response.Failed, "assets": assets,
	})
}

func (s *Server) refreshPortraitAssetForResponse(r *http.Request, user *auth.User, assetID string, fallback store.PortraitAsset) (store.PortraitAsset, bool, error) {
	asset, found, err := s.store.FindPortraitAssetForUser(r.Context(), user, assetID)
	if err != nil || !found {
		return fallback, found, err
	}
	return asset, true, nil
}

func (s *Server) portraitAssetResponse(r *http.Request, asset store.PortraitAsset) map[string]any {
	active, total, lastError, updatedAt, err := s.store.PortraitAssetBindingSummary(r.Context(), asset.ID)
	if err != nil {
		active, total, lastError, updatedAt = 0, 0, asset.LastError, asset.UpdatedAt.UTC().Format(time.RFC3339Nano)
	}
	summaryStatus := asset.Status
	if summaryStatus == "not_synced" && total == 0 {
		summaryStatus = "not_synced"
	}
	response := map[string]any{
		"id": asset.ID, "name": asset.Name, "description": asset.Description, "url": asset.URL, "preview": firstNonEmpty(asset.Preview, asset.URL),
		"type": "personal", "fileType": asset.SourceType, "sourceType": asset.SourceType, "size": asset.ByteSize,
		"privateAvatarEligible": asset.PrivateAvatarEligible,
		"createdAt":             asset.CreatedAt.UTC().Format(time.RFC3339Nano), "updatedAt": asset.UpdatedAt.UTC().Format(time.RFC3339Nano),
		"seedanceAssetSummary": map[string]any{
			"eligible": asset.PrivateAvatarEligible, "status": summaryStatus, "provider": "volces", "activePlatformCount": active,
			"totalPlatformCount": total, "sourceType": asset.SourceType, "updatedAt": updatedAt,
		},
	}
	if lastError != "" {
		response["seedanceAssetSummary"].(map[string]any)["lastError"] = lastError
	}
	if asset.SourceType == "image" {
		response["thumbnail"] = firstNonEmpty(asset.Preview, asset.URL)
	}
	return response
}

func writePortraitAssetError(w http.ResponseWriter, err error) {
	status := http.StatusInternalServerError
	if clientErr := clients.ErrorCode(err); clientErr != "client_error" {
		switch clientErr {
		case "portrait_asset_not_found":
			status = http.StatusNotFound
		case "portrait_asset_processing":
			status = http.StatusServiceUnavailable
		case "portrait_asset_authorization_required", "portrait_asset_unsupported_type", "portrait_asset_source_url_required", "portrait_asset_id_required", "portrait_asset_unsupported_model", "portrait_asset_audio_only":
			status = http.StatusBadRequest
		}
		writeError(w, status, err.Error(), clientErr)
		return
	}
	writeError(w, status, err.Error())
}

func portraitAssetSourceMatchesContentType(sourceType string, contentType string) bool {
	contentType = strings.ToLower(strings.TrimSpace(contentType))
	switch sourceType {
	case "image":
		return strings.HasPrefix(contentType, "image/")
	case "video":
		return strings.HasPrefix(contentType, "video/")
	case "audio":
		return strings.HasPrefix(contentType, "audio/")
	default:
		return false
	}
}

func portraitAssetQueryInt(r *http.Request, keys ...string) int {
	for _, key := range keys {
		value := strings.TrimSpace(r.URL.Query().Get(key))
		if value == "" {
			continue
		}
		var parsed int
		if _, err := fmt.Sscan(value, &parsed); err == nil {
			return parsed
		}
	}
	return 0
}

func firstNonEmptyQuery(r *http.Request, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(r.URL.Query().Get(key)); value != "" {
			return value
		}
	}
	return ""
}

func portraitAssetString(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	case fmt.Stringer:
		return typed.String()
	default:
		return ""
	}
}
