#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AI 漫剧 · 画布 <-> manju 分镜桥接
====================================
把 aicomic-canvas.html 的场景卡/角色卡转换为 manju/build.py 需要的
script/shots.json，并复用画布上已经「🎨 生图」得到的图片 URL，
让两个项目合并成一条龙：画布规划 -> 一键导出 -> manju pipeline 出片。

用法（一般由 aicomic-server.py 调用）:
    import aicomic_bridge
    shots = aicomic_bridge.export_shots(cards)   # 写 manju/script/shots.json
"""
import json
import os
import re

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MANJU_DIR = os.path.join(BASE_DIR, "manju")
SHOTS_PATH = os.path.join(MANJU_DIR, "script", "shots.json")

# 运镜：画布中文 -> manju build.py 的 MOTION_EXPR key
MOTION_MAP = [
    (("推镜", "推进", "推"), "zoom_in"),
    (("拉镜", "拉远", "拉"), "zoom_out"),
    (("上摇", "摇上"), "pan_up"),
    (("下摇", "摇下"), "pan_down"),
    (("左摇", "摇左"), "pan_left"),
    (("右摇", "摇右"), "pan_right"),
    (("摇镜", "摇", "pan"), "pan_up"),               # 泛摇镜 -> 上摇
    (("跟镜", "跟拍", "跟踪", "track"), "pan_right"), # 跟镜 -> 右移
    (("静止", "固定", "static", "不动"), "static"),
]
DEFAULT_MOTION = "zoom_in"


def map_motion(move):
    """中文运镜 -> manju motion key"""
    m = (move or "").strip().lower()
    if not m:
        return DEFAULT_MOTION
    for keys, val in MOTION_MAP:
        for k in keys:
            if k.lower() in m:
                return val
    return DEFAULT_MOTION


def _stable_seed(card_id, existing):
    """同一卡片每次导出 seed 稳定；无 seed 时由卡片 id 派生"""
    if existing:
        return int(existing)
    s = sum(ord(ch) for ch in str(card_id))
    return 100 + (s % 900000)


def parse_lines(text, char_names):
    """把台词文本解析成 [{speaker, text}]。

    支持的写法:
        旁白：xxxxx
        赵铁：「新人？」
        叶青：什么修武世界？
    speaker 优先取「名字：」前缀；其次按行内出现的角色名；含「系统」取 system；否则旁白。
    """
    out = []
    if not text:
        return out
    names = [n for n in char_names if n]
    for raw in text.splitlines():
        line = raw.strip()
        if not line:
            continue
        speaker = None
        body = line
        m = re.match(r'^([^：:]{1,16})[：:]\s*(.*)$', line, re.S)
        if m:
            prefix = m.group(1).strip()
            body = m.group(2).strip()
            if prefix in ("旁白", "画外音", "旁白音", "独白", "字幕"):
                speaker = "narrator"
            elif prefix in names:
                speaker = prefix
        if speaker is None:
            for n in names:
                if n and n in line:
                    speaker = n
                    break
        if speaker is None:
            speaker = "system" if ("系统" in line) else "narrator"
        if not body:
            continue
        # 去掉可能残留的「」引号（仅当整句被引号包裹）
        body = re.sub(r'^[「“『](.*)[」”』]$', r'\1', body.strip())
        if body:
            out.append({"speaker": speaker, "text": body})
    return out


def cards_to_shots(cards):
    """画布 cards -> manju shots 列表"""
    scenes = [c for c in cards if c.get("type") == "scene"]
    chars = [c for c in cards if c.get("type") == "char"]
    char_by_id = {c["id"]: (c.get("data") or {}) for c in chars}
    char_names = [d.get("name", "") for d in char_by_id.values() if d.get("name")]

    shots = []
    for i, c in enumerate(scenes, start=1):
        d = c.get("data") or {}
        # 角色锁定描述：外貌/服装/特征，保证跨镜头一致性
        role_parts = []
        for cid in (d.get("charIds") or []):
            cd = char_by_id.get(cid)
            if not cd:
                continue
            parts = [str(cd.get("name") or "")]
            for k in ("appearance", "outfit", "feature"):
                if cd.get(k):
                    parts.append(str(cd[k]))
            role_parts.append("，".join(p for p in parts if p))
        prompt_parts = []
        if d.get("scene"):
            prompt_parts.append("场景：" + str(d["scene"]))
        if d.get("prompt"):
            prompt_parts.append("细节：" + str(d["prompt"]))
        if role_parts:
            prompt_parts.append("角色：" + "；".join(role_parts))
        prompt = "，".join(prompt_parts) or "empty scene"

        lines = parse_lines(d.get("lines", ""), char_names)
        if not lines:
            lines = [{"speaker": "narrator", "text": (d.get("lines") or "（无台词）")}]

        img = d.get("img") or ""
        img_url = img if img.startswith("http") else ""
        shots.append({
            "id": i,
            "location": (d.get("ep") or d.get("scene") or "镜头%d" % i)[:60],
            "prompt": prompt,
            "lines": lines,
            "motion": map_motion(d.get("move")),
            "sfx": [],
            "seed": _stable_seed(c["id"], d.get("seed")),
            "effects": [],
            "img_url": img_url,
        })
    return shots


import glob
import hashlib


def _shot_key(s):
    """分镜内容指纹：任一字段变化即认为需要重渲染"""
    return json.dumps([
        s.get("id"), s.get("prompt"), s.get("motion"), s.get("seed"),
        s.get("location"), s.get("img_url"),
        s.get("lines"),
    ], ensure_ascii=False)


def _clear_shot_assets(shot_ids):
    """删除某几个镜头的旧缓存资产（图/音/镜头/字幕/音轨），
    防止画布内容变化后误用 manju 旧剧本遗留的文件。"""
    removed = 0
    for sid in shot_ids:
        stem = "shot_%02d" % sid
        singles = [
            os.path.join(MANJU_DIR, "assets", "images", stem + ".jpg"),
            os.path.join(MANJU_DIR, "assets", "clips", stem + ".mp4"),
            os.path.join(MANJU_DIR, "assets", "tmp", "base_%02d.mp4" % sid),
            os.path.join(MANJU_DIR, "assets", "tmp", "shot_%02d_track.wav" % sid),
        ]
        for f in singles:
            if os.path.exists(f):
                os.remove(f)
                removed += 1
        for pat in (
                os.path.join(MANJU_DIR, "assets", "audio", stem + "_*.mp3"),
                os.path.join(MANJU_DIR, "assets", "subs", stem + "_*.png")):
            for f in glob.glob(pat):
                os.remove(f)
                removed += 1
    return removed

def export_shots(cards):
    """转换并写入 manju/script/shots.json；内容变化的镜头自动清旧缓存"""
    shots = cards_to_shots(cards)
    os.makedirs(os.path.join(MANJU_DIR, "script"), exist_ok=True)
    old_shots = []
    if os.path.exists(SHOTS_PATH):
        try:
            with open(SHOTS_PATH, encoding="utf-8") as f:
                old_shots = json.load(f)
        except Exception:
            old_shots = []
    old_map = {s.get("id"): s for s in old_shots}
    changed_ids = []
    for s in shots:
        if _shot_key(s) != _shot_key(old_map.get(s.get("id"))):
            changed_ids.append(s["id"])
    removed = _clear_shot_assets(changed_ids)
    with open(SHOTS_PATH, "w", encoding="utf-8") as f:
        json.dump(shots, f, ensure_ascii=False, indent=2)
    return shots, removed
