package com.yaoai.api.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yaoai.agent.service.ScriptUnderstandingService;
import com.yaoai.api.dto.script.ParsedScript;
import com.yaoai.api.dto.script.ScriptImportResultDTO;
import com.yaoai.api.service.ScriptImportService;
import com.yaoai.api.util.DocScriptParser;
import com.yaoai.common.context.UserContext;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.domain.entity.*;
import com.yaoai.domain.mapper.*;
import com.yaoai.storage.service.TosService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ScriptImportServiceImpl implements ScriptImportService {

    private static final List<String> PRODUCTION_METADATA_LABELS = List.of(
            "类型", "题材", "集数", "每集", "字数", "制作方式", "变现", "商业模式", "付费解锁", "AI 3D漫剧", "ai 3d漫剧"
    );

    private final ProjectAssetMapper assetMapper;
    private final TosService tosService;
    private final OutlineMapper outlineMapper;
    private final EpisodeMapper episodeMapper;
    private final StoryboardMapper storyboardMapper;
    private final CharacterMapper characterMapper;
    private final SceneMapper sceneMapper;
    private final ScriptUnderstandingService scriptUnderstandingService;

    // ── 公开方法 ──────────────────────────────────────────────────────────────

    @Override
    public ScriptImportResultDTO parseScript(Long projectId, Long tenantId) {
        ProjectAsset asset = findScriptAsset(projectId, tenantId);
        ParsedScript parsed = parseAsset(asset);
        return toSummaryDTO(parsed);
    }

    @Override
    @Transactional
    public ScriptImportResultDTO importScript(Long projectId, Long tenantId) {
        ProjectAsset asset = findScriptAsset(projectId, tenantId);
        ParsedScript parsed = parseAsset(asset);

        // 1. 清空该项目的旧数据
        deleteExisting(projectId, tenantId);

        // 2. 创建大纲
        Outline outline = new Outline();
        outline.setProjectId(projectId);
        outline.setTenantId(tenantId);
        outline.setTitle(parsed.getTitle());
        outline.setGenre(parsed.getGenre());
        outline.setSynopsis(truncate(parsed.getSynopsis(), 1000));
        outline.setEpisodeCount(parsed.getEpisodeCount());
        outline.setStatus("ready");
        outlineMapper.insert(outline);

        // 3. 创建角色。分镜引用由后续 Agent 生成时按角色库名称和素材 key 绑定。
        for (ParsedScript.Character pc : parsed.getCharacters()) {
            com.yaoai.domain.entity.Character ch = new com.yaoai.domain.entity.Character();
            ch.setProjectId(projectId);
            ch.setTenantId(tenantId);
            ch.setName(pc.getName());
            ch.setPersonality(pc.getDescription());
            ch.setRoleType(pc.getRoleType());
            ch.setGender(pc.getGender());
            ch.setStatus("draft");
            characterMapper.insert(ch);
        }

        // 4. 创建场景
        for (ParsedScript.Scene ps : parsed.getScenes()) {
            Scene scene = new Scene();
            scene.setProjectId(projectId);
            scene.setTenantId(tenantId);
            scene.setName(ps.getName());
            scene.setSceneType(ps.getSceneType());
            scene.setDescription(ps.getDescription());
            scene.setStatus("draft");
            sceneMapper.insert(scene);
        }

        // 5. 建立集数→大纲/脚本索引
        Map<Integer, ParsedScript.EpisodeOutline> outlineByEp = parsed.getEpisodeOutlines().stream()
                .collect(Collectors.toMap(ParsedScript.EpisodeOutline::getEpisodeNumber, o -> o, (a, b) -> a));
        Map<Integer, ParsedScript.EpisodeScript> scriptByEp = parsed.getEpisodeScripts().stream()
                .collect(Collectors.toMap(ParsedScript.EpisodeScript::getEpisodeNumber, s -> s, (a, b) -> a));

        int storyboardCount = 0;
        int epCount = parsed.getEpisodeCount();

        // 6. 逐集创建 Episode。分镜不在导入阶段由正文行直接落库，
        //    由用户手工创建或通过 Agent 基于分集、角色、场景和一致性上下文生成。
        for (int epNum = 1; epNum <= epCount; epNum++) {
            ParsedScript.EpisodeOutline epOutline = outlineByEp.get(epNum);
            ParsedScript.EpisodeScript epScript = scriptByEp.get(epNum);

            Episode episode = new Episode();
            episode.setProjectId(projectId);
            episode.setTenantId(tenantId);
            episode.setOutlineId(outline.getId());
            episode.setEpisodeNumber(epNum);
            episode.setTitle("第" + epNum + "集");
            episode.setSummary(epOutline != null ? truncate(epOutline.getContent(), 500) : "");
            episode.setScript(buildScriptText(epScript));
            episode.setStatus("ready");
            episodeMapper.insert(episode);
        }

        ScriptImportResultDTO dto = toSummaryDTO(parsed);
        dto.setStoryboardCount(storyboardCount);
        refreshScriptUnderstanding(projectId, tenantId);
        return dto;
    }

    // ── 私有辅助 ──────────────────────────────────────────────────────────────

    private ProjectAsset findScriptAsset(Long projectId, Long tenantId) {
        return assetMapper.findByProject(projectId).stream()
                .filter(a -> tenantId.equals(a.getTenantId()) && "script".equals(a.getAssetType()))
                .findFirst()
                .orElseThrow(() -> new BizException(ErrorCode.NOT_FOUND, "未找到上传的剧本文件"));
    }

    private ParsedScript parseAsset(ProjectAsset asset) {
        String name = Optional.ofNullable(asset.getOriginalName()).orElse(asset.getName());
        String lowerName = Optional.ofNullable(name).orElse("").toLowerCase(Locale.ROOT);
        String mime = Optional.ofNullable(asset.getMimeType()).orElse("").toLowerCase(Locale.ROOT);

        if (lowerName.endsWith(".docx") || mime.contains("wordprocessingml.document")) {
            return parseDocxFromTos(asset.getTosKey());
        }

        try {
            return parsePlainText(tosService.readText(asset.getTosKey()), name);
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            log.error("Failed to parse script text: key={}", asset.getTosKey(), e);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "解析文本剧本失败: " + e.getMessage());
        }
    }

    private ParsedScript parseDocxFromTos(String tosKey) {
        try (InputStream in = tosService.getInputStream(tosKey)) {
            return DocScriptParser.parse(in);
        } catch (IOException e) {
            log.error("Failed to parse script docx: key={}", tosKey, e);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "解析剧本文档失败: " + e.getMessage());
        }
    }

    private ParsedScript parsePlainText(String text, String originalName) {
        String normalized = Optional.ofNullable(text).orElse("").replace("\r\n", "\n").replace('\r', '\n');
        List<String> lines = Arrays.stream(normalized.split("\n"))
                .map(this::normalizeNarrativeLine)
                .filter(s -> !s.isEmpty())
                .filter(s -> !s.matches("^[-*_`#\\s]+$"))
                .filter(s -> !isProductionMetadataLine(s))
                .toList();

        ParsedScript parsed = new ParsedScript();
        parsed.setCharacters(new ArrayList<>());
        parsed.setScenes(new ArrayList<>());
        parsed.setEpisodeOutlines(new ArrayList<>());
        parsed.setEpisodeScripts(new ArrayList<>());
        parsed.setTitle(inferTitle(lines, originalName));
        parsed.setGenre(inferGenre(normalized));
        parsed.setSynopsis(inferSynopsis(lines));

        List<EpisodeBlock> blocks = splitEpisodeBlocks(lines);
        if (blocks.isEmpty()) {
            blocks = List.of(new EpisodeBlock(1, lines));
        }

        for (EpisodeBlock block : blocks) {
            ParsedScript.EpisodeOutline outline = new ParsedScript.EpisodeOutline();
            outline.setEpisodeNumber(block.number());
            outline.setContent(truncate(String.join("\n", block.lines()), 500));
            parsed.getEpisodeOutlines().add(outline);

            ParsedScript.EpisodeScript episodeScript = new ParsedScript.EpisodeScript();
            episodeScript.setEpisodeNumber(block.number());
            episodeScript.setSceneSections(new ArrayList<>());
            ParsedScript.SceneSection section = new ParsedScript.SceneSection();
            section.setSectionId(block.number() + "-1");
            section.setShots(new ArrayList<>());

            List<String> shotLines = block.lines().stream()
                    .filter(s -> !isEpisodeHeader(s))
                    .filter(s -> !s.startsWith("#"))
                    .limit(12)
                    .toList();
            int seq = 1;
            for (String line : shotLines) {
                ParsedScript.Shot shot = new ParsedScript.Shot();
                shot.setShotNumber(seq++);
                shot.setDescription(truncate(line, 300));
                section.getShots().add(shot);
            }
            if (!section.getShots().isEmpty()) {
                episodeScript.getSceneSections().add(section);
            }
            parsed.getEpisodeScripts().add(episodeScript);
        }

        parsed.setEpisodeCount(blocks.stream().mapToInt(EpisodeBlock::number).max().orElse(1));
        return parsed;
    }

    private String inferTitle(List<String> lines, String originalName) {
        return lines.stream()
                .map(line -> line.replaceFirst("^#+\\s*", "").trim())
                .filter(line -> !line.startsWith(">"))
                .filter(line -> line.length() >= 2)
                .findFirst()
                .orElseGet(() -> Optional.ofNullable(originalName)
                        .map(name -> name.replaceFirst("\\.[^.]+$", ""))
                        .orElse("未命名短剧"));
    }

    private String inferGenre(String text) {
        if (text.contains("仙侠") || text.contains("玄幻")) return "仙侠玄幻";
        if (text.contains("悬疑") || text.contains("探案")) return "悬疑";
        if (text.contains("古风") || text.contains("古装")) return "古装";
        if (text.contains("都市") || text.contains("职场") || text.contains("豪门")) return "现代都市";
        return "短剧";
    }

    private String inferSynopsis(List<String> lines) {
        return lines.stream()
                .filter(line -> !isEpisodeHeader(line))
                .filter(line -> !line.startsWith("|"))
                .limit(8)
                .collect(Collectors.joining("\n"));
    }

    private List<EpisodeBlock> splitEpisodeBlocks(List<String> lines) {
        List<EpisodeBlock> blocks = new ArrayList<>();
        EpisodeBlock current = null;
        for (String line : lines) {
            Matcher matcher = Pattern.compile("第\\s*(\\d{1,3})\\s*集").matcher(line);
            if (matcher.find()) {
                if (current != null && !current.lines().isEmpty()) {
                    blocks.add(current);
                }
                current = new EpisodeBlock(Integer.parseInt(matcher.group(1)), new ArrayList<>());
            }
            if (current != null) {
                current.lines().add(line);
            }
        }
        if (current != null && !current.lines().isEmpty()) {
            blocks.add(current);
        }
        return blocks;
    }

    private boolean isEpisodeHeader(String line) {
        return Pattern.compile("第\\s*\\d{1,3}\\s*集").matcher(line).find();
    }

    private record EpisodeBlock(int number, List<String> lines) {}

    private void deleteExisting(Long projectId, Long tenantId) {
        storyboardMapper.delete(Wrappers.<Storyboard>lambdaQuery()
                .eq(Storyboard::getProjectId, projectId).eq(Storyboard::getTenantId, tenantId));
        episodeMapper.delete(Wrappers.<Episode>lambdaQuery()
                .eq(Episode::getProjectId, projectId).eq(Episode::getTenantId, tenantId));
        outlineMapper.delete(Wrappers.<Outline>lambdaQuery()
                .eq(Outline::getProjectId, projectId).eq(Outline::getTenantId, tenantId));
        characterMapper.delete(Wrappers.<com.yaoai.domain.entity.Character>lambdaQuery()
                .eq(com.yaoai.domain.entity.Character::getProjectId, projectId).eq(com.yaoai.domain.entity.Character::getTenantId, tenantId));
        sceneMapper.delete(Wrappers.<Scene>lambdaQuery()
                .eq(Scene::getProjectId, projectId).eq(Scene::getTenantId, tenantId));
    }

    private ScriptImportResultDTO toSummaryDTO(ParsedScript parsed) {
        ScriptImportResultDTO dto = new ScriptImportResultDTO();
        dto.setTitle(parsed.getTitle());
        dto.setGenre(parsed.getGenre());
        dto.setSynopsis(truncate(parsed.getSynopsis(), 200));
        dto.setEpisodeCount(parsed.getEpisodeCount());
        dto.setCharacterCount(parsed.getCharacters().size());
        dto.setSceneCount(parsed.getScenes().size());
        dto.setStoryboardCount(0);
        return dto;
    }

    private String normalizeNarrativeLine(String raw) {
        String line = Optional.ofNullable(raw).orElse("").trim();
        while (line.startsWith(">")) {
            line = line.substring(1).trim();
        }
        return line;
    }

    private boolean isProductionMetadataLine(String line) {
        String normalized = Optional.ofNullable(line).orElse("").replace("：", ":").strip();
        if (normalized.isBlank()) {
            return false;
        }
        if (normalized.contains("|")) {
            long metadataFragments = Arrays.stream(normalized.split("\\|"))
                    .map(String::strip)
                    .filter(this::isProductionMetadataFragment)
                    .count();
            if (metadataFragments > 0) {
                return true;
            }
        }
        return isProductionMetadataFragment(normalized);
    }

    private boolean isProductionMetadataFragment(String value) {
        if (value == null || value.isBlank()) {
            return false;
        }
        String normalized = value.replace("：", ":").strip();
        String lower = normalized.toLowerCase(Locale.ROOT);
        for (String label : PRODUCTION_METADATA_LABELS) {
            String labelLower = label.toLowerCase(Locale.ROOT);
            if (lower.startsWith(labelLower + ":")
                    || lower.equals(labelLower)
                    || lower.contains(labelLower + ":")) {
                return true;
            }
        }
        return lower.matches("^每集.{0,8}\\d+.*(字|分钟|秒).*")
                || lower.matches("^\\d+\\s*集.*")
                || lower.contains("付费解锁");
    }

    private void refreshScriptUnderstanding(Long projectId, Long tenantId) {
        Long userId = UserContext.get();
        scriptUnderstandingService.understandLatestScript(
                projectId,
                tenantId,
                userId != null ? userId : 0L,
                "一键导入后建立剧情理解与一致性上下文"
        );
    }

    /** 将 EpisodeScript 序列化为可读脚本文本，存入 episode.script 字段 */
    private String buildScriptText(ParsedScript.EpisodeScript epScript) {
        if (epScript == null) return "";
        StringBuilder sb = new StringBuilder();
        if (epScript.getLocation() != null) {
            sb.append("场地：").append(epScript.getLocation()).append("\n");
        }
        for (ParsedScript.SceneSection section : epScript.getSceneSections()) {
            sb.append("\n[").append(section.getSectionId()).append("]");
            if (section.getCharacterNames() != null && !section.getCharacterNames().isEmpty()) {
                sb.append("（").append(section.getCharacterNames()).append("）");
            }
            sb.append("\n");
            for (ParsedScript.Shot shot : section.getShots()) {
                sb.append("镜头").append(shot.getShotNumber()).append("：")
                  .append(shot.getDescription()).append("\n");
            }
        }
        return sb.toString().trim();
    }

    private static String truncate(String s, int maxLen) {
        if (s == null) return "";
        return s.length() > maxLen ? s.substring(0, maxLen) : s;
    }
}
