package com.yaoai.pipeline.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.ai.core.model.ChatMessage;
import com.yaoai.ai.core.model.ChatRequest;
import com.yaoai.ai.core.service.LlmService;
import com.yaoai.billing.dto.BillingChargeRequest;
import com.yaoai.billing.service.BillingService;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.domain.entity.Character;
import com.yaoai.domain.entity.Episode;
import com.yaoai.domain.entity.Scene;
import com.yaoai.domain.entity.Storyboard;
import com.yaoai.domain.mapper.CharacterMapper;
import com.yaoai.domain.mapper.EpisodeMapper;
import com.yaoai.domain.mapper.SceneMapper;
import com.yaoai.domain.mapper.StoryboardMapper;
import com.yaoai.pipeline.service.StoryboardPipelineService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class StoryboardPipelineServiceImpl implements StoryboardPipelineService {

    private final LlmService llmService;
    private final StoryboardMapper storyboardMapper;
    private final EpisodeMapper episodeMapper;
    private final CharacterMapper characterMapper;
    private final SceneMapper sceneMapper;
    private final ObjectMapper objectMapper;
    private final BillingService billingService;

    private static final String SYSTEM_PROMPT_BASE = """
            你是一位专业的影视分镜师。根据分集脚本，将其拆分为若干个连续镜头（shot）。
            每个镜头时长 4~8 秒。

            %s

            请以 JSON 数组格式返回，每个镜头格式如下：
            [
              {
                "sequence_num": 镜头序号（从1开始，整数）,
                "scene_number": "001",
                "short_description": "一句话描述镜头内容（中文，20字以内）",
                "detailed_description": "详细场景描述（中文，50字以内）",
                "characters": "出场的角色/场景，用@名称格式，逗号分隔，例如：@小明,@咖啡馆，无则填空字符串",
                "dialogues": "对白内容，格式：角色名：「台词」，无对白填空字符串",
                "camera_direction": "镜头运动方式（如：固定/推进/拉远/横移/跟拍）",
                "composition_guide": "构图说明（如：近景/中景/远景/特写）",
                "duration_seconds": 时长秒数（整数，4-8）,
                "start_frame_prompt": "首帧英文 Prompt（供文生图使用，描述画面静态构图，30词以内）",
                "motion_script": "镜头运动描述（供图生视频使用，中文，20字以内）"
              }
            ]
            只返回 JSON 数组，不要有其他说明文字。镜头数量根据脚本长度决定，通常 5~15 个。
            """;

    @Override
    public List<Storyboard> generateStoryboards(Long episodeId, Long projectId, Long tenantId) {
        Episode episode = episodeMapper.selectById(episodeId);
        if (episode == null) {
            throw new BizException(ErrorCode.NOT_FOUND, "分集不存在");
        }

        // 构建角色/场景设定上下文
        String assetContext = buildAssetContext(projectId, tenantId);
        String systemPrompt = String.format(SYSTEM_PROMPT_BASE, assetContext);

        String userMsg = String.format("集标题：%s\n\n分集脚本：\n%s",
                episode.getTitle(), episode.getScript() != null ? episode.getScript() : episode.getSummary());

        log.info("Generating storyboards: episodeId={}, projectId={}", episodeId, projectId);

        ChatRequest request = ChatRequest.builder()
                .messages(List.of(
                        ChatMessage.system(systemPrompt),
                        ChatMessage.user(userMsg)
                ))
                .build();

        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(StpUtil.getLoginIdAsLong())
                .projectId(projectId)
                .operation("storyboard_generate")
                .modality("TEXT")
                .modelProvider(llmService.getProvider())
                .modelId(llmService.getModelId())
                .modelName(llmService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(episodeId))
                .build());

        String raw = llmService.chat(request);
        log.debug("Storyboard LLM response length={}", raw.length());

        try {
            String json = extractJson(raw);
            List<Map<String, Object>> items = objectMapper.readValue(json, new TypeReference<>() {});

            List<Storyboard> result = new ArrayList<>();
            for (Map<String, Object> item : items) {
                Storyboard sb = new Storyboard();
                sb.setProjectId(projectId);
                sb.setEpisodeId(episodeId);
                sb.setTenantId(tenantId);
                sb.setSequenceNum(((Number) item.getOrDefault("sequence_num", result.size() + 1)).intValue());
                sb.setSceneNumber((String) item.getOrDefault("scene_number", String.format("%03d", result.size() + 1)));
                sb.setShortDescription((String) item.getOrDefault("short_description", ""));
                sb.setDetailedDescription((String) item.getOrDefault("detailed_description", ""));
                sb.setCharacters((String) item.getOrDefault("characters", ""));
                sb.setDialogues((String) item.getOrDefault("dialogues", ""));
                sb.setCameraDirection((String) item.getOrDefault("camera_direction", ""));
                sb.setCompositionGuide((String) item.getOrDefault("composition_guide", ""));
                sb.setDurationSeconds(((Number) item.getOrDefault("duration_seconds", 5)).intValue());
                sb.setStartFramePrompt((String) item.getOrDefault("start_frame_prompt", ""));
                sb.setMotionScript((String) item.getOrDefault("motion_script", ""));
                sb.setStatus("draft");
                storyboardMapper.insert(sb);
                result.add(sb);
            }
            billingService.charge(BillingChargeRequest.builder()
                    .tenantId(tenantId)
                    .userId(StpUtil.getLoginIdAsLong())
                    .projectId(projectId)
                    .operation("storyboard_generate")
                    .modality("TEXT")
                    .modelProvider(llmService.getProvider())
                    .modelId(llmService.getModelId())
                    .modelName(llmService.getModelName())
                    .unitCount(result.size())
                    .refId(String.valueOf(episodeId))
                    .build());
            log.info("Storyboards generated: count={}", result.size());
            return result;
        } catch (Exception e) {
            log.error("Storyboard parsing failed", e);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "分镜解析失败: " + e.getMessage());
        }
    }

    @Override
    public List<Storyboard> listByEpisode(Long episodeId, Long tenantId) {
        return storyboardMapper.findByEpisode(episodeId, tenantId);
    }

    @Override
    public List<Storyboard> listByProject(Long projectId, Long tenantId) {
        return storyboardMapper.findByProject(projectId, tenantId);
    }

    @Override
    public Storyboard create(Storyboard storyboard) {
        int maxSeq = storyboardMapper.maxSequenceByEpisode(storyboard.getEpisodeId());
        storyboard.setSequenceNum(maxSeq + 1);
        storyboard.setStatus("draft");
        storyboardMapper.insert(storyboard);
        return storyboard;
    }

    @Override
    public Storyboard update(Long id, Long tenantId, Storyboard patch) {
        Storyboard existing = storyboardMapper.selectById(id);
        if (existing == null || !tenantId.equals(existing.getTenantId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "分镜不存在");
        }
        patch.setId(id);
        patch.setTenantId(tenantId);
        storyboardMapper.updateById(patch);
        return storyboardMapper.selectById(id);
    }

    @Override
    public void delete(Long id, Long tenantId) {
        Storyboard existing = storyboardMapper.selectById(id);
        if (existing == null || !tenantId.equals(existing.getTenantId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "分镜不存在");
        }
        storyboardMapper.deleteById(id);
    }

    @Override
    public void reorder(List<Long> orderedIds, Long tenantId) {
        for (int i = 0; i < orderedIds.size(); i++) {
            storyboardMapper.updateSequence(orderedIds.get(i), i + 1, tenantId);
        }
    }

    private static final String PROMPT_GEN_SYSTEM = """
            你是一位专业的影视分镜撰写师，擅长根据剧集剧情、角色外观设定和场景设定，将分镜信息转化为精炼的视频生成提示词。
            我会提供：① 该分镜所属集数的剧情、② 项目角色和场景的视觉设定、③ 当前分镜的镜头信息。
            请严格依据以上内容，生成按时间轴分段的影视风格描述，格式如下：

            0-Xs（氛围标签，如：抓眼/转折/高潮/收尾）
            镜头类型（特写/中景/远景/近景）：场景动作描述
            角色名（情绪）："对白内容"（无对白则省略此行）
            音效/BGM描述

            要求：
            - 描述必须忠实于提供的剧情和角色/场景设定，角色外貌、服装等严格参照设定
            - 分镜中 @角色名 / @场景名 表示该分镜引用了该角色或场景的视觉设定
            - 按时间段分2-4段，总时长等于分镜时长
            - 每段包含：时间轴、画面、对白（有则写）、声音
            - 文字精炼生动，适合作为视频生成提示词
            - 只返回提示词文本，不加任何说明
            """;

    @Override
    public String generatePrompt(Long storyboardId, Long tenantId) {
        Storyboard sb = storyboardMapper.selectById(storyboardId);
        if (sb == null || !tenantId.equals(sb.getTenantId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "分镜不存在");
        }

        // ① 所属集数剧情
        StringBuilder userMsg = new StringBuilder();
        if (sb.getEpisodeId() != null) {
            Episode episode = episodeMapper.selectById(sb.getEpisodeId());
            if (episode != null) {
                String episodeContent = episode.getScript() != null && !episode.getScript().isBlank()
                        ? episode.getScript() : nullSafe(episode.getSummary());
                userMsg.append(String.format("""
                        【所属集数剧情】
                        第%d集《%s》
                        %s

                        """,
                        episode.getEpisodeNumber() != null ? episode.getEpisodeNumber() : 0,
                        nullSafe(episode.getTitle()), episodeContent));
            }
        }

        // ② 角色视觉设定（本项目全部角色）
        List<Character> characters = characterMapper.findByProject(sb.getProjectId(), tenantId);
        if (!characters.isEmpty()) {
            userMsg.append("【角色视觉设定】\n");
            for (Character c : characters) {
                userMsg.append(String.format("- @%s（%s）：%s，服装：%s，外貌特征：%s\n",
                        nullSafe(c.getName()),
                        nullSafe(c.getRoleType()),
                        nullSafe(c.getPersonality()),
                        nullSafe(c.getCostume()),
                        nullSafe(c.getVisualHint())));
            }
            userMsg.append("\n");
        }

        // ③ 场景视觉设定
        List<Scene> scenes = sceneMapper.findByProject(sb.getProjectId(), tenantId);
        if (!scenes.isEmpty()) {
            userMsg.append("【场景视觉设定】\n");
            for (Scene s : scenes) {
                userMsg.append(String.format("- @%s（%s）：%s\n",
                        nullSafe(s.getName()),
                        s.getSceneType() != null ? ("indoor".equals(s.getSceneType()) ? "室内" : "室外") : "",
                        nullSafe(s.getDescription())));
            }
            userMsg.append("\n");
        }

        // ④ 当前分镜信息
        userMsg.append(String.format("""
                【当前分镜信息】（第%d个镜头）
                - 镜头描述：%s
                - 详细场景：%s
                - 出场角色/场景：%s
                - 对白内容：%s
                - 镜头运动：%s
                - 构图方式：%s
                - 分镜时长：%d 秒
                """,
                sb.getSequenceNum() != null ? sb.getSequenceNum() : 0,
                nullSafe(sb.getShortDescription()),
                nullSafe(sb.getDetailedDescription()),
                nullSafe(sb.getCharacters()),
                nullSafe(sb.getDialogues()),
                nullSafe(sb.getCameraDirection()),
                nullSafe(sb.getCompositionGuide()),
                sb.getDurationSeconds() != null ? sb.getDurationSeconds() : 5));

        ChatRequest request = ChatRequest.builder()
                .messages(List.of(
                        ChatMessage.system(PROMPT_GEN_SYSTEM),
                        ChatMessage.user(userMsg.toString())
                ))
                .build();

        log.info("Generating storyboard prompt: storyboardId={}, episodeId={}, characters={}, scenes={}",
                storyboardId, sb.getEpisodeId(), characters.size(), scenes.size());
        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(StpUtil.getLoginIdAsLong())
                .projectId(sb.getProjectId())
                .operation("storyboard_prompt_generate")
                .modality("TEXT")
                .modelProvider(llmService.getProvider())
                .modelId(llmService.getModelId())
                .modelName(llmService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(storyboardId))
                .build());
        String prompt = llmService.chat(request);
        billingService.charge(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(StpUtil.getLoginIdAsLong())
                .projectId(sb.getProjectId())
                .operation("storyboard_prompt_generate")
                .modality("TEXT")
                .modelProvider(llmService.getProvider())
                .modelId(llmService.getModelId())
                .modelName(llmService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(storyboardId))
                .build());
        return prompt;
    }

    private static String nullSafe(String s) {
        return s != null ? s : "";
    }

    private String buildAssetContext(Long projectId, Long tenantId) {
        StringBuilder sb = new StringBuilder();
        List<Character> characters = characterMapper.findByProject(projectId, tenantId);
        List<Scene> scenes = sceneMapper.findByProject(projectId, tenantId);

        if (characters.isEmpty() && scenes.isEmpty()) {
            return "（本项目暂无角色/场景设定，请根据脚本自行设计）";
        }

        sb.append("【项目角色设定】（请在 characters 字段用 @名称 引用）\n");
        for (Character c : characters) {
            sb.append(String.format("- @%s：%s，%s，服装：%s\n",
                    nullSafe(c.getName()),
                    nullSafe(c.getPersonality()),
                    nullSafe(c.getVisualHint()),
                    nullSafe(c.getCostume())));
        }
        if (!scenes.isEmpty()) {
            sb.append("\n【项目场景设定】（请在 characters 字段用 @名称 引用场景）\n");
            for (Scene s : scenes) {
                sb.append(String.format("- @%s（%s）：%s\n",
                        nullSafe(s.getName()),
                        "indoor".equals(s.getSceneType()) ? "室内" : "室外",
                        nullSafe(s.getDescription())));
            }
        }
        sb.append("\n重要：characters 字段必须使用 @名称 格式引用以上角色/场景，例如：@小明,@咖啡馆");
        return sb.toString();
    }

    private String extractJson(String raw) {
        String s = raw.strip();
        if (s.startsWith("```")) {
            int start = s.indexOf('\n') + 1;
            int end = s.lastIndexOf("```");
            if (end > start) s = s.substring(start, end).strip();
        }
        return s;
    }
}
