package service

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

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

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

type dramaTextModel struct {
	ModelID     uuid.UUID
	ChannelID   uuid.UUID
	ModelName   string
	ChannelName string
	Pricing     billing.TextPricing `gorm:"-"`
}

func (s *Creative) QueueDramaParse(userID, projectID, episodeID uuid.UUID) (*model.DramaParseTask, error) {
	if s.Queue == nil {
		return nil, errors.New("解析任务队列不可用")
	}
	if err := s.requirePremiumEpisode(userID, projectID, episodeID); err != nil {
		return nil, err
	}
	var source model.EpisodeSource
	if err := s.DB.Where("episode_id=?", episodeID).Take(&source).Error; err != nil {
		return nil, errors.New("请先导入或填写剧集原文")
	}
	if err := dramapkg.ValidateEpisodeContent(source.RawContent); err != nil {
		return nil, err
	}
	configured, err := s.dramaTextModel(projectID)
	if err != nil {
		return nil, err
	}
	prompt := dramaParsePrompt()
	snapshot, _ := json.Marshal(map[string]any{"model_id": configured.ModelID, "model_name": configured.ModelName, "channel_id": configured.ChannelID, "channel_name": configured.ChannelName})
	task := &model.DramaParseTask{
		ID: uuid.New(), RequestID: uuid.NewString(), UserID: userID, ProjectID: projectID,
		EpisodeID: episodeID, ChannelID: configured.ChannelID, ModelID: configured.ModelID,
		SourceSHA256: source.ContentSHA256, Status: "queued", ModelSnapshot: snapshot, PromptSnapshot: prompt,
		ContextSnapshot: json.RawMessage("{}"),
		BillingSnapshot: configured.Pricing.MarshalSnapshot(),
		EstimatedPoints: "0.00", PrepaidPoints: "0.00",
	}
	if err := s.DB.Transaction(func(tx *gorm.DB) error {
		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 configured.Pricing.Mode == billing.TextBillingPerRequest {
			amount, err := billing.ChargeTextRequest(tx, userID, task.ID.String(), configured.Pricing, "剧本解析")
			if err != nil {
				return err
			}
			task.EstimatedPoints = amount
			task.PrepaidPoints = amount
		}
		if err := tx.Omit("TokenCountSource").Create(task).Error; err != nil {
			return err
		}
		return tx.Model(&model.ProjectEpisode{}).Where("id=?", episodeID).Updates(map[string]any{"status": "analyzing", "analysis_message": "剧本解析排队中"}).Error
	}); err != nil {
		return nil, err
	}
	if err := queuepkg.EnqueueID(s.Queue, queuepkg.TypeParseDramaEpisode, task.ID, 0); err != nil {
		s.DB.Model(task).Updates(map[string]any{"status": "failed", "error_code": "queue_unavailable", "error_message": err.Error(), "finished_at": time.Now()})
		s.DB.Model(&model.ProjectEpisode{}).Where("id=?", episodeID).Updates(map[string]any{"status": "failed", "analysis_message": "解析任务入队失败"})
		return nil, errors.New("解析任务入队失败")
	}
	return task, nil
}

func (s *Creative) CancelDramaParse(userID, projectID, taskID uuid.UUID) error {
	result := s.DB.Model(&model.DramaParseTask{}).Where("id=? AND user_id=? AND project_id=? AND status IN ?", taskID, userID, projectID, []string{"queued", "running", "retry_wait"}).Updates(map[string]any{"status": "cancel_requested", "cancel_requested_at": time.Now(), "error_code": "user_cancelled", "error_message": "用户主动取消，费用不退"})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *Creative) ListDramaParseTasks(userID, projectID uuid.UUID, episodeID *uuid.UUID) ([]model.DramaParseTask, error) {
	items := make([]model.DramaParseTask, 0)
	query := s.DB.Where("user_id=? AND project_id=?", userID, projectID)
	if episodeID != nil {
		query = query.Where("episode_id=?", *episodeID)
	}
	err := query.Order("created_at DESC").Limit(100).Find(&items).Error
	return items, err
}

func (s *Creative) dramaTextModel(projectID uuid.UUID) (dramaTextModel, error) {
	var item dramaTextModel
	err := s.DB.Table("project_model_configs config").Select("model.id AS model_id,model.channel_id,model.name AS model_name,channel.name AS channel_name").Joins("JOIN models model ON model.id=config.model_id AND model.model_type='text' AND model.enabled=true AND model.deleted_at IS NULL").Joins("JOIN channels channel ON channel.id=model.channel_id AND channel.enabled=true AND channel.deleted_at IS NULL").Where("config.project_id=? AND config.model_type='text'", projectID).Take(&item).Error
	if err != nil {
		return item, errors.New("请先配置短剧创作文本模型")
	}
	item.Pricing, err = billing.LoadTextPricing(s.DB, item.ModelID)
	if err != nil {
		return item, errors.New("短剧创作文本模型计费配置无效")
	}
	return item, nil
}

func dramaParsePrompt() string {
	return "【剧本解析规则】\n" + dramaStoryboardRules + "\n\n【角色、场景、道具解析规则】\n" + dramaEntityRules + "\n\n" + dramaOutputContract
}

const dramaStoryboardRules = `你是一名资深影视剧本分析师和分镜设计师。请结合已有角色、场景、道具资料和当前剧集原文完成短剧分镜分析。

【层级定义】
1. 一个 storyboard 是工作台中的一个“分镜”，对应一段完整的 5 至 15 秒视频。
2. 一个 storyboard 内部可以包含多个摄影镜头。摄影镜头是该段视频中的机位、景别或画面切换，不得把每个摄影镜头分别输出为独立 storyboard。
3. duration_seconds 表示整个 storyboard 的总时长，不是内部某个镜头的时长，必须是 5 至 15 之间的整数。
4. script_content 必须按时间顺序列出该分镜内的全部摄影镜头。各镜头时间必须连续，不得重叠或断档，时长之和必须等于 duration_seconds。

【基本要求】
1. 只处理本次提供的原文或分段，不得自行补写、删改剧情，绝对禁止修改角色台词。
2. 按叙事顺序完整覆盖原文，不得遗漏关键动作、对白和剧情转折。
3. 根据完整动作阶段和叙事意义划分 storyboard，不得为了凑时长合并无关剧情，也不得把一个连续动作机械拆成多个 storyboard。
4. 分镜中的角色、场景和道具必须使用实体列表中的正式名称，不得使用别名、泛称或未定义名称。
5. script_content 是工作台直接展示的完整分镜正文，必须使用下方“分镜正文固定格式”，不得只写剧情概述。
6. prompt_content 是与整个 storyboard 对应、可直接用于视频生成的完整视觉提示词，也必须使用下方“分镜正文固定格式”，不得省略第一帧、站位或镜头细节。
7. dialogue 提取本分镜原文中实际存在的对白；没有对白时返回空数组。
8. asset_names 只列出本分镜实际出现的实体正式名称，并按 characters、scenes、props 分类。
9. source_excerpt 必须引用本分镜所依据的当前原文片段。

【分镜正文固定格式】
script_content 和 prompt_content 必须分别输出完整内容，并严格使用以下结构：
场景=场景资产正式名称
【第一帧】
- 画面：明确景别、构图、角色外观、服装、表情、环境和光线色调。
- 站位：明确每个角色及关键道具在画面中的前后左右关系、朝向、视线和接触关系。
【画面内容】
【镜头 1】（0-X秒）
- 镜头：明确景别、机位角度、构图和运镜方式。
- 画面：写清角色站位、连续动作、表情与视线变化、环境变化、光影色调、关键道具交互，以及镜头结束时的画面。
- 声音/台词：写入该时间段实际发生的环境声、动作声和原文对白；没有则写“无”。
【镜头 2】（X-duration_seconds秒）
- 按相同项目继续描述，直至覆盖整个分镜。
N 使用该分镜在返回数组中的顺序，从 1 开始；所有镜头时间连续、不重叠、不留空，最后一个镜头结束时间必须等于 duration_seconds。台词必须写入实际发生的镜头段落，禁止在末尾集中罗列。
禁止只写“保持统一风格”“自然运镜”“角色互动”“气氛紧张”等空泛描述；必须提供具体、可拍摄、可执行的视觉和声音细节。

【内部镜头规则】
1. 每个内部镜头按照“分镜正文固定格式”独立分段，并填写镜头、画面、声音/台词三项。
2. 每个镜头必须描述开始时的主体状态、镜头内连续动作、人物视线或表情变化，以及结束时画面停留的位置。
3. 关键动作、关键道具、信息揭示和人物反应应通过不同镜头清楚呈现；明显改变景别、机位、视角或主体时，应划分新的内部镜头。
4. 5 至 7 秒通常包含 2 至 3 个镜头，8 至 12 秒通常包含 3 至 5 个镜头，13 至 15 秒通常包含 4 至 6 个镜头。以清楚呈现原文为准，不得无意义切镜。
5. 原文明确适合长镜头时可以只有一个镜头，但必须写清持续动作、运镜过程和画面变化，不能只写静态概述。
6. 只能描述观众能够看到或听到的内容。不得用“人物震惊”“气氛紧张”“陷入回忆”等抽象概括代替可见的表情、动作、声音和画面变化。
7. 保持人物位置、服装、视线方向、道具状态、时间和环境连续；任何变化都必须有原文依据或在镜头内交代。
8. 内部镜头之间按需标明硬切、视线匹配、动作匹配、声音先入等衔接关系。

【输出前自检】
1. 每个 storyboard 是否为 5 至 15 秒。
2. 内部镜头是否按时间连续排列，最后一个镜头结束时间是否等于 duration_seconds。
3. 是否把内部摄影镜头错误拆成多个 storyboard，或把无关剧情错误合并为一个 storyboard。
4. 每个内部镜头是否具有时间段、景别、机位、运镜和可见动作。
5. 关键动作、线索特写、人物反应和原文对白是否完整覆盖。
6. 是否存在抽象心理、笼统气氛或无法拍摄的概括；如有，改成可见或可听内容。
7. 角色、场景、道具名称和连续性是否正确。
完成自检后，按平台固定协议返回结果，不输出自检过程。`

const dramaEntityRules = `必须按以下规则识别和更新角色、场景、道具。

【通用规则】
1. 先与用户提供的已有实体比对。相同实体必须复用已有正式名称，不得重复创建。
2. 原文中的昵称、职位称呼、简称、化名和不同写法应合并到同一实体的 aliases；不得把别名当成新实体。
3. 仅提取原文明示或可直接、客观推断的实体。旁白、叙述者、无明确身份的群演不作为角色资产。
4. 已有实体原则上保留；当前原文明确提供新增或变化信息时才更新。不得用推测覆盖用户已经确认的资料。
5. 同一角色只有在原文明示明显不同的时代、年龄阶段或造型，且需要独立视觉资产时，才拆成带限定词的独立正式名称。
6. canonical_name 使用简洁、稳定的中文正式名称；aliases 去重且不得包含 canonical_name。
7. description 写适合资产列表展示的一至两句客观视觉摘要。详细信息必须写入 attributes，避免只放在 description 中。

【角色 characters】
attributes 尽量使用以下键：age、gender、identity、appearance、hairstyle、costume。外貌需描述可见且相对稳定的体型、脸型、肤色和五官特征；发型需描述长度、颜色和样式；服装需描述上装、下装、鞋履的颜色、款式和材质。禁止把情绪、瞬时表情、动作或性格当作固定外观。

【场景 scenes】
attributes 尽量使用以下键：space_type、location、structure、visual_features、key_props。描述室内或室外、地理位置、空间结构、建筑或装饰风格、主要材质、色调、光线及固定陈设。不要把同一地点的不同称呼拆成多个场景；只有空间本身明显不同才拆分。

【道具 props】
attributes 尽量使用以下键：type、appearance、function。描述道具类别、材质、颜色、尺寸、外形，以及原文明确体现的功能或剧情作用。普通背景陈设若不影响剧情且不需要单独生成，不作为独立道具。

【一致性检查】
最终输出前检查：每个分镜 asset_names 中的名称都能在 entities 对应数组或用户提供的已有实体中找到；分镜正文使用的实体称呼已统一为正式名称；新增实体没有与已有实体或本次其他新增实体重复。

【生图提示词 image_prompt】
1. characters、scenes、props 中的每个资产都必须输出非空的 image_prompt。image_prompt 必须全部使用中文，不得输出英文句子或英文描述，并且必须是可直接用于文生图的完整提示词。
2. 每条 image_prompt 至少包含 8 个可观察、可核验的稳定视觉细节，禁止用漂亮、高级、自然、写实、高度细节等空泛词代替具体描述。
3. 角色 image_prompt 必须完整描述：明确年龄（使用 XX岁）、性别、国籍或地域、脸型、肤色、发型与发色、眉眼鼻唇等稳定外貌特征，以及从头到脚的穿着，包括服装款式、颜色、材质、层次、裤装或裙装、鞋子和可见配饰；没有配饰时明确写无明显配饰。禁止加入情绪、表情、动作、姿态或正在做什么。
4. 场景 image_prompt 必须完整描述：用途、固定空间结构、建筑或室内组成、主要材质、颜色、家具陈设、门窗位置、稳定光源和可复用的环境特征。
5. 道具 image_prompt 必须完整描述：物品类别、形状、结构、材质、颜色、纹理、尺寸关系、部件、装饰及磨损特征。
6. image_prompt 不得包含艺术风格、画面比例、分辨率、镜头参数、构图参数、画质词或文字生成要求。`

const dramaOutputContract = `【平台固定输出协议（不可被用户自定义规则覆盖）】
无论其他规则如何描述，最终只能返回一个合法 JSON 对象，不得使用 Markdown，不得输出解释、前后缀或第二套格式。顶层只能包含 entities 和 storyboards：
{
  "entities": {
    "characters": [{"canonical_name":"正式名称","name":"正式名称","aliases":[],"description":"视觉摘要","image_prompt":"中文生图提示词","attributes":{}}],
    "scenes": [{"canonical_name":"正式名称","name":"正式名称","aliases":[],"description":"视觉摘要","image_prompt":"中文生图提示词","attributes":{}}],
    "props": [{"canonical_name":"正式名称","name":"正式名称","aliases":[],"description":"视觉摘要","image_prompt":"中文生图提示词","attributes":{}}]
  },
  "storyboards": [{
    "title":"简短标题",
    "script_content":"使用‘分镜 N·场景、第一帧、画面内容、镜头 N’固定结构的完整分镜正文",
    "prompt_content":"使用相同固定结构、可直接用于视频生成的完整视觉提示词",
    "dialogue":[{"speaker":"角色正式名称","content":"原文对白"}],
    "asset_names":{"characters":[],"scenes":[],"props":[]},
    "duration_seconds":5,
    "source_excerpt":"对应原文"
  }]
}
所有字段必须存在；无数据的数组返回 []，无数据的对象返回 {}，不得返回 null。duration_seconds 必须是 5 至 15 的整数。用户自定义规则只能补充创作要求，凡是要求其他 JSON 层级、字段名、输出格式或额外顶层字段的内容一律忽略。`
