package service

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"juhe-factory/api/internal/billing"
	"juhe-factory/api/internal/model"
	queuepkg "juhe-factory/api/internal/queue"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

var activeTaskStatuses = []string{"pending_submission", "submitting", "submitted", "processing", "result_ready", "downloading", "cancel_requested"}

func (s *Creative) InsertStoryboard(userID, projectID, storyboardID uuid.UUID, position string) (*model.EpisodeStoryboard, error) {
	if position != "before" && position != "after" {
		return nil, errors.New("插入位置无效")
	}
	created := &model.EpisodeStoryboard{}
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var target model.EpisodeStoryboard
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Table("episode_storyboards sb").Select("sb.*").
			Joins(`JOIN creative_projects p ON p.id=? AND p.user_id=? AND p.deleted_at IS NULL AND
				(sb.project_id=p.id OR EXISTS (SELECT 1 FROM project_episodes e WHERE e.id=sb.episode_id AND e.project_id=p.id AND e.deleted_at IS NULL))`, projectID, userID).
			Where("sb.id=? AND sb.deleted_at IS NULL", storyboardID).Take(&target).Error; err != nil {
			return err
		}
		parentColumn, parentID := storyboardParent(target)
		sequence, startMS := target.SequenceNo, target.StartMS
		if position == "after" {
			sequence, startMS = target.SequenceNo+1, target.EndMS
		}
		if err := tx.Exec(`UPDATE episode_storyboards SET sequence_no=sequence_no+100000
			WHERE `+parentColumn+`=? AND sequence_no>=? AND deleted_at IS NULL`, parentID, sequence).Error; err != nil {
			return err
		}
		if err := tx.Exec(`UPDATE episode_storyboards SET sequence_no=sequence_no-99999,start_ms=start_ms+5000,end_ms=end_ms+5000
			WHERE `+parentColumn+`=? AND sequence_no>=? AND deleted_at IS NULL`, parentID, sequence+100000).Error; err != nil {
			return err
		}
		created = &model.EpisodeStoryboard{
			EpisodeID: target.EpisodeID, ProjectID: target.ProjectID, SequenceNo: sequence, StableKey: "manual-" + uuid.NewString(),
			StartMS: startMS, EndMS: startMS + 5000, DurationSeconds: 5, Title: fmt.Sprintf("分镜 %d", sequence),
			Dialogue: json.RawMessage(`[]`), AssetRefs: json.RawMessage(`[]`), Status: "idle",
		}
		return tx.Create(created).Error
	})
	return created, err
}

func (s *Creative) DeleteStoryboard(userID, projectID, storyboardID uuid.UUID) ([]string, error) {
	media := newDeletionMediaSet()
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var storyboard model.EpisodeStoryboard
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Table("episode_storyboards sb").Select("sb.*").
			Joins(`JOIN creative_projects p ON p.id=? AND p.user_id=? AND p.deleted_at IS NULL AND
				(sb.project_id=p.id OR EXISTS (SELECT 1 FROM project_episodes e WHERE e.id=sb.episode_id AND e.project_id=p.id AND e.deleted_at IS NULL))`, projectID, userID).
			Where("sb.id=? AND sb.deleted_at IS NULL", storyboardID).Take(&storyboard).Error; err != nil {
			return err
		}
		parentColumn, parentID := storyboardParent(storyboard)
		var active int64
		if err := tx.Model(&model.GenerationTask{}).Where("storyboard_id=? AND status IN ?", storyboardID, activeTaskStatuses).Count(&active).Error; err != nil {
			return err
		}
		if active > 0 {
			return errors.New("当前分镜存在进行中的任务，暂时不能删除")
		}
		if err := media.addQuery(tx.Table("media_assets media").Select("DISTINCT media.id,media.object_key").
			Joins("JOIN generation_outputs output ON output.media_asset_id=media.id").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id").
			Where("gt.storyboard_id=? AND (gt.task_type IN ('video_generation','prompt_reverse') OR (gt.task_type='image_generation' AND gt.input_data->>'target_type'='storyboard'))", storyboardID)); err != nil {
			return err
		}
		if storyboard.ThumbnailAssetID != nil {
			if err := media.addQuery(tx.Table("media_assets").Select("id,object_key").Where("id=?", *storyboard.ThumbnailAssetID)); err != nil {
				return err
			}
		}
		storyboardMediaPattern := fmt.Sprintf("juyou_ran/video-redraw/projects/%s/storyboards/%%-%s/%%", projectID, storyboardID)
		if storyboard.EpisodeID != nil {
			storyboardMediaPattern = fmt.Sprintf("juyou_ran/video-redraw/projects/%s/episodes/%s/storyboards/%%-%s/%%", projectID, *storyboard.EpisodeID, storyboardID)
		}
		if err := collectMediaByObjectKey(tx, media, storyboardMediaPattern); err != nil {
			return err
		}
		if err := tx.Model(&storyboard).Update("active_output_id", nil).Error; err != nil {
			return err
		}
		if err := tx.Exec(`DELETE FROM generation_outputs WHERE task_id IN (
			SELECT id FROM generation_tasks WHERE storyboard_id=? AND (task_type IN ('video_generation','prompt_reverse')
			OR (task_type='image_generation' AND input_data->>'target_type'='storyboard')))`, storyboardID).Error; err != nil {
			return err
		}
		if err := tx.Exec(`DELETE FROM generation_tasks WHERE storyboard_id=? AND (task_type IN ('video_generation','prompt_reverse')
			OR (task_type='image_generation' AND input_data->>'target_type'='storyboard'))`, storyboardID).Error; err != nil {
			return err
		}
		if err := tx.Model(&model.GenerationTask{}).Where("storyboard_id=?", storyboardID).Update("storyboard_id", nil).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM episode_storyboards WHERE id=?", storyboardID).Error; err != nil {
			return err
		}
		if err := tx.Exec(`UPDATE episode_storyboards SET sequence_no=sequence_no+100000
			WHERE `+parentColumn+`=? AND sequence_no>? AND deleted_at IS NULL`, parentID, storyboard.SequenceNo).Error; err != nil {
			return err
		}
		shiftMS := int64(storyboard.DurationSeconds * 1000)
		if err := tx.Exec(`UPDATE episode_storyboards SET sequence_no=sequence_no-100001,
			start_ms=GREATEST(0,start_ms-?),end_ms=GREATEST(1,end_ms-?)
			WHERE `+parentColumn+`=? AND sequence_no>? AND deleted_at IS NULL`, shiftMS, shiftMS, parentID, storyboard.SequenceNo+100000).Error; err != nil {
			return err
		}
		return media.deleteRows(tx)
	})
	return media.objectKeys(), err
}

func storyboardParent(storyboard model.EpisodeStoryboard) (string, uuid.UUID) {
	if storyboard.ProjectID != nil {
		return "project_id", *storyboard.ProjectID
	}
	return "episode_id", *storyboard.EpisodeID
}

func (s *Creative) DeleteEpisode(userID, projectID, episodeID uuid.UUID) ([]string, error) {
	media := newDeletionMediaSet()
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var episode model.ProjectEpisode
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Table("project_episodes e").Select("e.*").
			Joins("JOIN creative_projects p ON p.id=e.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
			Where("e.id=? AND e.project_id=? AND e.deleted_at IS NULL", episodeID, projectID).Take(&episode).Error; err != nil {
			return err
		}
		var active int64
		if err := tx.Model(&model.GenerationTask{}).Where("episode_id=? AND status IN ?", episodeID, activeTaskStatuses).Count(&active).Error; err != nil {
			return err
		}
		if active > 0 {
			return errors.New("剧集存在排队中或处理中的任务，暂时不能删除")
		}
		if err := tx.Model(&model.DramaParseTask{}).Where("episode_id=? AND status IN ?", episodeID, []string{"queued", "running", "retry_wait", "cancel_requested"}).Count(&active).Error; err != nil {
			return err
		}
		if active > 0 {
			return errors.New("剧集存在进行中的剧本解析任务，暂时不能删除")
		}
		if err := removeEpisodeDerivedContent(tx, projectID, episodeID, media); err != nil {
			return err
		}
		if err := media.addQuery(tx.Table("media_assets media").Select("DISTINCT media.id,media.object_key").
			Joins(`JOIN project_episodes episode ON media.id=episode.cover_asset_id
				OR media.id=episode.source_video_asset_id OR media.id=episode.subtitle_asset_id`).
			Where("episode.id=?", episodeID)); err != nil {
			return err
		}
		// 如果删除的是第一集，将项目封面切换到新的第一集封面。
		if episode.CoverAssetID != nil {
			var nextEpisode struct{ CoverAssetID *uuid.UUID }
			if err := tx.Table("project_episodes").Select("cover_asset_id").Where("project_id=? AND id<>? AND deleted_at IS NULL AND cover_asset_id IS NOT NULL", projectID, episodeID).
				Order("episode_no").Limit(1).Find(&nextEpisode).Error; err != nil {
				return err
			}
			if err := tx.Model(&model.CreativeProject{}).Where("id=?", projectID).Update("cover_asset_id", nextEpisode.CoverAssetID).Error; err != nil {
				return err
			}
		}
		episodeMediaPattern := fmt.Sprintf("juyou_ran/video-redraw/projects/%s/episodes/%%%s/%%", projectID, episodeID)
		if err := collectMediaByObjectKey(tx, media, episodeMediaPattern); err != nil {
			return err
		}
		if err := tx.Exec("UPDATE generation_tasks SET episode_id=NULL WHERE episode_id=?", episodeID).Error; err != nil {
			return err
		}
		if err := tx.Exec("UPDATE project_assets SET source_episode_id=NULL WHERE source_episode_id=?", episodeID).Error; err != nil {
			return err
		}
		if err := tx.Exec("UPDATE drama_parse_batches SET current_episode_id=NULL WHERE current_episode_id=?", episodeID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM project_episodes WHERE id=?", episodeID).Error; err != nil {
			return err
		}
		return media.deleteRows(tx)
	})
	return media.objectKeys(), err
}

// ResetEpisodeAnalysis permanently removes generated storyboards, prompts and their task history.
// Project assets are intentionally left untouched so they can be reused for a new analysis.
func (s *Creative) ResetEpisodeAnalysis(userID, projectID, episodeID uuid.UUID) ([]string, error) {
	media := newDeletionMediaSet()
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var episode model.ProjectEpisode
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Table("project_episodes e").Select("e.*").
			Joins("JOIN creative_projects p ON p.id=e.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
			Where("e.id=? AND e.project_id=? AND e.deleted_at IS NULL", episodeID, projectID).Take(&episode).Error; err != nil {
			return err
		}
		var active int64
		if err := tx.Model(&model.GenerationTask{}).Where("episode_id=? AND status IN ?", episodeID, activeTaskStatuses).Count(&active).Error; err != nil {
			return err
		}
		if active > 0 {
			return errors.New("当前剧集存在进行中的生成任务，暂时不能重新解析")
		}
		if err := tx.Model(&model.DramaParseTask{}).Where("episode_id=? AND status IN ?", episodeID, []string{"queued", "running", "retry_wait", "cancel_requested"}).Count(&active).Error; err != nil {
			return err
		}
		if active > 0 {
			return errors.New("当前剧集存在进行中的剧本解析任务，暂时不能重新解析")
		}
		if err := removeEpisodeDerivedContent(tx, projectID, episodeID, media); err != nil {
			return err
		}
		if err := media.deleteRows(tx); err != nil {
			return err
		}
		return tx.Model(&episode).Updates(map[string]any{"status": "uploaded", "analysis_message": nil, "redraw_script": nil}).Error
	})
	return media.objectKeys(), err
}

func (s *Creative) ListTasks(userID, projectID uuid.UUID, episodeID *uuid.UUID) ([]map[string]any, error) {
	items := make([]map[string]any, 0)
	query := s.DB.Table("generation_tasks gt").
		Select(`gt.id,gt.request_id,gt.task_type,gt.status,gt.episode_id,gt.storyboard_id,gt.input_data->>'mode' AS mode,gt.input_data->>'phase_started_at' AS phase_started_at,gt.estimated_points::text AS estimated_points,
			gt.actual_points::text AS actual_points,gt.error_code,gt.error_message,gt.created_at,gt.submitted_at,gt.finished_at,
			coalesce(sb.sequence_no,(gt.input_data->>'current_sequence')::int,0) AS sequence_no,gt.input_data->>'asset_id' AS asset_id,media.public_url AS result_url,
			(SELECT max(progress.created_at) FROM generation_outputs progress WHERE progress.task_id=gt.id) AS last_result_at`).
		Joins("JOIN creative_projects p ON p.id=gt.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
		Joins("LEFT JOIN episode_storyboards sb ON sb.id=gt.storyboard_id").
		Joins("LEFT JOIN generation_outputs output ON output.task_id=gt.id AND output.sequence_no=1").
		Joins("LEFT JOIN media_assets media ON media.id=output.media_asset_id AND media.deleted_at IS NULL").
		Where("gt.project_id=?", projectID)
	if episodeID != nil {
		query = query.Where("gt.episode_id=?", *episodeID)
	}
	return items, query.Order("gt.created_at DESC").Limit(500).Find(&items).Error
}

func (s *Creative) CancelTask(userID, projectID, taskID uuid.UUID) error {
	var channelID *uuid.UUID
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var task model.GenerationTask
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=? AND project_id=? AND user_id=?", taskID, projectID, userID).First(&task).Error; err != nil {
			return err
		}
		var projectType string
		if err := tx.Table("creative_projects").Where("id=?", projectID).Pluck("project_type", &projectType).Error; err != nil {
			return err
		}
		channelID = task.ChannelID
		if task.TaskType == "prompt_reverse" && (task.Status == "pending_submission" || task.Status == "submitted" || task.Status == "processing" || task.Status == "cancel_requested") {
			return cancelPromptReverseTask(tx, &task)
		}
		switch task.Status {
		case "pending_submission":
			refunded := false
			chargeCancellation := task.TaskType == "video_generation" || projectType == "premium_drama"
			if !chargeCancellation {
				var err error
				refunded, err = billing.RefundGenerationTask(tx, &task, "视频生成失败返还")
				if err != nil {
					return err
				}
			}
			actual := "0.00"
			if chargeCancellation {
				actual = task.PrepaidPoints
			}
			updates := map[string]any{"status": "cancelled", "actual_points": actual, "finished_at": time.Now(), "error_code": nil, "error_message": "用户取消"}
			if refunded {
				updates["cost_refunded"] = true
			}
			if err := tx.Model(&task).Updates(updates).Error; err != nil {
				return err
			}
			return resetRelatedStatus(tx, task)
		case "submitted", "processing":
			if task.TaskType != "prompt_reverse" && strings.TrimSpace(task.UpstreamTaskID) == "" {
				return errors.New("上游任务标识尚未就绪，请稍后重试取消")
			}
			return tx.Model(&task).Updates(map[string]any{"status": "cancel_requested", "error_code": nil, "error_message": "等待当前处理安全结束"}).Error
		case "submitting":
			return errors.New("任务正在提交，请稍后重试取消")
		case "cancel_requested":
			return nil
		case "result_ready", "downloading", "succeeded":
			return errors.New("生成已经完成，不能取消")
		case "failed", "cancelled":
			return nil
		default:
			return fmt.Errorf("任务状态 %s 不允许取消", task.Status)
		}
	})
	if err == nil && channelID != nil && s.Queue != nil {
		_ = queuepkg.EnqueueID(s.Queue, queuepkg.TypeDispatchChannel, *channelID, 0)
	}
	return err
}

func cancelPromptReverseTask(tx *gorm.DB, task *model.GenerationTask) error {
	var input struct {
		Mode string `json:"mode"`
	}
	_ = json.Unmarshal(task.InputData, &input)
	updates := map[string]any{
		"status": "cancelled", "actual_points": task.PrepaidPoints, "finished_at": time.Now(),
		"error_code": nil, "error_message": "用户取消，已扣积分不退",
	}
	if err := tx.Model(task).Updates(updates).Error; err != nil {
		return err
	}
	if task.StoryboardID != nil {
		return nil
	}
	if task.EpisodeID == nil && task.ProjectID != nil {
		if input.Mode == "script" {
			return tx.Model(&model.CreativeProject{}).Where("id=?", *task.ProjectID).
				Updates(map[string]any{"redraw_status": "review", "analysis_message": "剧本反推已取消"}).Error
		}
		var storyboardCount int64
		if err := tx.Model(&model.EpisodeStoryboard{}).Where("project_id=? AND deleted_at IS NULL", *task.ProjectID).Count(&storyboardCount).Error; err != nil {
			return err
		}
		status := "uploaded"
		if storyboardCount > 0 {
			status = "review"
		}
		return tx.Model(&model.CreativeProject{}).Where("id=?", *task.ProjectID).Updates(map[string]any{"redraw_status": status, "analysis_message": "视频分析已取消"}).Error
	}
	if task.EpisodeID == nil {
		return nil
	}
	var storyboardCount int64
	if err := tx.Model(&model.EpisodeStoryboard{}).Where("episode_id=? AND deleted_at IS NULL", *task.EpisodeID).Count(&storyboardCount).Error; err != nil {
		return err
	}
	status := "uploaded"
	if storyboardCount > 0 {
		status = "review"
	}
	return tx.Model(&model.ProjectEpisode{}).Where("id=?", *task.EpisodeID).Updates(map[string]any{"status": status, "analysis_message": "视频分析已取消"}).Error
}

func (s *Creative) failQueuedTask(taskID uuid.UUID, code, message string) error {
	return s.failQueuedTasks([]uuid.UUID{taskID}, code, message)
}

func (s *Creative) failQueuedTasks(taskIDs []uuid.UUID, code, message string) error {
	return s.DB.Transaction(func(tx *gorm.DB) error {
		for _, taskID := range taskIDs {
			var task model.GenerationTask
			if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", taskID).First(&task).Error; err != nil {
				return err
			}
			if task.Status == "succeeded" || task.Status == "failed" || task.Status == "cancelled" {
				continue
			}
			refundRemark := map[string]string{"prompt_reverse": "视频反推失败返还", "script_analysis": "剧本分析失败返还", "image_generation": "图片生成失败返还", "video_generation": "视频生成失败返还"}[task.TaskType]
			var refunded bool
			var err error
			if task.TaskType == "prompt_reverse" || task.TaskType == "script_analysis" {
				refunded, err = billing.RefundTextGenerationTask(tx, &task, refundRemark)
			} else {
				refunded, err = billing.RefundGenerationTask(tx, &task, refundRemark)
			}
			if err != nil {
				return err
			}
			updates := map[string]any{
				"status": "failed", "actual_points": "0.00", "finished_at": time.Now(),
				"error_code": code, "error_message": message,
			}
			if refunded {
				updates["cost_refunded"] = true
			}
			if err := tx.Model(&task).Updates(updates).Error; err != nil {
				return err
			}
			if task.TaskType == "video_generation" && task.StoryboardID != nil {
				if err := tx.Model(&model.EpisodeStoryboard{}).Where("id=?", *task.StoryboardID).Update("status", "idle").Error; err != nil {
					return err
				}
			}
			if task.TaskType == "prompt_reverse" && task.StoryboardID == nil && task.EpisodeID != nil {
				var storyboardCount int64
				if err := tx.Model(&model.EpisodeStoryboard{}).Where("episode_id=? AND deleted_at IS NULL", *task.EpisodeID).Count(&storyboardCount).Error; err != nil {
					return err
				}
				status := "uploaded"
				if storyboardCount > 0 {
					status = "review"
				}
				if err := tx.Model(&model.ProjectEpisode{}).Where("id=?", *task.EpisodeID).Updates(map[string]any{"status": status, "analysis_message": message}).Error; err != nil {
					return err
				}
			} else if task.TaskType == "prompt_reverse" && task.StoryboardID == nil && task.ProjectID != nil {
				var storyboardCount int64
				if err := tx.Model(&model.EpisodeStoryboard{}).Where("project_id=? AND deleted_at IS NULL", *task.ProjectID).Count(&storyboardCount).Error; err != nil {
					return err
				}
				status := "uploaded"
				if storyboardCount > 0 {
					status = "review"
				}
				if err := tx.Model(&model.CreativeProject{}).Where("id=?", *task.ProjectID).Updates(map[string]any{"redraw_status": status, "analysis_message": message}).Error; err != nil {
					return err
				}
			}
		}
		return nil
	})
}

func resetRelatedStatus(tx *gorm.DB, task model.GenerationTask) error {
	if task.TaskType == "video_generation" && task.StoryboardID != nil {
		var active int64
		if err := tx.Model(&model.GenerationTask{}).Where("storyboard_id=? AND task_type='video_generation' AND status IN ?", *task.StoryboardID, activeTaskStatuses).Count(&active).Error; err != nil {
			return err
		}
		if active == 0 {
			return tx.Model(&model.EpisodeStoryboard{}).Where("id=?", *task.StoryboardID).Update("status", "idle").Error
		}
	}
	return nil
}

func (s *Creative) ListStoryboardOutputs(userID, projectID, storyboardID uuid.UUID) ([]map[string]any, error) {
	items := make([]map[string]any, 0)
	err := s.DB.Table("generation_outputs output").
		Select(`output.id,output.media_asset_id,output.output_type,output.sequence_no,output.metadata,output.created_at,output.task_id,
			media.public_url,media.mime_type,media.size_bytes,gt.estimated_points::text AS estimated_points,
			gt.actual_points::text AS actual_points,CASE WHEN gt.task_type='video_generation' THEN sb.active_output_id=output.id ELSE sb.thumbnail_asset_id=output.media_asset_id END AS active,
			CASE WHEN gt.task_type='video_generation' THEN sb.active_output_id=output.id OR coalesce(output.metadata->>'candidate','false')='true' ELSE false END AS candidate`).
		Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND (gt.task_type='video_generation' OR (gt.task_type='image_generation' AND gt.input_data->>'target_type'='storyboard'))", storyboardID).
		Joins("JOIN media_assets media ON media.id=output.media_asset_id AND media.deleted_at IS NULL").
		Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id AND sb.deleted_at IS NULL").
		Joins(`JOIN creative_projects p ON p.id=? AND p.user_id=? AND p.deleted_at IS NULL AND
			(sb.project_id=p.id OR EXISTS (SELECT 1 FROM project_episodes e WHERE e.id=sb.episode_id AND e.project_id=p.id AND e.deleted_at IS NULL))`, projectID, userID).
		Order("output.created_at DESC").Find(&items).Error
	return items, err
}

// RemoveStoryboardImage 清空分镜主图引用，保留生成结果以便在历史记录中恢复。
func (s *Creative) RemoveStoryboardImage(userID, projectID, storyboardID uuid.UUID) error {
	ownedStoryboard := `(project_id=? AND EXISTS (SELECT 1 FROM creative_projects p WHERE p.id=? AND p.user_id=? AND p.deleted_at IS NULL)) OR
		(episode_id IN (SELECT e.id FROM project_episodes e JOIN creative_projects p ON p.id=e.project_id
			WHERE e.project_id=? AND p.user_id=? AND e.deleted_at IS NULL AND p.deleted_at IS NULL))`
	result := s.DB.Model(&model.EpisodeStoryboard{}).
		Where("id=? AND thumbnail_asset_id IS NOT NULL AND deleted_at IS NULL AND ("+ownedStoryboard+")", storyboardID, projectID, projectID, userID, projectID, userID).
		Updates(map[string]any{"thumbnail_asset_id": nil, "status": "idle", "user_edited": true})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		var exists int64
		if err := s.DB.Model(&model.EpisodeStoryboard{}).
			Where("id=? AND deleted_at IS NULL AND ("+ownedStoryboard+")", storyboardID, projectID, projectID, userID, projectID, userID).
			Count(&exists).Error; err != nil {
			return err
		}
		if exists == 0 {
			return gorm.ErrRecordNotFound
		}
	}
	return nil
}

func (s *Creative) AddStoryboardCandidate(userID, projectID, storyboardID, outputID uuid.UUID) error {
	return s.DB.Transaction(func(tx *gorm.DB) error {
		var row struct{ Candidate bool }
		if err := tx.Table("generation_outputs output").
			Select("sb.active_output_id=output.id OR coalesce(output.metadata->>'candidate','false')='true' AS candidate").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND gt.task_type='video_generation'", storyboardID).
			Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id AND sb.deleted_at IS NULL").
			Joins("JOIN project_episodes e ON e.id=sb.episode_id AND e.deleted_at IS NULL").
			Joins("JOIN creative_projects p ON p.id=e.project_id AND p.id=? AND p.user_id=? AND p.project_type='premium_drama' AND p.deleted_at IS NULL", projectID, userID).
			Where("output.id=?", outputID).Take(&row).Error; err != nil {
			return err
		}
		if row.Candidate {
			return nil
		}
		var count int64
		if err := tx.Table("generation_outputs output").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND gt.task_type='video_generation'", storyboardID).
			Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id").
			Where("sb.active_output_id=output.id OR coalesce(output.metadata->>'candidate','false')='true'").Count(&count).Error; err != nil {
			return err
		}
		if count >= 3 {
			return errors.New("当前分镜最多保留 3 个备选视频，请先删除一个备选视频")
		}
		var activeTasks int64
		if err := tx.Model(&model.GenerationTask{}).
			Where("storyboard_id=? AND task_type='video_generation' AND status IN ?", storyboardID, activeTaskStatuses).
			Count(&activeTasks).Error; err != nil {
			return err
		}
		if count+activeTasks >= 3 {
			return errors.New("生成中的视频已占用备选位置，请等待生成完成或先删除一个备选视频")
		}
		return tx.Model(&model.GenerationOutput{}).Where("id=?", outputID).
			Update("metadata", gorm.Expr("jsonb_set(coalesce(metadata,'{}'::jsonb),'{candidate}','true'::jsonb,true)")).Error
	})
}

func (s *Creative) RemoveStoryboardCandidate(userID, projectID, storyboardID, outputID uuid.UUID) error {
	return s.DB.Transaction(func(tx *gorm.DB) error {
		var row struct {
			Active    bool
			Candidate bool
		}
		if err := tx.Table("generation_outputs output").
			Select("sb.active_output_id=output.id AS active,sb.active_output_id=output.id OR coalesce(output.metadata->>'candidate','false')='true' AS candidate").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND gt.task_type='video_generation'", storyboardID).
			Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id AND sb.deleted_at IS NULL").
			Joins("JOIN project_episodes e ON e.id=sb.episode_id AND e.deleted_at IS NULL").
			Joins("JOIN creative_projects p ON p.id=e.project_id AND p.id=? AND p.user_id=? AND p.project_type='premium_drama' AND p.deleted_at IS NULL", projectID, userID).
			Where("output.id=?", outputID).Take(&row).Error; err != nil {
			return err
		}
		if !row.Candidate {
			return errors.New("该视频不在备选列表中")
		}
		if row.Active {
			var replacement struct{ ID uuid.UUID }
			if err := tx.Table("generation_outputs output").Select("output.id").
				Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND gt.task_type='video_generation'", storyboardID).
				Where("output.id<>? AND coalesce(output.metadata->>'candidate','false')='true'", outputID).
				Order("output.created_at DESC").Limit(1).Scan(&replacement).Error; err != nil {
				return err
			}
			var replacementID *uuid.UUID
			if replacement.ID != uuid.Nil {
				replacementID = &replacement.ID
			}
			status := "idle"
			if replacementID != nil {
				status = "completed"
			}
			if err := tx.Model(&model.EpisodeStoryboard{}).Where("id=?", storyboardID).
				Updates(map[string]any{"active_output_id": replacementID, "status": status}).Error; err != nil {
				return err
			}
		}
		return tx.Model(&model.GenerationOutput{}).Where("id=?", outputID).
			Update("metadata", gorm.Expr("coalesce(metadata,'{}'::jsonb)-'candidate'")).Error
	})
}

func (s *Creative) ActivateStoryboardOutput(userID, projectID, storyboardID, outputID uuid.UUID) error {
	var output struct {
		MediaAssetID uuid.UUID
		TaskType     string
	}
	if err := s.DB.Table("generation_outputs output").Select("output.media_asset_id,gt.task_type").
		Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND (gt.task_type='video_generation' OR (gt.task_type='image_generation' AND gt.input_data->>'target_type'='storyboard'))", storyboardID).
		Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id AND sb.deleted_at IS NULL").
		Joins(`JOIN creative_projects p ON p.id=? AND p.user_id=? AND p.deleted_at IS NULL AND
			(sb.project_id=p.id OR EXISTS (SELECT 1 FROM project_episodes e WHERE e.id=sb.episode_id AND e.project_id=p.id AND e.deleted_at IS NULL))`, projectID, userID).
		Where("output.id=?", outputID).Take(&output).Error; err != nil {
		return err
	}
	updates := map[string]any{"status": "completed", "updated_at": time.Now()}
	if output.TaskType == "video_generation" {
		updates["active_output_id"] = outputID
	} else {
		updates["thumbnail_asset_id"] = output.MediaAssetID
	}
	result := s.DB.Model(&model.EpisodeStoryboard{}).Where("id=? AND deleted_at IS NULL", storyboardID).Updates(updates)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *Creative) DeleteStoryboardOutput(userID, projectID, storyboardID, outputID uuid.UUID) (string, error) {
	var objectKey string
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var row struct {
			TaskID    uuid.UUID
			MediaID   uuid.UUID
			ObjectKey string
			Status    string
			Candidate bool
		}
		if err := tx.Table("generation_outputs output").Select("output.task_id,output.media_asset_id AS media_id,media.object_key,gt.status,sb.active_output_id=output.id OR coalesce(output.metadata->>'candidate','false')='true' AS candidate").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.storyboard_id=? AND (gt.task_type='video_generation' OR (gt.task_type='image_generation' AND gt.input_data->>'target_type'='storyboard'))", storyboardID).
			Joins("JOIN media_assets media ON media.id=output.media_asset_id").
			Joins("JOIN episode_storyboards sb ON sb.id=gt.storyboard_id").
			Joins(`JOIN creative_projects p ON p.id=? AND p.user_id=? AND p.deleted_at IS NULL AND
				(sb.project_id=p.id OR EXISTS (SELECT 1 FROM project_episodes e WHERE e.id=sb.episode_id AND e.project_id=p.id AND e.deleted_at IS NULL))`, projectID, userID).
			Where("output.id=?", outputID).Take(&row).Error; err != nil {
			return err
		}
		if row.Status == "pending_submission" || row.Status == "submitting" || row.Status == "submitted" || row.Status == "processing" || row.Status == "cancel_requested" {
			return errors.New("视频仍在生成中，暂时不能删除历史记录")
		}
		if row.Candidate {
			return errors.New("备选视频不能直接删除，请先移入历史记录")
		}
		if err := tx.Exec("UPDATE episode_storyboards SET active_output_id=NULL,status='idle' WHERE id=? AND active_output_id=?", storyboardID, outputID).Error; err != nil {
			return err
		}
		if err := tx.Exec("UPDATE episode_storyboards SET thumbnail_asset_id=NULL,status='idle' WHERE id=? AND thumbnail_asset_id=?", storyboardID, row.MediaID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM generation_outputs WHERE id=?", outputID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM generation_tasks WHERE id=?", row.TaskID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM channel_asset_cache WHERE media_asset_id=?", row.MediaID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM media_assets WHERE id=?", row.MediaID).Error; err != nil {
			return err
		}
		objectKey = row.ObjectKey
		return nil
	})
	return objectKey, err
}

func (s *Creative) ListAssetOutputs(userID, projectID, assetID uuid.UUID) ([]map[string]any, error) {
	items := make([]map[string]any, 0)
	err := s.DB.Table("generation_outputs output").
		Select(`output.id,output.media_asset_id,output.output_type,output.sequence_no,output.metadata,output.created_at,output.task_id,
			media.public_url,media.mime_type,media.size_bytes,gt.estimated_points::text AS estimated_points,
			gt.actual_points::text AS actual_points,(a.image_asset_id=output.media_asset_id) AS active`).
		Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.task_type='image_generation' AND gt.input_data->>'asset_id'=?", assetID.String()).
		Joins("JOIN media_assets media ON media.id=output.media_asset_id AND media.deleted_at IS NULL").
		Joins("JOIN project_assets a ON a.id=? AND a.project_id=? AND a.deleted_at IS NULL", assetID, projectID).
		Joins("JOIN creative_projects p ON p.id=a.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
		Order("output.created_at DESC").Find(&items).Error
	return items, err
}

func (s *Creative) ActivateAssetOutput(userID, projectID, assetID, outputID uuid.UUID) error {
	result := s.DB.Exec(`UPDATE project_assets a SET image_asset_id=(SELECT media_asset_id FROM generation_outputs WHERE id=?),updated_at=CURRENT_TIMESTAMP
		WHERE a.id=? AND a.project_id=? AND a.deleted_at IS NULL AND EXISTS (
			SELECT 1 FROM generation_outputs output JOIN generation_tasks gt ON gt.id=output.task_id
			JOIN creative_projects p ON p.id=a.project_id
			WHERE output.id=? AND gt.task_type='image_generation' AND gt.input_data->>'asset_id'=? AND p.user_id=? AND p.deleted_at IS NULL)`,
		outputID, assetID, projectID, outputID, assetID.String(), userID)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *Creative) DeleteAssetOutput(userID, projectID, assetID, outputID uuid.UUID) (string, error) {
	var objectKey string
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var row struct {
			TaskID    uuid.UUID
			MediaID   uuid.UUID
			ObjectKey string
			Status    string
		}
		if err := tx.Table("generation_outputs output").Select("output.task_id,output.media_asset_id AS media_id,media.object_key,gt.status").
			Joins("JOIN generation_tasks gt ON gt.id=output.task_id AND gt.task_type='image_generation' AND gt.input_data->>'asset_id'=?", assetID.String()).
			Joins("JOIN media_assets media ON media.id=output.media_asset_id").
			Joins("JOIN project_assets a ON a.id=? AND a.project_id=? AND a.deleted_at IS NULL", assetID, projectID).
			Joins("JOIN creative_projects p ON p.id=a.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
			Where("output.id=?", outputID).Take(&row).Error; err != nil {
			return err
		}
		for _, status := range activeTaskStatuses {
			if status == row.Status {
				return errors.New("鍥剧墖浠嶅湪鐢熸垚涓紝鏆傛椂涓嶈兘鍒犻櫎鍘嗗彶璁板綍")
			}
		}
		if err := tx.Exec("UPDATE project_assets SET image_asset_id=NULL WHERE id=? AND image_asset_id=?", assetID, row.MediaID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM generation_outputs WHERE id=?", outputID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM generation_tasks WHERE id=?", row.TaskID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM channel_asset_cache WHERE media_asset_id=?", row.MediaID).Error; err != nil {
			return err
		}
		if err := tx.Exec("DELETE FROM media_assets WHERE id=?", row.MediaID).Error; err != nil {
			return err
		}
		objectKey = row.ObjectKey
		return nil
	})
	return objectKey, err
}

func (s *Creative) UpdateEpisodeAnalysisSettings(userID, projectID, episodeID uuid.UUID, audioSource, sourceLanguage string) error {
	if audioSource != "video_audio" && audioSource != "subtitle_file" {
		return errors.New("音频来源无效")
	}
	sourceLanguage = strings.TrimSpace(sourceLanguage)
	if len(sourceLanguage) > 16 {
		return errors.New("源语言设置无效")
	}
	return s.DB.Transaction(func(tx *gorm.DB) error {
		var episode model.ProjectEpisode
		if err := tx.Table("project_episodes e").Select("e.*").
			Joins("JOIN creative_projects p ON p.id=e.project_id AND p.user_id=? AND p.deleted_at IS NULL", userID).
			Where("e.id=? AND e.project_id=? AND e.deleted_at IS NULL", episodeID, projectID).Take(&episode).Error; err != nil {
			return err
		}
		// Selecting subtitle mode is persisted even before the file is uploaded.
		// The workbench disables analysis until a subtitle asset is present.
		updates := map[string]any{"audio_source": audioSource, "source_language": nil}
		if sourceLanguage != "" {
			updates["source_language"] = sourceLanguage
		}
		return tx.Model(&episode).Updates(updates).Error
	})
}
