package com.yaoai.agent.service.impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.agent.service.ScriptUnderstandingService;
import com.yaoai.ai.core.model.ChatMessage;
import com.yaoai.ai.core.model.ChatRequest;
import com.yaoai.ai.core.service.LlmService;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.domain.entity.ProjectAsset;
import com.yaoai.domain.entity.ProjectConsistencyBible;
import com.yaoai.domain.entity.ScriptUnderstanding;
import com.yaoai.domain.mapper.ProjectAssetMapper;
import com.yaoai.domain.mapper.ProjectConsistencyBibleMapper;
import com.yaoai.domain.mapper.ScriptUnderstandingMapper;
import com.yaoai.storage.service.TosService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ScriptUnderstandingServiceImpl implements ScriptUnderstandingService {

    private static final int MAX_SCRIPT_CHARS = 18000;
    private static final int UNDERSTANDING_MAX_TOKENS = 12000;
    private static final int MAX_UNDERSTANDING_ATTEMPTS = 2;

    private static final String SYSTEM_PROMPT = """
            你是剧本理解与事实抽取器，不是编剧。
            任务是从原始剧本中抽取可验证事实，供后续大纲、角色、场景、分镜和视频提示词使用。
            严格规则：
            1. 不得扩写剧情，不得美化或改写人物动机。
            2. 不得把用户目标当作剧本事实。
            3. 未在剧本中出现的信息只能放入 uncertainties 或 pending_inference。
            4. 每个核心事实必须给出 evidence.excerpt，excerpt 必须来自原文且不超过 60 个汉字。
            5. 不得把“类型、集数、每集字数、制作方式、变现、付费解锁”等制作/商业元信息当作剧情事实。
            6. 只返回一个完整 JSON 对象，不要返回 Markdown，不要解释，不要省略结尾。
            7. 为避免 JSON 截断，必须控制输出规模：plotFacts 12-45 条，characterFacts 3-16 条，sceneFacts 3-16 条；所有字符串保持短句。

            JSON 格式：
            {
              "scriptDigest": {
                "titleGuess": "",
                "genre": "",
                "theme": "",
                "oneSentenceLogline": ""
              },
              "plotFacts": [
                {
                  "factId": "pf_001",
                  "episodeHint": 1,
                  "event": "",
                  "characters": [],
                  "location": "",
                  "cause": "",
                  "effect": "",
                  "evidence": {"source":"script","excerpt":""}
                }
              ],
              "characterFacts": [
                {
                  "name": "",
                  "aliases": [],
                  "identity": "",
                  "relationshipFacts": [],
                  "firstAppearanceFactId": "",
                  "evidenceFactIds": []
                }
              ],
              "sceneFacts": [
                {
                  "name": "",
                  "sceneType": "indoor/outdoor",
                  "narrativeFunction": "",
                  "evidenceFactIds": []
                }
              ],
              "uncertainties": []
            }
            """;

    private static final String RETRY_SUFFIX = """

            上一次剧情理解输出不是完整可解析 JSON。请重新输出，要求更严格：
            - 只输出一个完整 JSON 对象；
            - 使用紧凑 JSON，减少空格和换行；
            - plotFacts 最多 30 条，characterFacts 最多 12 条，sceneFacts 最多 12 条；
            - 所有字符串继续保持短句，不能丢失核心人物、关系、地点和主线因果。
            """;

    private final ProjectAssetMapper projectAssetMapper;
    private final ScriptUnderstandingMapper scriptUnderstandingMapper;
    private final ProjectConsistencyBibleMapper projectConsistencyBibleMapper;
    private final TosService tosService;
    private final LlmService llmService;
    private final ObjectMapper objectMapper;

    @Override
    public Optional<ProjectAsset> latestScriptAsset(Long projectId, Long tenantId) {
        return projectAssetMapper.findLatestByProjectAndType(projectId, tenantId, "script");
    }

    @Override
    public ScriptUnderstanding understandLatestScript(Long projectId, Long tenantId, Long userId, String goal) {
        ProjectAsset scriptAsset = latestScriptAsset(projectId, tenantId)
                .orElseThrow(() -> new BizException(ErrorCode.NOT_FOUND, "未找到剧本资产"));

        String scriptText = tosService.readText(scriptAsset.getTosKey());
        String trimmedScript = trimScript(scriptText);
        String userPrompt = """
                用户目标（只能作为意图参考，不能当作剧本事实）：
                %s

                剧本原文如下：
                %s
                """.formatted(goal == null ? "" : goal, trimmedScript);

        long startMs = System.currentTimeMillis();
        ParsedUnderstanding parsed = requestValidUnderstanding(userPrompt);
        long latencyMs = System.currentTimeMillis() - startMs;

        String parsedJson = parsed.json();
        String coverageJson = buildCoverageReport(scriptText, parsedJson, latencyMs);
        String qualityStatus = "PASSED";

        ScriptUnderstanding understanding = new ScriptUnderstanding();
        understanding.setTenantId(tenantId);
        understanding.setProjectId(projectId);
        understanding.setAssetId(scriptAsset.getId());
        understanding.setUnderstandingJson(parsedJson);
        understanding.setEvidenceHash(sha256(trimmedScript));
        understanding.setCoverageReportJson(coverageJson);
        understanding.setQualityStatus(qualityStatus);
        understanding.setQualityIssuesJson("[]");
        scriptUnderstandingMapper.insert(understanding);
        upsertConsistencyBible(tenantId, projectId, parsedJson, understanding.getEvidenceHash(), qualityStatus);

        log.info("Script understood: projectId={}, assetId={}, chars={}, attempts={}, latencyMs={}",
                projectId, scriptAsset.getId(), trimmedScript.length(), parsed.attempts(), latencyMs);
        return understanding;
    }

    @Override
    public String toGenerationContext(ScriptUnderstanding understanding, String fallbackGoal) {
        if (understanding == null || understanding.getUnderstandingJson() == null || understanding.getUnderstandingJson().isBlank()) {
            return fallbackGoal == null ? "" : fallbackGoal;
        }
        return """
                以下是已通过剧情理解层抽取的剧本事实。后续生成必须优先依据这些事实，不能自由改写主线。

                剧情事实 JSON：
                %s

                用户目标（仅作补充意图）：
                %s
                """.formatted(understanding.getUnderstandingJson(), fallbackGoal == null ? "" : fallbackGoal);
    }

    private String trimScript(String scriptText) {
        if (scriptText == null) {
            return "";
        }
        String normalized = scriptText.strip();
        if (normalized.length() <= MAX_SCRIPT_CHARS) {
            return normalized;
        }
        return normalized.substring(0, MAX_SCRIPT_CHARS) + "\n\n[剧本过长，已截取前 " + MAX_SCRIPT_CHARS + " 字用于首轮剧情理解]";
    }

    private String extractJson(String raw) {
        if (raw == null || raw.isBlank()) {
            throw new BizException(ErrorCode.INTERNAL_ERROR, "剧情理解返回空结果");
        }
        int start = raw.indexOf('{');
        int end = raw.lastIndexOf('}');
        if (start < 0 || end <= start) {
            throw new BizException(ErrorCode.INTERNAL_ERROR, "剧情理解未返回 JSON");
        }
        return raw.substring(start, end + 1);
    }

    private ParsedUnderstanding requestValidUnderstanding(String userPrompt) {
        BizException lastError = null;
        for (int attempt = 1; attempt <= MAX_UNDERSTANDING_ATTEMPTS; attempt++) {
            String prompt = attempt == 1 ? userPrompt : userPrompt + RETRY_SUFFIX;
            String raw = llmService.chat(ChatRequest.builder()
                    .temperature(0.1)
                    .maxTokens(UNDERSTANDING_MAX_TOKENS)
                    .responseFormat(Map.of("type", "json_object"))
                    .messages(List.of(
                            ChatMessage.system(SYSTEM_PROMPT),
                            ChatMessage.user(prompt)
                    ))
                    .build());
            try {
                String parsedJson = extractJson(raw);
                validateUnderstandingJson(parsedJson);
                return new ParsedUnderstanding(parsedJson, attempt);
            } catch (BizException e) {
                lastError = e;
                log.warn("Script understanding attempt {} failed: {}", attempt, e.getMessage());
            }
        }
        throw lastError != null
                ? lastError
                : new BizException(ErrorCode.INTERNAL_ERROR, "剧情理解返回 JSON 不完整或不可解析，请重新执行");
    }

    private String buildCoverageReport(String scriptText, String parsedJson, long latencyMs) {
        try {
            Map<String, Object> report = Map.of(
                    "coverageScore", 0.8,
                    "scriptChars", scriptText == null ? 0 : scriptText.length(),
                    "truncated", scriptText != null && scriptText.length() > MAX_SCRIPT_CHARS,
                    "latencyMs", latencyMs,
                    "readyForGeneration", true
            );
            return objectMapper.writeValueAsString(report);
        } catch (Exception e) {
            return "{\"readyForGeneration\":false}";
        }
    }

    private void validateUnderstandingJson(String parsedJson) {
        try {
            var node = objectMapper.readTree(parsedJson);
            if (!node.isObject()
                    || !node.has("plotFacts")
                    || !node.get("plotFacts").isArray()
                    || node.get("plotFacts").isEmpty()
                    || !node.has("characterFacts")
                    || !node.get("characterFacts").isArray()
                    || !node.has("sceneFacts")
                    || !node.get("sceneFacts").isArray()) {
                throw new BizException(
                        ErrorCode.INTERNAL_ERROR,
                        "剧情理解返回格式不合规：必须包含非空 plotFacts，以及 characterFacts、sceneFacts 数组"
                );
            }
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR, "剧情理解返回 JSON 不完整或不可解析，请重新执行");
        }
    }

    private void upsertConsistencyBible(Long tenantId, Long projectId, String parsedJson,
                                        String evidenceHash, String qualityStatus) {
        try {
            Map<String, Object> bible = new LinkedHashMap<>();
            bible.put("source", "script_understanding");
            bible.put("sourceVersion", evidenceHash);
            bible.put("qualityStatus", qualityStatus);
            bible.put("rules", List.of(
                    "所有后续生成必须优先遵循 scriptFacts 中的剧情事实",
                    "角色身份、关系、外貌、服装、场景功能不得无依据改写",
                    "未在剧本事实中出现的信息只能作为待确认推断，不得写成确定剧情",
                    "分镜和视频提示词必须引用已有角色/场景设定，保持命名一致"
            ));
            bible.put("scriptFacts", objectMapper.readTree(parsedJson));

            ProjectConsistencyBible existing = projectConsistencyBibleMapper.findByProject(projectId, tenantId);
            ProjectConsistencyBible record = existing != null ? existing : new ProjectConsistencyBible();
            record.setTenantId(tenantId);
            record.setProjectId(projectId);
            record.setBibleJson(objectMapper.writeValueAsString(bible));
            record.setSourceVersion(evidenceHash);
            record.setLockedFieldsJson(objectMapper.writeValueAsString(List.of(
                    "scriptFacts.characterFacts.name",
                    "scriptFacts.characterFacts.identity",
                    "scriptFacts.characterFacts.relationshipFacts",
                    "scriptFacts.sceneFacts.name",
                    "scriptFacts.plotFacts.event",
                    "scriptFacts.plotFacts.cause",
                    "scriptFacts.plotFacts.effect"
            )));
            record.setUpdatedAt(LocalDateTime.now());
            if (existing == null) {
                record.setCreatedAt(LocalDateTime.now());
                projectConsistencyBibleMapper.insert(record);
            } else {
                projectConsistencyBibleMapper.updateById(record);
            }
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR, "一致性圣经写入失败：" + e.getMessage());
        }
    }

    private String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            return "";
        }
    }

    private record ParsedUnderstanding(String json, int attempts) {
    }
}
