import os
import sys
import json
import time
from datetime import datetime
import glob
import shutil
import re
import asyncio
import subprocess
from pathlib import Path
from typing import Optional, Dict, Any, List

try:
    from pydantic import BaseModel, model_validator
    HAS_PYDANTIC_V2 = True
except ImportError:
    from pydantic import BaseModel, root_validator
    HAS_PYDANTIC_V2 = False

import uvicorn
from fastapi import FastAPI, BackgroundTasks, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware

# Module-level Port & Host Configuration
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 1515

BASE_DIR = Path(__file__).resolve().parent.parent
PIPELINE_DIR = BASE_DIR / "05_Production_Pipeline"
VIDEOS_DIR = BASE_DIR / "04_Assets" / "videos"
EXPORTS_DIR = BASE_DIR / "06_Exports"
CHARACTERS_DIR = BASE_DIR / "04_Assets" / "characters"
KEYFRAMES_DIR = BASE_DIR / "04_Assets" / "keyframes"
PROMPTS_FILE = BASE_DIR / "02_AI_Prompts" / "muse_ai_video_prompts.json"
BANANA_PROMPTS_FILE = BASE_DIR / "02_AI_Prompts" / "gemini_banana_prompts.json"
FILMMAKER_DIR = BASE_DIR / "FilmMaker"
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
DOWNLOADS_DIR = Path(os.path.expanduser("~")) / "Downloads"

# Ensure pipeline directory and base dir are on sys.path for direct imports
for p in [str(PIPELINE_DIR), str(BASE_DIR)]:
    if p not in sys.path:
        sys.path.insert(0, p)

# Reconfigure stdout/stderr to UTF-8 on Windows to prevent cp1252 UnicodeEncodeError
if sys.platform == "win32":
    try:
        sys.stdout.reconfigure(encoding="utf-8")
        sys.stderr.reconfigure(encoding="utf-8")
    except Exception:
        pass

# Pipeline & Muse2API Client
try:
    from muse_api_client import MuseApiClient, render_shot_via_api
    HAS_MUSE_API = True
except ImportError:
    HAS_MUSE_API = False
    MuseApiClient = None
    render_shot_via_api = None

# Antigravity Critic Gate
try:
    from antigravity_critic_gate import (
        evaluate_shot_gate, evaluate_scene_gate,
        FLOWKIT_ERROR_CATALOG, FLOWKIT_ERROR_TIERS,
        VideoCriticVerdict
    )
    HAS_CRITIC_GATE = True
except ImportError:
    HAS_CRITIC_GATE = False
    evaluate_shot_gate = None
    evaluate_scene_gate = None
    FLOWKIT_ERROR_CATALOG = {}
    FLOWKIT_ERROR_TIERS = {}
    VideoCriticVerdict = None

CRITIC_QUEUE_DIR = BASE_DIR / "04_Assets" / "critic_queue"
CRITIC_QUEUE_DIR.mkdir(parents=True, exist_ok=True)
VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
EXPORTS_DIR.mkdir(parents=True, exist_ok=True)

app = FastAPI(title="Thập Ngũ Niên - Cinema Review Studio", version="2.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Static mounts
app.mount("/assets/videos", StaticFiles(directory=str(VIDEOS_DIR)), name="videos")
app.mount("/assets/characters", StaticFiles(directory=str(CHARACTERS_DIR)), name="characters")
if KEYFRAMES_DIR.exists():
    app.mount("/assets/keyframes", StaticFiles(directory=str(KEYFRAMES_DIR)), name="keyframes")
if EXPORTS_DIR.exists():
    app.mount("/assets/exports", StaticFiles(directory=str(EXPORTS_DIR)), name="exports")
if CRITIC_QUEUE_DIR.exists():
    app.mount("/assets/critic_queue", StaticFiles(directory=str(CRITIC_QUEUE_DIR)), name="critic_queue")

# Global generation state
gen_state = {
    "status": "idle", # idle, generating, concatenating, completed, error
    "progress_text": "",
    "current_prompt": "",
    "last_video": "",
    "updated_at": time.time(),
    "logs": []
}

def log_msg(msg: str):
    ts = time.strftime("%H:%M:%S")
    entry = f"[{ts}] {msg}"
    gen_state["logs"].append(entry)
    gen_state["logs"] = gen_state["logs"][-30:] # keep last 30
    try:
        print(entry)
    except Exception:
        try:
            print(entry.encode("ascii", errors="replace").decode("ascii"))
        except Exception:
            pass

class GenerateRequest(BaseModel):
    prompt: Optional[str] = None
    shot_id: Optional[str] = None
    scene_id: Optional[str] = None
    title: Optional[str] = None
    input_frame: Optional[str] = None
    force: Optional[bool] = False
    session_name: Optional[str] = "muse"
    reverse_motion: Optional[bool] = None
    engine: Optional[str] = "auto"

    if HAS_PYDANTIC_V2:
        @model_validator(mode="after")
        def check_prompt_or_shot(self):
            if not self.prompt and not self.shot_id:
                raise ValueError("Either prompt or shot_id must be provided")
            return self
    else:
        @root_validator(pre=False)
        def check_prompt_or_shot(cls, values):
            if not values.get("prompt") and not values.get("shot_id"):
                raise ValueError("Either prompt or shot_id must be provided")
            return values

class ConcatRequest(BaseModel):
    scene_id: str
    output_path: Optional[str] = None
    crossfade_dur: Optional[float] = 1.0

class CriticEvalRequest(BaseModel):
    shot_id: str
    video_path: Optional[str] = None
    expected_character: Optional[str] = ""
    prompt: Optional[str] = ""
    force_engine: Optional[str] = "auto"

class BatchSceneRequest(BaseModel):
    scene_id: str
    engine: Optional[str] = "auto"
    force_rerun: Optional[bool] = False
    auto_concat: Optional[bool] = True

EPISODE_TITLES = {
    1: "Tập 1: Phong Trần Nhập Cuộc (Gia Biến)",
    2: "Tập 2: Thanh Lâu Nhập Mộng (Mã Giám Sinh & Sở Khanh)",
    3: "Tập 3: Hoa Lạc Vô Đoan (Đòn Ghen Hoạn Thư)",
    4: "Tập 4: Hoành Đao Trảm Lãng (Từ Hải Khởi Binh)",
    5: "Tập 5: Đoạn Trường Thập Ngũ (Tiền Đường Trầm Thân)",
    6: "Tập 6: Hoàn Nguyên Tái Ngộ (Kim Kiều Đoàn Tụ)"
}

# Cache for library scan
_library_cache: Dict[str, Any] = {}
_cache_time: float = 0.0
_cached_fingerprint: str = ""
CACHE_TTL: float = 30.0 # 30 seconds cache TTL fallback

def is_valid_video_file(p: Path) -> bool:
    """Lọc bỏ các file rác, file tạm, file cache upscale không hợp lệ."""
    for part in p.parts:
        if part.startswith(".") and part not in (".", ".."):
            return False
    name = p.name.lower()
    if name.startswith("chunk_") or name.startswith("temp_") or name.startswith("."):
        return False
    if name.endswith("_tmp.mp4") or name == "merged_video.mp4":
        return False
    return True

def extract_video_metadata(p: Path) -> Dict[str, Any]:
    """Trích xuất version (v1, v2, v3...), timestamp code (YYYYMMDD_HHMM), size và ngày sửa."""
    stat = p.stat()
    name = p.name
    lower = name.lower()
    mtime = stat.st_mtime

    # 1. Trích xuất version (v1, v2, v3...)
    v_m = re.search(r"[_.-]v(\d+)(?:[_.-]|$)", lower)
    if not v_m:
        v_m = re.search(r"_v(\d+)\.mp4$", lower)
    if not v_m:
        v_m = re.search(r"(?:^|_)v(\d+)(?:_|\.mp4)", lower)
    version_num = int(v_m.group(1)) if v_m else 1

    # 2. Trích xuất mã timestamp code YYYYMMDD_HHMM (kiểu 20260911_1725)
    ts_m = re.search(r"(202\d{5}_\d{4})", name)
    if ts_m:
        timestamp_code = ts_m.group(1)
    else:
        timestamp_code = time.strftime("%Y%m%d_%H%M", time.localtime(mtime))

    modified_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
    size_mb = round(stat.st_size / (1024 * 1024), 2)
    version_tag = f"[v{version_num} • {timestamp_code}]"

    return {
        "version_num": version_num,
        "version_str": f"v{version_num}",
        "timestamp_code": timestamp_code,
        "version_tag": version_tag,
        "size_mb": size_mb,
        "modified": modified_str,
        "mtime": mtime
    }

def get_library_fingerprint() -> Dict[str, Any]:
    """Tính toán fingerprint tức thời (số lượng file hợp lệ + mtime mới nhất) để phát hiện video mới sinh trong < 1ms."""
    try:
        raw_files = [f for f in VIDEOS_DIR.glob("*.mp4") if is_valid_video_file(f)]
        export_files = [f for f in EXPORTS_DIR.rglob("*.mp4") if is_valid_video_file(f)]
        count = len(raw_files) + len(export_files)
        latest_mtime = 0.0
        if raw_files:
            latest_mtime = max(latest_mtime, max(f.stat().st_mtime for f in raw_files))
        if export_files:
            latest_mtime = max(latest_mtime, max(f.stat().st_mtime for f in export_files))
        return {
            "total_videos": count,
            "latest_mtime": latest_mtime,
            "version_tag": f"{count}_{int(latest_mtime)}"
        }
    except Exception:
        return {"total_videos": 0, "latest_mtime": 0.0, "version_tag": "0_0"}

def resolve_next_version(base_dir: Path, stem: str, suffix: str = ".mp4") -> str:
    """Tự động xác định tên file version tiếp theo (_v1, _v2, _v3...)."""
    pattern = re.compile(rf"^{re.escape(stem)}_v(\d+){re.escape(suffix)}$", re.IGNORECASE)
    highest_v = 0
    for f in base_dir.glob(f"{stem}*{suffix}"):
        m = pattern.match(f.name)
        if m:
            highest_v = max(highest_v, int(m.group(1)))
    return f"{stem}_v{highest_v + 1}{suffix}"

def build_library_data(force: bool = False) -> Dict[str, Any]:
    global _library_cache, _cache_time, _cached_fingerprint
    now = time.time()
    curr_fp = get_library_fingerprint()

    # Nếu không ép buộc, cache còn hạn và fingerprint trên đĩa không đổi -> dùng cache
    if not force and _library_cache and (now - _cache_time < CACHE_TTL) and (_cached_fingerprint == curr_fp["version_tag"]):
        return _library_cache

    all_exports = [p for p in EXPORTS_DIR.rglob("*.mp4") if is_valid_video_file(p)]
    all_raw = [p for p in VIDEOS_DIR.glob("*.mp4") if is_valid_video_file(p)]

    feature_masters = []
    episode_masters = []
    scene_masters = []
    shorts = []
    raw_shots_by_scene: Dict[str, List[Dict[str, Any]]] = {}

    total_exports_size = sum(p.stat().st_size for p in all_exports)
    total_raw_size = sum(p.stat().st_size for p in all_raw)

    for p in all_exports:
        meta = extract_video_metadata(p)
        name = p.name
        lower = name.lower()
        rel_to_exports = p.relative_to(EXPORTS_DIR).as_posix()
        size_mb = meta["size_mb"]
        mtime = meta["modified"]
        url = f"/assets/exports/{rel_to_exports}"
        abs_path = str(p.resolve())
        version_num = meta["version_num"]
        version_str = meta["version_str"]
        timestamp_code = meta["timestamp_code"]
        version_tag = meta["version_tag"]

        # Shorts 9:16
        if "9x16" in lower or "shorts_1m_9x16" in rel_to_exports:
            ep_m = re.search(r"ep0?(\d+)", lower)
            ep_num = int(ep_m.group(1)) if ep_m else 1
            clean_title = name.replace(".mp4", "").replace("_9x16", "").replace("short_", "Short ").replace("_", " ").title()
            
            short_titles = {
                1: "Khuê Phòng & Gia Biến (Đêm Mưa)",
                2: "Cạm Bẫy Sở Khanh & Lầu Ngưng Bích",
                3: "Đòn Ghen Hoạn Thư & Tiệc Hoa Viên",
                4: "Oai Danh Từ Hải & Phá Vòng Vây",
                5: "Tiệc Say Phục Kích & Nỗi Oan Tiền Đường",
                6: "Trùng Phùng Đại Kết (15 Năm Lưu Lạc)"
            }
            custom_title = f"Shorts Tập {ep_num}: {short_titles.get(ep_num, clean_title)} {version_tag}"

            shorts.append({
                "name": name,
                "title": custom_title,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "episode": f"ep0{ep_num}",
                "episode_num": ep_num,
                "aspect": "9:16",
                "duration_str": "60 Giây (9:16)",
                "category": "short_9x16",
                "badge": f"Shorts Tập {ep_num} • {version_str}",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })
            continue

        # Grand Roadshow & Multi-episode cuts & Continuous
        if any(k in lower for k in ["ep01_to_ep06", "ep01_to_ep05", "ep01_to_ep04", "ep01_ep02_ep03", "ep01_ep02_roadshow", "roadshow"]):
            dur_m = re.search(r"(\d+s|\d+min|\d+m\d+s)", lower)
            dur_str = dur_m.group(1) if dur_m else ""
            dur_str = dur_str.replace("min", " Phút").replace("m", " Phút ").replace("s", " Giây")
            
            title = f"Grand Roadshow Master {version_tag}"
            desc = "Tuyển tập dài kết nối đa tập hoàn chỉnh, chuẩn âm thanh AAC và hình ảnh điện ảnh."
            priority = 50
            badge = f"Roadshow Cut • {version_str}"

            if "ep01_to_ep06" in lower:
                title = f"ĐẠI BẢN MASTER TOÀN BỘ 6 TẬP (FULL FEATURE CUT) {version_tag}"
                desc = "Bản tổng hợp tối thượng 17 phút 40 giây kết nối toàn bộ 6 tập phim Thập Ngũ Niên, từ Kinh kỳ, Trao duyên, Đòn ghen, Khởi binh Từ Hải đến Bến Tiền Đường."
                priority = 100
                badge = f"Full Feature Master (17m40s) • {version_str}"
            elif "ep01_to_ep05" in lower:
                title = f"Roadshow Tuyển Tập: Tập 1 đến Tập 5 {version_tag}"
                desc = "Tổng hợp liên hoàn 5 tập đầu tiên (16 phút 20 giây) - Từ hội Thanh Minh đến trước thềm Tiền Đường."
                priority = 90
                badge = f"Roadshow Tập 1-5 (16m20s) • {version_str}"
            elif "ep01_to_ep04" in lower:
                title = f"Roadshow Tuyển Tập: Tập 1 đến Tập 4 {version_tag}"
                desc = "Tổng hợp liên hoàn 4 tập (15 phút) - Khởi sắc gia đình họ Vương đến chiến trường Từ Hải."
                priority = 85
                badge = f"Roadshow Tập 1-4 (15m) • {version_str}"
            elif "ep01_ep02_ep03" in lower:
                title = f"Roadshow Tuyển Tập: Tập 1 - 2 - 3 {version_tag}"
                desc = "Chặng đường đầu của Thúy Kiều (13 phút) - Kinh kỳ, Lầu xanh Lâm Tri và Đòn ghen Hoạn Thư."
                priority = 80
                badge = f"Roadshow Tập 1-3 (13m) • {version_str}"
            elif "ep01_ep02" in lower:
                title = f"Roadshow Tuyển Tập: Tập 1 & Tập 2 {version_tag}"
                desc = "Trọn vẹn 2 tập đầu (10 phút 40 giây) - Biến cố gia đình và đêm trao duyên đẫm lệ."
                priority = 75
                badge = f"Roadshow Tập 1-2 (10m40s) • {version_str}"

            feature_masters.append({
                "name": name,
                "title": title,
                "description": desc,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or "Bản Dài",
                "badge": badge,
                "priority": priority,
                "category": "feature_master",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })
            continue

        # Intro Master & Cuts (20s)
        if (("intro" in lower and "prologue" not in lower) or ("thap_ngu_nien_intro" in lower)) and "prologue" not in lower:
            dur_m = re.search(r"(\d+s|\d+min|\d+m\d+s)", lower)
            dur_str = dur_m.group(1) if dur_m else "20s"
            dur_str = dur_str.replace("min", " Phút").replace("m", " Phút ").replace("s", " Giây")
            
            is_master = "master" in lower or "complete" in lower
            title = f"BIỂU TRƯNG & INTRO ĐẦU PHIM (INTRO MASTER - 20S) {version_tag}" if is_master else f"Intro Đầu Phim: Dải Lụa & Biểu Trưng ({dur_str}) {version_tag}"
            desc = "Master 20 giây: Dải lụa xanh ngọc bích lướt qua mặt hồ sương sớm, luồng sáng hoàng kim bùng nổ hiển lộ biểu trưng Thập Ngũ Niên dát vàng viền chu sa đỏ. Thiết kế âm thanh: Tiếng nước chảy róc rách trong vắt, thảm âm thanh cổ phong tĩnh lặng hòa quyện hoàn hảo với Prologue. Chuẩn EBU R128 (-14.0 LUFS)."
            badge = f"Intro Master (20s) • {version_str}" if is_master else f"Intro Cut (20s) • {version_str}"
            priority = 1200 if is_master else 45

            feature_masters.append({
                "name": name,
                "title": title,
                "description": desc,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or f"{size_mb} MB",
                "badge": badge,
                "priority": priority,
                "category": "feature_master" if is_master else "continuous_cut",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })

            if is_master:
                scene_masters.append({
                    "name": name,
                    "episode": "ep01",
                    "episode_num": 1,
                    "scene": "scene00_intro",
                    "scene_num": -1,
                    "title": f"Tập 1 — Biểu Trưng & Intro Đầu Phim Master {version_tag}",
                    "url": url,
                    "path": abs_path,
                    "size_mb": size_mb,
                    "modified": mtime,
                    "aspect": "16:9",
                    "badge": f"Tập 1 • Intro 20s • {version_str}",
                    "category": "scene_master",
                    "version": version_num,
                    "version_str": version_str,
                    "timestamp_code": timestamp_code,
                    "version_tag": version_tag,
                    "is_latest": True,
                    "mtime": meta["mtime"]
                })
            continue

        # Prologue Master & Cuts
        if "prologue" in lower and "intro" not in lower:
            dur_m = re.search(r"(\d+s|\d+min|\d+m\d+s)", lower)
            dur_str = dur_m.group(1) if dur_m else "40s"
            dur_str = dur_str.replace("min", " Phút").replace("m", " Phút ").replace("s", " Giây")
            
            is_master = "master" in lower
            title = f"PHÂN ĐOẠN DẪN NHẬP (PROLOGUE MASTER - 40S) {version_tag}" if is_master else f"Phân Đoạn Dẫn Nhập: Prologue ({dur_str}) {version_tag}"
            desc = "Master 40 giây: Dẫn nhập hoài niệm làng quê Việt Nam thập niên 1980 chuyển cảnh sang kinh thành Đại Minh Gia Tĩnh. 100% giọng cụ già ngâm Kiều truyền cảm, chuẩn EBU R128 (-14.0 LUFS)."
            badge = f"Prologue Master (40s) • {version_str}" if is_master else f"Prologue Concat (40s) • {version_str}"
            priority = 1000 if is_master else 42

            feature_masters.append({
                "name": name,
                "title": title,
                "description": desc,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or f"{size_mb} MB",
                "badge": badge,
                "priority": priority,
                "category": "feature_master" if is_master else "continuous_cut",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })

            if is_master:
                scene_masters.append({
                    "name": name,
                    "episode": "ep01",
                    "episode_num": 1,
                    "scene": "scene00_prologue",
                    "scene_num": 0,
                    "title": f"Tập 1 — Phân Đoạn Dẫn Nhập Prologue Master {version_tag}",
                    "url": url,
                    "path": abs_path,
                    "size_mb": size_mb,
                    "modified": mtime,
                    "aspect": "16:9",
                    "badge": f"Tập 1 • Prologue 40s • {version_str}",
                    "category": "scene_master",
                    "version": version_num,
                    "version_str": version_str,
                    "timestamp_code": timestamp_code,
                    "version_tag": version_tag,
                    "is_latest": True,
                    "mtime": meta["mtime"]
                })
            continue

        # Continuous cuts
        if "continuous_master" in lower or "intro_prologue" in lower:
            dur_m = re.search(r"(\d+s|\d+min|\d+m\d+s)", lower)
            dur_str = dur_m.group(1) if dur_m else ""
            dur_str = dur_str.replace("min", " Phút").replace("m", " Phút ").replace("s", " Giây")
            
            title = f"Bản Ghép Nối Liên Hoàn ({dur_str}) {version_tag}"
            badge = f"Continuous Cut • {version_str}"
            priority = 40

            if "intro_prologue_scenes01_02" in lower:
                title = f"Liên Hoàn Intro + Prologue + Cảnh 1 & Cảnh 2 {version_tag}"
                badge = f"Liên Hoàn 160s • {version_str}"
                desc = "Bản ghép nối 160 giây: Intro + Prologue + Cảnh 1 & Cảnh 2."
                priority = 45
            elif "intro_prologue_scene01" in lower:
                title = f"Liên Hoàn Intro + Prologue + Cảnh 1 {version_tag}"
                badge = f"Liên Hoàn 110s • {version_str}"
                desc = "Bản ghép nối 110 giây: Intro + Prologue + Cảnh 1."
                priority = 45
            elif "intro_prologue" in lower:
                title = f"LIÊN HOÀN KHỚP NỐI: INTRO (20S) + PROLOGUE (40S) {version_tag}"
                badge = f"Liên Hoàn 60s • {version_str}"
                desc = "Bản ghép nối 60 giây kiểm định độ ăn khớp: Dải lụa xanh & biểu trưng Thập Ngũ Niên chuyển tiếp mượt mà vào hoài niệm làng quê 1980s và tiếng cụ già ngâm Kiều truyền cảm. Chuẩn EBU R128 (-14.0 LUFS)."
                priority = 900
            else:
                desc = f"Bản ghép nối liền mạch âm thanh & hình ảnh thời lượng {dur_str}."

            feature_masters.append({
                "name": name,
                "title": title,
                "description": desc,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or f"{size_mb} MB",
                "badge": badge,
                "priority": priority,
                "category": "continuous_cut",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })
            continue

        # Episode Grand Master (hỗ trợ cả grand_feature_master, grand_master, master_30m, ep01_master_30m)
        if any(k in lower for k in ["grand_feature_master", "grand_master", "master_30m", "_master_30m"]) or (("ep0" in lower or "ep1" in lower) and "master" in lower and "scene" not in lower and "intro" not in lower and "prologue" not in lower):
            ep_m = re.search(r"ep0?(\d+)", lower)
            ep_num = int(ep_m.group(1)) if ep_m else 1
            dur_m = re.search(r"(\d+min|\d+m\d+s|\d+s|30m)", lower)
            dur_str = "30 Phút" if "30m" in lower else (dur_m.group(1) if dur_m else "")
            dur_str = dur_str.replace("min", " Phút").replace("m", " Phút ").replace("s", " Giây")

            ep_name = EPISODE_TITLES.get(ep_num, f"Tập {ep_num}")

            is_30m = "30m" in lower
            title = f"{ep_name} — ĐẠI BẢN MASTER HOÀN THIỆN (30 PHÚT) {version_tag}" if is_30m else f"{ep_name} — Bản Grand Master {version_tag}"
            desc = f"Bản Master điện ảnh 30 phút trọn vẹn toàn bộ 15 phân cảnh Tập {ep_num} + Intro & Prologue, âm thanh chuẩn EBU R128 (-14.0 LUFS), hình ảnh 1280x720 24fps FlowKit Smart Stitching." if is_30m else f"Master trọn vẹn toàn bộ các cảnh trong Tập {ep_num}, bảo toàn âm thanh ngũ cung và chất lượng hình ảnh cao nhất."
            badge = f"Master Tập {ep_num} • {version_str}" if is_30m else f"Master Tập {ep_num} • {version_str}"
            priority = 5000 if is_30m else 100

            entry = {
                "name": name,
                "episode": f"ep0{ep_num}",
                "episode_num": ep_num,
                "title": title,
                "description": desc,
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or f"{size_mb} MB",
                "badge": badge,
                "priority": priority,
                "category": "feature_master" if is_30m else "episode_master",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            }
            episode_masters.append(entry)
            feature_masters.append(entry)
            continue

        # Scene Cinematic Masters in Exports
        if "cinematic_master" in lower or ("master" in lower and "scene" in lower):
            ep_m = re.search(r"ep0?(\d+)", lower)
            sc_m = re.search(r"scene0?(\d+)", lower)
            ep_num = int(ep_m.group(1)) if ep_m else 1
            sc_num = int(sc_m.group(1)) if sc_m else 1

            scene_masters.append({
                "name": name,
                "episode": f"ep0{ep_num}",
                "episode_num": ep_num,
                "scene": f"scene{sc_num:02d}",
                "scene_num": sc_num,
                "title": f"Tập {ep_num} — Cảnh {sc_num:02d} Master {version_tag}",
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "badge": f"Tập {ep_num} • Cảnh {sc_num:02d} • {version_str}",
                "category": "scene_master",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })
            continue

    # Raw shots and scene masters in 04_Assets/videos
    raw_files_list = []
    seen_scene_master_names = {sm["name"] for sm in scene_masters}

    for p in all_raw:
        meta = extract_video_metadata(p)
        name = p.name
        lower = name.lower()
        size_mb = meta["size_mb"]
        mtime = meta["modified"]
        url = f"/assets/videos/{name}"
        abs_path = str(p.resolve())
        version_num = meta["version_num"]
        version_str = meta["version_str"]
        timestamp_code = meta["timestamp_code"]
        version_tag = meta["version_tag"]

        is_intro = ("intro" in lower and "prologue" not in lower) or ("thap_ngu_nien_intro" in lower)
        is_prologue = "prologue" in lower

        ep_m = re.search(r"ep0?(\d+)", lower)
        sc_m = re.search(r"scene0?(\d+)", lower)
        sh_m = re.search(r"shot_?0?(\d+)", lower)
        
        if is_intro:
            ep_num = 1
            sc_num = -1
            sh_num = int(sh_m.group(1)) if sh_m else (1 if "shot01" in lower or "shot1" in lower else (2 if "shot02" in lower or "shot2" in lower else 0))
            scene_key = "ep01_scene00_intro"
        elif is_prologue:
            ep_num = 1
            sc_num = 0
            sh_num = int(sh_m.group(1)) if sh_m else 0
            scene_key = "ep01_scene00_prologue"
        else:
            ep_num = int(ep_m.group(1)) if ep_m else 1
            sc_num = int(sc_m.group(1)) if sc_m else 1
            sh_num = int(sh_m.group(1)) if sh_m else 0
            scene_key = f"ep0{ep_num}_scene{sc_num:02d}"

        # Index Scene Masters stored in 04_Assets/videos/ (e.g. ep01_scene01_master_v1.mp4)
        has_intro_master = any("intro" in sm["name"].lower() for sm in scene_masters)
        if (("master" in lower and "scene" in lower) or ("_master" in lower and "shot" not in lower) or (is_prologue and "master" in lower) or (is_intro and not has_intro_master and ("complete" in lower or ("master" in lower and "shot" not in lower)))) and name not in seen_scene_master_names:
            scene_masters.append({
                "name": name,
                "episode": f"ep0{ep_num}",
                "episode_num": ep_num,
                "scene": "scene00_intro" if is_intro else ("scene00_prologue" if is_prologue else f"scene{sc_num:02d}"),
                "scene_num": sc_num,
                "title": f"Tập 1 — Biểu Trưng & Intro Đầu Phim Master {version_tag}" if is_intro else (f"Tập 1 — Phân Đoạn Dẫn Nhập Prologue Master {version_tag}" if is_prologue else f"Tập {ep_num} — Cảnh {sc_num:02d} Master {version_tag}"),
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "badge": f"Tập 1 • Intro 20s • {version_str}" if is_intro else (f"Tập 1 • Prologue 40s • {version_str}" if is_prologue else f"Tập {ep_num} • Cảnh {sc_num:02d} • {version_str}"),
                "category": "scene_master",
                "version": version_num,
                "version_str": version_str,
                "timestamp_code": timestamp_code,
                "version_tag": version_tag,
                "is_latest": True,
                "mtime": meta["mtime"]
            })
            seen_scene_master_names.add(name)

        if scene_key not in raw_shots_by_scene:
            raw_shots_by_scene[scene_key] = []

        is_shot = "shot" in lower

        intro_shot_titles = {
            1: "Intro • Shot 01: Dải Lụa Thanh Xuân Lướt Qua Mặt Hồ Sương Sớm",
            2: "Intro • Shot 02: Dải Lụa Xoáy Khởi Nguyên & Biểu Trưng Thập Ngũ Niên"
        }

        prologue_shot_titles = {
            1: "Prologue • Shot 01: Làng Quê 1980s Chiều Hoàng Hôn",
            2: "Prologue • Shot 02: Ông Cụ & Đứa Bé Bên Ngọn Đèn Dầu",
            3: "Prologue • Shot 03: Đôi Mắt Trầm Ngâm & Vòng Xoáy Thời Không",
            4: "Prologue • Shot 04: Cổng Thành Đại Minh Gia Tĩnh"
        }

        if is_intro and is_shot:
            base_t = intro_shot_titles.get(sh_num, f"Intro • Shot {sh_num:02d}")
            item_title = f"{base_t} {version_tag} (10s)"
        elif is_prologue and is_shot:
            base_t = prologue_shot_titles.get(sh_num, f"Prologue • Shot {sh_num:02d}")
            item_title = f"{base_t} {version_tag} (10s)"
        elif is_shot:
            item_title = f"Tập {ep_num} • Cảnh {sc_num:02d} • Shot {sh_num:02d} {version_tag} (10s)"
        else:
            clean_stem = name.replace(".mp4", "").replace("_", " ")
            item_title = f"{clean_stem} {version_tag}"

        item = {
            "name": name,
            "episode": f"ep0{ep_num}",
            "episode_num": ep_num,
            "scene": "scene00_intro" if is_intro else ("scene00_prologue" if is_prologue else f"scene{sc_num:02d}"),
            "scene_num": sc_num,
            "shot": f"shot{sh_num:02d}" if is_shot else "misc",
            "shot_num": sh_num,
            "title": item_title,
            "url": url,
            "path": abs_path,
            "size_mb": size_mb,
            "modified": mtime,
            "duration_str": "10s" if is_shot else "Raw/Cut",
            "aspect": "16:9",
            "category": "raw_shot" if is_shot else "raw_asset",
            "version": version_num,
            "version_str": version_str,
            "timestamp_code": timestamp_code,
            "version_tag": version_tag,
            "is_latest": True,
            "mtime": meta["mtime"]
        }
        raw_shots_by_scene[scene_key].append(item)
        raw_files_list.append(item)

    # Đánh dấu is_latest và sắp xếp khoa học cho từng phân cảnh:
    # Bản mới nhất (version cao nhất, timestamp mới nhất) đứng đầu trong nhóm của mỗi shot
    for sc in raw_shots_by_scene:
        shot_groups: Dict[int, List[Dict[str, Any]]] = {}
        for it in raw_shots_by_scene[sc]:
            sh_n = it.get("shot_num", 0)
            shot_groups.setdefault(sh_n, []).append(it)
        for sh_n, group in shot_groups.items():
            group.sort(key=lambda x: (x.get("version", 1), x.get("mtime", 0)), reverse=True)
            for idx, it in enumerate(group):
                it["is_latest"] = (idx == 0)
        # Sắp xếp: shot_num tăng dần, cùng shot_num thì version giảm dần (bản mới nhất đứng trước!)
        raw_shots_by_scene[sc].sort(key=lambda x: (x.get("shot_num", 0), -x.get("version", 1), -x.get("mtime", 0)))

    # Đánh dấu is_latest và sắp xếp cho Scene Masters
    sc_master_groups: Dict[str, List[Dict[str, Any]]] = {}
    for sm in scene_masters:
        k = f"{sm.get('episode', 'ep01')}_{sm.get('scene', 'sc01')}"
        sc_master_groups.setdefault(k, []).append(sm)
    for k, sm_list in sc_master_groups.items():
        sm_list.sort(key=lambda x: (x.get("version", 1), x.get("mtime", 0)), reverse=True)
        for idx, it in enumerate(sm_list):
            it["is_latest"] = (idx == 0)

    scene_masters.sort(key=lambda x: (x.get("episode_num", 1), x.get("scene_num", 0), -x.get("version", 1), -x.get("mtime", 0)))
    feature_masters.sort(key=lambda x: (x.get("priority", 0), x.get("mtime", 0)), reverse=True)
    episode_masters.sort(key=lambda x: (x.get("episode_num", 1), -x.get("mtime", 0)))
    shorts.sort(key=lambda x: (x.get("episode_num", 1), -x.get("mtime", 0)))

    # Sort scene groups
    sorted_raw_scenes = []
    for sc_key in sorted(raw_shots_by_scene.keys()):
        items = raw_shots_by_scene[sc_key]
        ep_m = re.search(r"ep0?(\d+)", sc_key)
        sc_m = re.search(r"scene0?(\d+)", sc_key)
        ep_num = int(ep_m.group(1)) if ep_m else 1
        sc_num = int(sc_m.group(1)) if sc_m else 1

        if "intro" in sc_key:
            sc_title = f"Tập 1 — Intro Đầu Phim: Dải Lụa & Biểu Trưng Thập Ngũ Niên ({len(items)} shots)"
            sc_num = -1
        elif "prologue" in sc_key:
            sc_title = f"Tập 1 — Phân Đoạn Dẫn Nhập: Prologue ({len(items)} shots)"
            sc_num = 0
        else:
            sc_title = f"Tập {ep_num} — Cảnh {sc_num:02d} ({len(items)} shots)"

        sorted_raw_scenes.append({
            "scene_key": sc_key,
            "episode_num": ep_num,
            "scene_num": sc_num,
            "title": sc_title,
            "count": len(items),
            "shots": items
        })

    summary = {
        "total_videos": len(all_exports) + len(all_raw),
        "feature_masters_count": len(feature_masters),
        "episode_masters_count": len(episode_masters),
        "scene_masters_count": len(scene_masters),
        "shorts_count": len(shorts),
        "raw_shots_count": len(all_raw),
        "total_exports_size_gb": round(total_exports_size / (1024**3), 2),
        "total_raw_size_gb": round(total_raw_size / (1024**3), 2),
        "total_storage_gb": round((total_exports_size + total_raw_size) / (1024**3), 2),
        "library_fingerprint": curr_fp
    }

    _library_cache = {
        "summary": summary,
        "feature_masters": feature_masters,
        "episode_masters": episode_masters,
        "scene_masters": scene_masters,
        "shorts": shorts,
        "raw_scenes": sorted_raw_scenes,
        "raw_files": raw_files_list,
        "episodes_meta": [
            {"id": f"ep0{i}", "num": i, "title": EPISODE_TITLES[i]} for i in range(1, 7)
        ]
    }
    _cache_time = now
    _cached_fingerprint = curr_fp["version_tag"]
    return _library_cache

@app.get("/", response_class=HTMLResponse)
async def serve_index():
    index_path = TEMPLATES_DIR / "index.html"
    if not index_path.exists():
        return HTMLResponse("<h1>index.html not found</h1>", status_code=404)
    return HTMLResponse(index_path.read_text(encoding="utf-8"))

@app.get("/api/library")
async def get_library():
    return build_library_data()

@app.post("/api/library/refresh")
async def refresh_library():
    """Làm mới bộ nhớ đệm thư viện video và quét lại toàn bộ file trên ổ đĩa."""
    global _library_cache
    _library_cache = {}
    return build_library_data(force=True)

@app.get("/api/gateway/health")
async def get_gateway_health():
    """Kiểm tra sức khỏe cổng dịch vụ Muse2API Gateway và trạng thái Account Pool."""
    if not HAS_MUSE_API or MuseApiClient is None:
        return {
            "ready": False,
            "error": "Module muse_api_client không khả dụng",
            "accounts": {"total": 0, "available": 0},
            "driver": {"ok": False, "tabs": 0, "busy_tabs": 0}
        }
    try:
        client = MuseApiClient()
        health = client.check_health(timeout=3.0)
        return {
            "ready": bool(health.get("ready", False)),
            "base_url": client.base_url,
            "accounts": health.get("accounts", {}),
            "driver": health.get("driver", {}),
            "raw": health
        }
    except Exception as e:
        return {
            "ready": False,
            "error": str(e),
            "accounts": {"total": 0, "available": 0},
            "driver": {"ok": False, "tabs": 0, "busy_tabs": 0}
        }

@app.get("/api/events")
async def event_stream():
    """Server-Sent Events (SSE) stream trạng thái thời gian thực và log cho frontend."""
    async def event_generator():
        while True:
            try:
                payload = json.dumps({
                    "status": gen_state.get("status", "idle"),
                    "progress_text": gen_state.get("progress_text", ""),
                    "current_prompt": gen_state.get("current_prompt", ""),
                    "last_video": gen_state.get("last_video", ""),
                    "updated_at": gen_state.get("updated_at", time.time()),
                    "logs": gen_state.get("logs", [])[-15:],
                    "library_fingerprint": get_library_fingerprint()
                }, ensure_ascii=False)
                yield f"data: {payload}\n\n"
            except Exception:
                pass
            current_st = gen_state.get("status")
            await asyncio.sleep(1.5 if current_st in ("generating", "concatenating") else 5.0)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"
        }
    )

@app.get("/api/videos")
async def list_videos(category: Optional[str] = None):
    lib = build_library_data()
    ordered = []
    ordered.extend(lib["feature_masters"])
    ordered.extend(lib["episode_masters"])
    ordered.extend(lib["scene_masters"])
    ordered.extend(lib["shorts"])
    ordered.extend(lib["raw_files"])
    
    if category:
        ordered = [v for v in ordered if v.get("category") == category]
        
    return {"videos": ordered, "summary": lib["summary"]}

CHARACTER_TITLES = {
    "thuy_kieu_maiden_16yo": {"name": "Thúy Kiều (Thanh Xuân 16 Tuổi)", "role": "Nữ chính • Tuyệt sắc giai nhân, tài hoa bạc mệnh"},
    "thuy_van_maiden_16yo": {"name": "Thúy Vân (Khuê Các 16 Tuổi)", "role": "Em gái Thúy Kiều • Khuôn trăng đầy đặn, nét ngài nở nang"},
    "kim_trong_18yo": {"name": "Kim Trọng (Thư Sinh 18 Tuổi)", "role": "Nam chính • Bậc tài danh tuấn tú, mối tình đầu của Kiều"},
    "thuy_kieu_hoa_no_servant": {"name": "Thúy Kiều (Hoa Nô Thúc Phủ)", "role": "Thân phận hoa nô hầu hạ gia đình Thúc Sinh & Hoạn Thư"},
    "thuy_kieu_trieu_doanh_phu_nhan": {"name": "Thúy Kiều (Triêu Doanh Phu Nhân)", "role": "Phu nhân của Từ Hải tại đại doanh khởi nghĩa"},
    "thuy_kieu_ao_nau_song": {"name": "Thúy Kiều (Áo Nâu Sồng)", "role": "Sau khi trầm thân Tiền Đường, nương nhờ cửa Phật Giác Duyên"},
    "kim_trong_mandarin_33yo": {"name": "Kim Trọng (Tri Phủ 33 Tuổi)", "role": "Quan tri phủ sau 15 năm đỗ đạt, tìm lại Thúy Kiều"},
    "tu_hai": {"name": "Từ Hải (Đại Tướng Quân)", "role": "Anh hùng cái thế, khởi binh lập nghiệp, cứu Kiều khỏi lầu xanh"},
    "dam_tien": {"name": "Đạm Tiên (Hương Hồn Ca Nhi)", "role": "Nấm mồ cỏ úa tiết Thanh Minh, hồn ma tiên báo số kiếp Kiều"},
    "vuong_ong_55yo": {"name": "Vương Ông (Gia Trưởng Họ Vương 55 Tuổi)", "role": "Cha Thúy Kiều, viên ngoại gia giáo lâm vòng lao lý oan khiên"},
    "vuong_ba_50yo": {"name": "Vương Bà (Hiền Thê Họ Vương 50 Tuổi)", "role": "Mẹ Thúy Kiều, tần tảo hiền đức"},
    "vuong_quan_16yo": {"name": "Vương Quan (Thư Sinh 16 Tuổi)", "role": "Em trai Thúy Kiều, bạn đồng môn của Kim Trọng"},
    "gia_no_nu_vuong": {"name": "Gia Nô Nữ Vương Gia", "role": "Gia nhân hầu hạ buồng khuê hai nàng Kiều"},
    "kim_attendants": {"name": "Tùy Tùng Kim Gia", "role": "Đoàn tùy tùng theo chân Kim Trọng du xuân Thanh Minh"},
    "quan_gia_kim_trong": {"name": "Quản Gia Kim Phủ", "role": "Quản lý phủ đệ họ Kim"},
    "quan_gia_vuong": {"name": "Quản Gia Vương Gia", "role": "Quản gia phủ viên ngoại họ Vương"},
    "tieu_dong_kim_trong": {"name": "Tiểu Đồng Kim Trọng", "role": "Cậu bé theo hầu gánh sách cho Kim Trọng"},
    "vuong_servants": {"name": "Gia Nhân Vương Phủ", "role": "Người hầu gánh lễ tảo mộ tiết Thanh Minh"},
    "thuc_sinh": {"name": "Thúc Sinh (Khách Biên Đình)", "role": "Thương gia chuộc Kiều khỏi lầu xanh nhưng nhu nhược"},
    "hoan_thu": {"name": "Hoạn Thư (Đại Tiểu Thư Hoạn Phủ)", "role": "Chính thất Thúc Sinh, đòn ghen trác tuyệt sâu cay"},
    "ma_giam_sinh": {"name": "Mã Giám Sinh (Kẻ Buôn Người)", "role": "Đội lốt thư sinh giám sinh mua Kiều với giá bốn trăm vàng"},
    "tu_ba": {"name": "Tú Bà (Chủ Lầu Xanh Lâm Tri)", "role": "Mụ chủ lầu xanh quỷ quyệt ép Kiều tiếp khách"},
    "so_khanh": {"name": "Sở Khanh (Kẻ Lừa Tình Bạc Ác)", "role": "Tay trong lầu xanh bày mưu cứu vớt giả rồi bỏ rơi Kiều"},
    "ho_ton_hien": {"name": "Hồ Tôn Hiến (Tổng Đốc Trọng Thần)", "role": "Đại thần triều đình dùng kế mỹ nhân và chiêu hàng hại Từ Hải"},
    "sai_nha": {"name": "Sai Nha (Lũ Đầu Trâu Mặt Ngựa)", "role": "Bọn sai nha ập vào cướp phá bức hại Vương gia"},
    "thang_ban_to": {"name": "Thằng Bán Tơ (Kẻ Vu Oan)", "role": "Kẻ vu oan giáng họa khiến gia đình họ Vương tan vỡ"},
    "thang_ban_to_sai_nha": {"name": "Thằng Bán Tơ & Sai Nha", "role": "Nhóm sai nha và kẻ cáo gian"},
    "giac_duyen": {"name": "Sư Giác Duyên (Ni Cô Cứu Khổ)", "role": "Sư bà cứu Thúy Kiều ở bến sông Tiền Đường"},
    "pho_tuong_tu_hai": {"name": "Phó Tướng Từ Hải", "role": "Chiến tướng hộ vệ sa trường bên cạnh Từ Hải"}
}

GROUP_LABELS = {
    "01_Main_Protagonists": "Nhóm 1: Tuyến Nhân Vật Chính (Main Protagonists)",
    "02_Vuong_Family_And_Fate": "Nhóm 2: Vương Tộc & Hồn Ma Tiền Định (Vuong Family & Fate)",
    "03_Thuc_Clan_And_Hoan_Mansion": "Nhóm 3: Gia Tộc Thúc Sinh & Hoạn Phủ (Thuc Clan & Hoan Mansion)",
    "04_Brokers_And_Brothels": "Nhóm 4: Mối Lái & Lầu Xanh Lâm Tri (Brokers & Brothels)",
    "05_Imperial_Court_And_Officials": "Nhóm 5: Triều Đình, Quan Lại & Sai Nha (Imperial Court & Officials)",
    "06_Buddhist_And_Spiritual": "Nhóm 6: Cửa Thiền & Tâm Linh Cứu Độ (Buddhist & Spiritual)",
    "07_Warlord_Camp_And_Rebels": "Nhóm 7: Quân Doanh Từ Hải & Sa Trường (Warlord Camp & Rebels)"
}

@app.get("/api/characters")
async def list_characters():
    groups = {}
    characters_list = []
    if CHARACTERS_DIR.exists():
        for group_dir in sorted(CHARACTERS_DIR.iterdir()):
            if not group_dir.is_dir():
                continue
            group_name = group_dir.name
            group_label = GROUP_LABELS.get(group_name, group_name.replace('_', ' '))
            legacy_items = []
            group_characters = []

            # Quét các thư mục con nhân vật (Character-Centric Structure)
            char_subdirs = [d for d in sorted(group_dir.iterdir()) if d.is_dir()]
            
            # Fallback nếu ảnh nằm trực tiếp ở group_dir
            root_pngs = sorted(group_dir.glob("*.png"))
            for img in root_pngs:
                is_720p = "720p" in img.name
                legacy_items.append({
                    "name": img.name,
                    "stem": img.stem,
                    "url": f"/assets/characters/{group_name}/{img.name}",
                    "path": str(img.resolve()),
                    "is_720p": is_720p,
                    "size_mb": round(img.stat().st_size / (1024 * 1024), 2)
                })

            for char_dir in char_subdirs:
                char_id = char_dir.name
                pngs = sorted(char_dir.glob("*.png"))
                views_dir = char_dir / "views"
                view_pngs = sorted(views_dir.glob("*.png")) if views_dir.exists() else []

                meta = CHARACTER_TITLES.get(char_id, {
                    "name": char_id.replace("_", " ").title(),
                    "role": "Nhân vật trong tác phẩm Thập Ngũ Niên"
                })

                # Tìm ảnh master, 720p, turnaround
                master_img = next((p for p in pngs if "master" in p.name.lower() and "turnaround" not in p.name.lower()), None)
                p720_img = next((p for p in pngs if "720p" in p.name.lower() and "turnaround" not in p.name.lower()), None)
                turnaround_master = next((p for p in pngs if "turnaround" in p.name.lower() and "master" in p.name.lower()), None)
                turnaround_720p = next((p for p in pngs if "turnaround" in p.name.lower() and "720p" in p.name.lower()), None)
                turnaround_any = turnaround_master or turnaround_720p or next((p for p in pngs if "turnaround" in p.name.lower()), None)

                primary_avatar = p720_img or master_img or (pngs[0] if pngs else None)
                primary_url = f"/assets/characters/{group_name}/{char_id}/{primary_avatar.name}" if primary_avatar else ""

                # Phân loại 4 góc nhìn chuẩn Turnaround
                view_dict = {}
                for v in view_pngs:
                    v_lower = v.name.lower()
                    v_url = f"/assets/characters/{group_name}/{char_id}/views/{v.name}"
                    if "portrait" in v_lower or "bust" in v_lower:
                        view_dict["portrait"] = {"name": v.name, "url": v_url, "label": "Chân dung (Portrait)"}
                    elif "front" in v_lower:
                        view_dict["front"] = {"name": v.name, "url": v_url, "label": "Chính diện (Front)"}
                    elif "profile" in v_lower or "side" in v_lower:
                        view_dict["profile"] = {"name": v.name, "url": v_url, "label": "Góc nghiêng (Profile)"}
                    elif "back" in v_lower:
                        view_dict["back"] = {"name": v.name, "url": v_url, "label": "Sau lưng (Back)"}

                char_obj = {
                    "id": char_id,
                    "name": meta["name"],
                    "role": meta["role"],
                    "group": group_name,
                    "group_label": group_label,
                    "avatar_url": primary_url,
                    "master_url": f"/assets/characters/{group_name}/{char_id}/{master_img.name}" if master_img else primary_url,
                    "turnaround_url": f"/assets/characters/{group_name}/{char_id}/{turnaround_any.name}" if turnaround_any else None,
                    "views": view_dict,
                    "has_turnaround": bool(turnaround_any),
                    "views_count": len(view_dict),
                    "all_images": [
                        {"name": p.name, "url": f"/assets/characters/{group_name}/{char_id}/{p.name}", "is_720p": "720p" in p.name}
                        for p in pngs
                    ] + [
                        {"name": f"View: {v.name}", "url": f"/assets/characters/{group_name}/{char_id}/views/{v.name}", "is_view": True}
                        for v in view_pngs
                    ]
                }
                group_characters.append(char_obj)
                characters_list.append(char_obj)

                # Nạp vào legacy_items để tương thích ngược 100% với giao diện cũ
                if primary_avatar:
                    legacy_items.append({
                        "name": f"{meta['name']} ({char_id})",
                        "stem": char_id,
                        "url": primary_url,
                        "path": str(primary_avatar.resolve()),
                        "is_720p": ("720p" in primary_avatar.name),
                        "size_mb": round(primary_avatar.stat().st_size / (1024 * 1024), 2),
                        "character_data": char_obj
                    })

            groups[group_name] = {
                "label": group_label,
                "items": legacy_items,
                "characters": group_characters
            }

    # Backward compatibility: nếu caller mong muốn data.groups là Dict[group, items]
    compat_groups = {k: v["items"] for k, v in groups.items()}

    return {
        "groups": compat_groups,
        "rich_groups": groups,
        "all_characters": characters_list,
        "total_characters": len(characters_list)
    }

@app.get("/api/prompts")
async def get_prompts():
    if PROMPTS_FILE.exists():
        try:
            return json.loads(PROMPTS_FILE.read_text(encoding="utf-8"))
        except Exception as e:
            return {"error": str(e)}
    return {}

@app.get("/api/banana-prompts")
async def get_banana_prompts():
    if BANANA_PROMPTS_FILE.exists():
        try:
            return json.loads(BANANA_PROMPTS_FILE.read_text(encoding="utf-8"))
        except Exception as e:
            return {"error": str(e)}
    return {}

@app.get("/api/shots")
async def list_shots_api(
    scene: Optional[str] = Query(None, description="Lọc theo Scene ID (vd: ep01_scene02)"),
    episode: Optional[str] = Query(None, description="Lọc theo Episode ID (vd: ep01)")
):
    shots = {}
    if PROMPTS_FILE.exists():
        try:
            data = json.loads(PROMPTS_FILE.read_text(encoding="utf-8"))
            shots = data.get("motion_prompts", {})
        except Exception as e:
            return {"error": str(e), "shots": {}}
    
    if scene:
        sc_clean = scene.lower().strip()
        shots = {k: v for k, v in shots.items() if sc_clean in k.lower()}
    elif episode:
        ep_clean = episode.lower().strip()
        shots = {k: v for k, v in shots.items() if k.lower().startswith(ep_clean)}

    return {"shots": shots}

@app.get("/api/scenes")
async def list_scenes_api(
    episode: Optional[str] = Query(None, description="Lọc theo Episode ID (vd: ep01)")
):
    lib = build_library_data()
    raw_scenes = lib.get("raw_scenes", [])
    if episode:
        ep_clean = episode.lower().strip()
        raw_scenes = [sc for sc in raw_scenes if sc.get("scene_key", "").startswith(ep_clean)]
    return {"scenes": raw_scenes}

@app.get("/api/matrix/ep01")
async def get_matrix_ep01():
    """Cung cấp ma trận 188 shots EP01 chia theo 15 phân cảnh chuẩn kịch bản Hollywood."""
    lib = build_library_data()
    raw_files = lib.get("raw_files", [])
    
    motion_prompts = {}
    if PROMPTS_FILE.exists():
        try:
            p_data = json.loads(PROMPTS_FILE.read_text(encoding="utf-8"))
            motion_prompts = p_data.get("motion_prompts", {})
        except Exception:
            pass

    banana_prompts = {}
    if BANANA_PROMPTS_FILE.exists():
        try:
            banana_prompts = json.loads(BANANA_PROMPTS_FILE.read_text(encoding="utf-8"))
        except Exception:
            pass

    scene_defs = [
        {"id": "ep01_intro", "title": "Intro Đầu Phim: Dải Lụa & Biểu Trưng Thập Ngũ Niên", "count": 2},
        {"id": "ep01_scene00", "title": "Phân Đoạn Dẫn Nhập: Prologue (1980s -> Gia Tĩnh)", "count": 4},
        {"id": "ep01_scene01", "title": "Cảnh 01: Bức Họa Mùa Xuân & Gia Phong Họ Vương", "count": 12},
        {"id": "ep01_scene02", "title": "Cảnh 02: Gia Trang Họ Vương & Chị Em Thúy Kiều", "count": 15},
        {"id": "ep01_scene03", "title": "Cảnh 03: Du Xuân Tiết Thanh Minh & Lễ Hội", "count": 15},
        {"id": "ep01_scene04", "title": "Cảnh 04: Không Khí Hội Xuân Tảo Mộ", "count": 12},
        {"id": "ep01_scene05", "title": "Cảnh 05: Tiết Thanh Minh - Mộ Đạm Tiên", "count": 27},
        {"id": "ep01_scene06", "title": "Cảnh 06: Chiều Tà & Vương Quan Thuật Chuyện", "count": 14},
        {"id": "ep01_scene07", "title": "Cảnh 07: Kiều Khóc Đạm Tiên & Cảm Hoài", "count": 8},
        {"id": "ep01_scene08", "title": "Cảnh 08: Kim Trọng Xuất Hiện", "count": 8},
        {"id": "ep01_scene09", "title": "Cảnh 09: Kỳ Ngộ Kim - Kiều", "count": 6},
        {"id": "ep01_scene10", "title": "Cảnh 10: Kim Trọng Nhặt Trâm & Tương Tư", "count": 14},
        {"id": "ep01_scene11", "title": "Cảnh 11: Kim Trọng Thuê Trọ Cạnh Vương Gia", "count": 10},
        {"id": "ep01_scene12", "title": "Cảnh 12: Bức Tường Hoa & Gặp Gỡ", "count": 8},
        {"id": "ep01_scene13", "title": "Cảnh 13: Thề Nguyện Dưới Trăng", "count": 12},
        {"id": "ep01_scene14", "title": "Cảnh 14: Đêm Trao Duyên", "count": 12},
        {"id": "ep01_scene15", "title": "Cảnh 15: Gia Biến Bắt Đầu", "count": 6}
    ]

    # Dynamic Shot Counter & Discovery: Tự động đếm theo số shot thực tế đang có
    # Không giới hạn cứng bởi count ban đầu nếu thư mục video, prompt hoặc keyframe có thêm shot mới
    known_scene_ids = {sc["id"] for sc in scene_defs}
    extra_scenes: Dict[str, Dict[str, Any]] = {}

    for sc in scene_defs:
        sc_id = sc["id"]
        base_count = sc["count"]
        found_nums = set()

        # 1. Quét từ raw_files
        for rf in raw_files:
            rf_name = rf.get("name", "").lower()
            if sc_id == "ep01_intro":
                m = re.search(r"ep01_intro_shot0?(\d+)", rf_name)
                if m: found_nums.add(int(m.group(1)))
            elif sc_id == "ep01_scene00":
                m = re.search(r"prologue_shot0?(\d+)", rf_name) or re.search(r"ep01_scene00_shot0?(\d+)", rf_name)
                if m: found_nums.add(int(m.group(1)))
            else:
                m = re.search(rf"{sc_id}_shot0?(\d+)", rf_name)
                if m: found_nums.add(int(m.group(1)))

        # 2. Quét từ motion_prompts
        for pk in motion_prompts.keys():
            pk_lower = pk.lower()
            if sc_id == "ep01_intro":
                m = re.search(r"ep01_intro_shot0?(\d+)", pk_lower)
                if m: found_nums.add(int(m.group(1)))
            elif sc_id == "ep01_scene00":
                m = re.search(r"prologue_shot_?0?(\d+)", pk_lower) or re.search(r"ep01_scene00_shot0?(\d+)", pk_lower)
                if m: found_nums.add(int(m.group(1)))
            else:
                m = re.search(rf"{sc_id}_shot0?(\d+)", pk_lower)
                if m: found_nums.add(int(m.group(1)))

        # 3. Quét từ KEYFRAMES_DIR
        if KEYFRAMES_DIR.exists():
            for kf_dir in KEYFRAMES_DIR.glob(f"{sc_id}_shot*"):
                m = re.search(r"shot0?(\d+)", kf_dir.name.lower())
                if m: found_nums.add(int(m.group(1)))

        max_found = max(found_nums) if found_nums else 0
        sc["count"] = max(base_count, max_found)

    # 4. Tự động phát hiện phân cảnh mới nếu có trong raw_files
    for rf in raw_files:
        m = re.search(r"(ep01_scene(\d+))_shot0?(\d+)", rf.get("name", "").lower())
        if m:
            full_sc_id, sc_idx, sh_idx = m.group(1), int(m.group(2)), int(m.group(3))
            if full_sc_id not in known_scene_ids:
                extra_scenes.setdefault(full_sc_id, {"id": full_sc_id, "title": f"Cảnh {sc_idx:02d}", "max_shot": 0})
                extra_scenes[full_sc_id]["max_shot"] = max(extra_scenes[full_sc_id]["max_shot"], sh_idx)

    for ex_id, ex_info in sorted(extra_scenes.items()):
        scene_defs.append({
            "id": ex_id,
            "title": ex_info["title"],
            "count": max(1, ex_info["max_shot"])
        })

    scenes = []
    total_rendered = 0
    total_shots = sum(sc["count"] for sc in scene_defs)

    prologue_prompt_keys = {
        1: "prologue_shot_1_lang_que_198x",
        2: "prologue_shot_2_ngam_kieu",
        3: "prologue_shot_3_xuyen_khong",
        4: "prologue_shot_4_gia_tinh_trieu_minh"
    }

    for sc in scene_defs:
        sc_id = sc["id"]
        sc_shots = []
        for i in range(1, sc["count"] + 1):
            shot_id = f"{sc_id}_shot{i:02d}"
            b_info = banana_prompts.get(shot_id) or banana_prompts.get("ep01_start_frames", {}).get(shot_id, {})

            if sc_id == "ep01_intro":
                p_key = f"ep01_intro_shot0{i}"
                p_info = motion_prompts.get(shot_id) or motion_prompts.get(p_key) or {}
                matches = [f for f in raw_files if f.get("name", "").startswith(f"ep01_intro_shot{i:02d}") or f.get("name", "").startswith(f"ep01_intro_shot0{i}")]
                matches.sort(key=lambda x: (x.get("version", 1), x.get("mtime", 0)), reverse=True)
                match = matches[0] if matches else None
                kf_shot_dir = KEYFRAMES_DIR / f"ep01_intro_shot{i:02d}"
                if (kf_shot_dir / "clean_frame_239.jpg").exists():
                    start_frame_path = f"/assets/keyframes/ep01_intro_shot{i:02d}/clean_frame_239.jpg"
                elif (kf_shot_dir / "start_frame_720p.png").exists():
                    start_frame_path = f"/assets/keyframes/ep01_intro_shot{i:02d}/start_frame_720p.png"
                else:
                    start_frame_path = ""
                target_video_file = f"ep01_intro_shot{i:02d}_10s_master.mp4"
            elif sc_id == "ep01_scene00":
                p_key = prologue_prompt_keys.get(i, "")
                p_info = motion_prompts.get(shot_id) or motion_prompts.get(p_key) or {}
                matches = [f for f in raw_files if f.get("name", "").startswith(f"prologue_shot{i:02d}") or f.get("name", "").startswith(shot_id)]
                matches.sort(key=lambda x: (x.get("version", 1), x.get("mtime", 0)), reverse=True)
                match = matches[0] if matches else None
                kf_shot_dir = KEYFRAMES_DIR / f"prologue_shot{i:02d}"
                start_frame_path = f"/assets/keyframes/prologue_shot{i:02d}/clean_frame_239.jpg" if (kf_shot_dir / "clean_frame_239.jpg").exists() else ""
                target_video_file = f"prologue_shot{i:02d}_10s_v1.mp4"
            else:
                p_info = motion_prompts.get(shot_id, {})
                matches = [f for f in raw_files if f.get("name", "").startswith(shot_id)]
                matches.sort(key=lambda x: (x.get("version", 1), x.get("mtime", 0)), reverse=True)
                match = matches[0] if matches else None
                kf_shot_dir = KEYFRAMES_DIR / shot_id
                start_frame_path = f"/assets/keyframes/{shot_id}/clean_frame_239.jpg" if (kf_shot_dir / "clean_frame_239.jpg").exists() else ""
                target_video_file = f"{shot_id}_10s_v1.mp4"

            is_rendered = bool(match)
            if is_rendered:
                total_rendered += 1

            motion_str = p_info.get("motion_prompt", "") if isinstance(p_info, dict) else str(p_info)
            has_lip_guard = "KHÓA KHẨU HÌNH" in motion_str or "LIPS CLOSED" in motion_str

            # Critic Gate Evaluation status
            verdict_file = CRITIC_QUEUE_DIR / f"{shot_id}_verdict.json"
            critic_info = None
            if verdict_file.exists():
                try:
                    with open(verdict_file, "r", encoding="utf-8") as vf:
                        v_data = json.load(vf)
                    critic_info = {
                        "score": v_data.get("overall_score", 1.0),
                        "approved": v_data.get("approved", True),
                        "suggested_action": v_data.get("suggested_action", "KEEP"),
                        "detected_errors": v_data.get("shot_eval", {}).get("detected_errors", []),
                        "error_severities": v_data.get("shot_eval", {}).get("error_severities", {}),
                        "visual_defects": v_data.get("shot_eval", {}).get("visual_defects", []),
                        "evaluated_at": v_data.get("timestamp")
                    }
                except Exception:
                    pass

            # Head-Tail Chaining Info
            prev_shot_id = f"{sc_id}_shot{i-1:02d}" if i > 1 else None
            prev_kf_dir = KEYFRAMES_DIR / prev_shot_id if prev_shot_id else None
            has_prev_tail = False
            prev_tail_url = ""
            if prev_kf_dir and (prev_kf_dir / "clean_frame_239.jpg").exists():
                has_prev_tail = True
                prev_tail_url = f"/assets/keyframes/{prev_shot_id}/clean_frame_239.jpg"

            chaining_type = "HEAD_TAIL_CONTINUOUS" if (i > 1 and has_prev_tail) else ("TURNAROUND_VIEW" if "shot" in shot_id else "BANANA_KEYFRAME")

            all_vers = [
                {
                    "name": m["name"],
                    "url": m["url"],
                    "version": m.get("version_str", "v1"),
                    "version_num": m.get("version", 1),
                    "timestamp_code": m.get("timestamp_code", ""),
                    "version_tag": m.get("version_tag", ""),
                    "is_latest": m.get("is_latest", False),
                    "size_mb": m.get("size_mb", 0),
                    "modified": m.get("modified", "")
                }
                for m in matches
            ]

            sc_shots.append({
                "shot_id": shot_id,
                "scene_id": sc_id,
                "shot_num": i,
                "title": p_info.get("scene_title", f"Shot {i:02d}") if isinstance(p_info, dict) else f"Shot {i:02d}",
                "duration_sec": 10,
                "target_video_file": target_video_file,
                "rendered": is_rendered,
                "video_url": match.get("url") if match else None,
                "video_name": match.get("name") if match else None,
                "version": match.get("version_str", "v1") if match else "v1",
                "version_num": match.get("version", 1) if match else 1,
                "timestamp_code": match.get("timestamp_code", "") if match else "",
                "version_tag": match.get("version_tag", "") if match else "",
                "is_latest": match.get("is_latest", True) if match else True,
                "all_versions": all_vers,
                "versions_count": len(matches),
                "start_frame_url": start_frame_path,
                "has_lip_sync_guard": has_lip_guard,
                "reverse_motion": bool(p_info.get("reverse_motion", False)) if isinstance(p_info, dict) else False,
                "motion_prompt": motion_str,
                "camera_motion": p_info.get("camera_motion", "Medium Close-Up (Cinematic)") if isinstance(p_info, dict) else "Medium Close-Up",
                "character_anchor": b_info.get("character_anchor", "") if isinstance(b_info, dict) else "",
                "environment_anchor": b_info.get("environment_anchor", "") if isinstance(b_info, dict) else "",
                "banana_prompt": b_info if isinstance(b_info, dict) else {},
                "audio_guard": "Tuyệt đối KHÔNG sinh nhạc nền (no music/BGM), không âm thanh điện tử. Chỉ sinh foley, ambience và thoại chân thực.",
                "critic": critic_info,
                "chaining_type": chaining_type,
                "prev_tail_frame_url": prev_tail_url if has_prev_tail else None
            })

        rendered_in_sc = sum(1 for s in sc_shots if s["rendered"])
        scenes.append({
            "scene_id": sc_id,
            "title": sc["title"],
            "shots_total": sc["count"],
            "shots_rendered": rendered_in_sc,
            "shots": sc_shots
        })

    total_pending = max(0, total_shots - total_rendered)
    dur_min = (total_shots * 10) // 60
    dur_sec = (total_shots * 10) % 60
    dur_str = f"{dur_min} Phút {dur_sec} Giây" if dur_sec else f"{dur_min} Phút"

    return {
        "episode_id": "ep01",
        "total_shots": total_shots,
        "total_rendered": total_rendered,
        "total_pending": total_pending,
        "scenes_count": len(scenes),
        "duration_sec": total_shots * 10,
        "duration_str": dur_str,
        "scenes": scenes
    }

@app.get("/api/episodes")
async def list_episodes():
    episodes = []
    if FILMMAKER_DIR.exists():
        for ep in sorted(FILMMAKER_DIR.glob("TAP_*.md")):
            episodes.append({
                "filename": ep.name,
                "title": ep.stem.replace("_", " "),
                "size_kb": round(ep.stat().st_size / 1024, 1)
            })
    return {"episodes": episodes}

@app.get("/api/episode/{filename}")
async def get_episode_content(filename: str):
    ep_path = (FILMMAKER_DIR / filename).resolve()
    film_dir_resolved = FILMMAKER_DIR.resolve()
    if ep_path.is_relative_to(film_dir_resolved) and ep_path.exists() and ep_path.is_file() and ep_path.suffix.lower() == ".md":
        return {"filename": filename, "content": ep_path.read_text(encoding="utf-8")}
    raise HTTPException(status_code=404, detail="Episode not found")

@app.get("/api/status")
async def get_status():
    res = dict(gen_state)
    res["library_fingerprint"] = get_library_fingerprint()
    return res

def run_agent_browser_generation(prompt: str, scene_title: str):
    global gen_state, _library_cache
    try:
        gen_state["status"] = "generating"
        gen_state["current_prompt"] = prompt
        gen_state["progress_text"] = "Đang gửi prompt đến Muse.ai..."
        log_msg(f"Bắt đầu tạo video: {scene_title or 'Custom'}")

        subprocess.run(["agent-browser", "--session", "muse", "fill", "@e14", prompt], capture_output=True, text=True, cwd=str(BASE_DIR))
        subprocess.run(["agent-browser", "--session", "muse", "click", "@e51"], capture_output=True, text=True, cwd=str(BASE_DIR))
        log_msg("Đã gửi prompt lên chat Muse.ai")

        max_attempts = 30 # 30 * 4s = 120s
        video_downloaded = False
        initial_downloads = set(DOWNLOADS_DIR.glob("*.mp4"))

        for i in range(max_attempts):
            time.sleep(4)
            snap_res = subprocess.run(["agent-browser", "--session", "muse", "snapshot", "-i"], capture_output=True, text=True, cwd=str(BASE_DIR))
            output = snap_res.stdout

            if "Đang tạo video" in output:
                gen_state["progress_text"] = f"Muse đang render video... ({i*4}s)"
                log_msg(f"Render progress: {i*4}s")

            if "Tải video xuống" in output:
                gen_state["progress_text"] = "Render hoàn tất! Đang tải file về..."
                log_msg("Render hoàn tất! Bấm nút tải...")
                
                ref = "e79"
                for line in output.splitlines():
                    if "Tải video xuống" in line and "[ref=" in line:
                        ref = line.split("[ref=")[-1].split("]")[0]
                        break

                subprocess.run(["agent-browser", "--session", "muse", "click", f"@{ref}"], capture_output=True, text=True, cwd=str(BASE_DIR))
                time.sleep(3)

                current_downloads = set(DOWNLOADS_DIR.glob("*.mp4"))
                new_files = current_downloads - initial_downloads
                if not new_files:
                    recent = sorted(DOWNLOADS_DIR.glob("*.mp4"), key=lambda f: f.stat().st_mtime, reverse=True)
                    if recent and (time.time() - recent[0].stat().st_mtime < 120):
                        new_files = [recent[0]]

                if new_files:
                    target_source = list(new_files)[0]
                    safe_stem = "custom_generation"
                    if scene_title:
                        safe_stem = "".join(c if c.isalnum() else "_" for c in scene_title).strip("_").lower()
                    
                    clean_name = resolve_next_version(VIDEOS_DIR, safe_stem)
                    target_dest = VIDEOS_DIR / clean_name
                    shutil.copy2(target_source, target_dest)
                    
                    # Extract tail frame if OpenCV is available
                    try:
                        import cv2
                        cap = cv2.VideoCapture(str(target_dest))
                        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
                        cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, total_frames - 1))
                        ret, tail_frame = cap.read()
                        cap.release()
                        if ret and tail_frame is not None:
                            kf_dir = KEYFRAMES_DIR / safe_stem
                            kf_dir.mkdir(parents=True, exist_ok=True)
                            cv2.imwrite(str(kf_dir / "clean_frame_239.jpg"), tail_frame)
                    except Exception:
                        pass

                    gen_state["last_video"] = clean_name
                    gen_state["status"] = "completed"
                    gen_state["progress_text"] = f"Đã tải thành công: {clean_name}"
                    log_msg(f"Hoàn thành: Đã lưu {clean_name}")
                    video_downloaded = True
                    _library_cache = {}
                    break

        if not video_downloaded:
            gen_state["status"] = "error"
            gen_state["progress_text"] = "Timeout chờ render từ Muse.ai"
            log_msg("Timeout: Không phát hiện file download sau 120s")

    except Exception as e:
        gen_state["status"] = "error"
        gen_state["progress_text"] = f"Lỗi: {str(e)}"
        log_msg(f"Exception: {str(e)}")

def run_unified_generation(req: GenerateRequest):
    """Điều phối sinh video: ưu tiên orchestrator nếu có shot_id, fallback pipeline/gateway nếu custom prompt."""
    global gen_state, _library_cache
    shot_id = req.shot_id
    prompt = req.prompt
    scene_id = req.scene_id
    title = req.title or scene_id or (shot_id if shot_id else "Custom")
    engine = req.engine or "auto"

    gen_state["status"] = "generating"
    gen_state["current_prompt"] = prompt or f"[Shot: {shot_id}]"
    gen_state["progress_text"] = f"Đang khởi động tiến trình render (Engine: {engine})..."
    log_msg(f"Bắt đầu render video: {title} | Engine: {engine}")

    if shot_id:
        try:
            from production_orchestrator import render_single_shot, find_rendered_video
            gen_state["progress_text"] = f"Đang render shot {shot_id} qua Production Orchestrator (Engine: {engine})..."
            log_msg(f"Bắt đầu render shot: {shot_id} (Engine: {engine})")

            success = render_single_shot(
                shot_id,
                input_frame=req.input_frame,
                prompt_override=prompt,
                session_name=req.session_name or "muse",
                reverse_motion=req.reverse_motion,
                engine=engine
            )
            if success:
                v_path = find_rendered_video(shot_id)
                v_name = v_path.name if v_path else f"{shot_id}_10s.mp4"
                gen_state["status"] = "completed"
                gen_state["last_video"] = v_name
                gen_state["progress_text"] = f"Đã render thành công: {v_name}"
                log_msg(f"Hoàn thành shot {shot_id}: {v_name}")
                _library_cache = {}
                return
            else:
                gen_state["status"] = "error"
                gen_state["progress_text"] = f"Lỗi render shot {shot_id}"
                log_msg(f"Lỗi render shot {shot_id}")
                return
        except Exception as e:
            gen_state["status"] = "error"
            gen_state["progress_text"] = f"Lỗi: {str(e)}"
            log_msg(f"Exception render shot {shot_id}: {str(e)}")
            return

    if prompt:
        safe_stem = "custom_generation"
        if title:
            safe_stem = "".join(c if c.isalnum() else "_" for c in title).strip("_").lower()
            if not safe_stem:
                safe_stem = f"custom_{int(time.time())}"
        else:
            safe_stem = f"custom_{int(time.time())}"

        # 1. Thử qua run_shot_pipeline (tự động phân luồng Muse2API Gateway hoặc Gradio LTX)
        try:
            from run_shot import run_shot_pipeline
            gen_state["progress_text"] = f"Đang gửi tác vụ render qua Pipeline (Engine: {engine})..."
            log_msg(f"Render custom prompt [{safe_stem}] qua engine: {engine}")

            ok, tail_path = run_shot_pipeline(
                shot_id=safe_stem,
                prompt=prompt,
                input_frame=req.input_frame,
                reverse_motion=req.reverse_motion or False,
                engine=engine
            )
            if ok:
                recent_vids = sorted(VIDEOS_DIR.glob(f"{safe_stem}*.mp4"), key=lambda f: f.stat().st_mtime, reverse=True)
                v_name = recent_vids[0].name if recent_vids else f"{safe_stem}_10s.mp4"
                gen_state["status"] = "completed"
                gen_state["last_video"] = v_name
                gen_state["progress_text"] = f"Đã render thành công ({engine}): {v_name}"
                log_msg(f"Hoàn thành custom prompt: {v_name}")
                _library_cache = {}
                return
            else:
                log_msg(f"Pipeline không hoàn tất cho {safe_stem}, kiểm tra fallback...")
        except Exception as e:
            log_msg(f"Exception trong run_shot_pipeline: {e}")

        # 2. Fallback sang browser automation nếu pipeline gặp sự cố
        run_agent_browser_generation(prompt, title)

def run_scene_concat(scene_id: str, output_path: Optional[str] = None, crossfade_dur: float = 1.0):
    """Thực thi ghép nối Master Scene qua Production Orchestrator."""
    global gen_state, _library_cache
    try:
        gen_state["status"] = "concatenating"
        gen_state["progress_text"] = f"Đang ghép nối các shot cho cảnh {scene_id}..."
        log_msg(f"Bắt đầu ghép nối Master cảnh: {scene_id}")

        try:
            from production_orchestrator import concat_scene_shots
            result = concat_scene_shots(scene_id, output_path=output_path, crossfade_dur=crossfade_dur)
        except ImportError:
            result = None
            log_msg("Lỗi: Không tìm thấy module production_orchestrator")

        if result and Path(result).exists():
            gen_state["status"] = "completed"
            gen_state["last_video"] = Path(result).name
            gen_state["progress_text"] = f"Ghép nối thành công cảnh {scene_id}: {Path(result).name}"
            log_msg(f"Hoàn thành ghép nối cảnh {scene_id} -> {Path(result).name}")
            _library_cache = {}
        else:
            gen_state["status"] = "error"
            gen_state["progress_text"] = f"Ghép nối cảnh {scene_id} thất bại (thiếu shot hoặc lỗi FFmpeg)"
            log_msg(f"Thất bại khi ghép nối cảnh: {scene_id}")
    except Exception as e:
        gen_state["status"] = "error"
        gen_state["progress_text"] = f"Lỗi ghép nối: {str(e)}"
        log_msg(f"Exception khi ghép nối {scene_id}: {str(e)}")

@app.post("/api/generate")
async def trigger_generate(req: GenerateRequest, bg_tasks: BackgroundTasks):
    global gen_state
    if gen_state["status"] in ("generating", "concatenating"):
        return JSONResponse({"status": "busy", "message": "Một tác vụ sinh video hoặc ghép nối đang chạy"}, status_code=409)
    
    bg_tasks.add_task(run_unified_generation, req)
    return {"status": "accepted", "message": "Đã bắt đầu tác vụ sinh video trong nền"}

@app.post("/api/concat")
async def trigger_concat(req: ConcatRequest, bg_tasks: BackgroundTasks):
    global gen_state
    if gen_state["status"] in ("generating", "concatenating"):
        return JSONResponse({"status": "busy", "message": "Một tác vụ khác đang chạy"}, status_code=409)
    
    if not req.scene_id or not req.scene_id.strip():
        raise HTTPException(status_code=422, detail="scene_id is required")

    bg_tasks.add_task(run_scene_concat, req.scene_id.strip(), req.output_path, req.crossfade_dur or 1.0)
    return {"status": "accepted", "message": f"Đã bắt đầu tác vụ ghép nối cảnh {req.scene_id} trong nền"}

def run_scene_batch(scene_id: str, engine: str = "auto", force_rerun: bool = False, auto_concat: bool = True):
    """Điều phối render tự động toàn bộ phân cảnh theo chuỗi Head-Tail liên hoàn."""
    global gen_state, _library_cache
    try:
        gen_state["status"] = "generating"
        gen_state["progress_text"] = f"Bắt đầu quy trình Batch Render Scene {scene_id} (Engine: {engine})..."
        log_msg(f"Bắt đầu Batch Render Scene: {scene_id} | Engine: {engine} | Auto Concat: {auto_concat}")

        try:
            from production_orchestrator import batch_render_scene
            success = batch_render_scene(
                scene_id=scene_id,
                force=force_rerun,
                auto_concat=auto_concat,
                engine=engine,
                enable_critic=True
            )
            if success:
                gen_state["status"] = "completed"
                gen_state["progress_text"] = f"Hoàn tất Batch Render Cảnh {scene_id}!"
                log_msg(f"Hoàn thành xuất sắc Batch Render Scene {scene_id}")
                _library_cache = {}
            else:
                gen_state["status"] = "error"
                gen_state["progress_text"] = f"Batch Render Cảnh {scene_id} gặp sự cố hoặc gián đoạn"
                log_msg(f"Lỗi hoặc dừng trong Batch Render Scene {scene_id}")
        except ImportError as e:
            gen_state["status"] = "error"
            gen_state["progress_text"] = f"Không nạp được production_orchestrator: {e}"
            log_msg(f"ImportError batch_render_scene: {e}")
    except Exception as e:
        gen_state["status"] = "error"
        gen_state["progress_text"] = f"Lỗi Batch Render: {str(e)}"
        log_msg(f"Exception batch_render_scene: {str(e)}")

@app.post("/api/scene/batch_render")
async def trigger_scene_batch_render(req: BatchSceneRequest, bg_tasks: BackgroundTasks):
    """Kích hoạt render tự động toàn bộ các shot trong phân cảnh theo chuỗi Head-Tail Chaining."""
    global gen_state
    if gen_state["status"] in ("generating", "concatenating"):
        return JSONResponse({"status": "busy", "message": "Một tác vụ sinh video hoặc ghép nối đang chạy"}, status_code=409)

    if not req.scene_id or not req.scene_id.strip():
        raise HTTPException(status_code=422, detail="scene_id is required")

    bg_tasks.add_task(
        run_scene_batch,
        req.scene_id.strip(),
        req.engine or "auto",
        req.force_rerun or False,
        req.auto_concat if req.auto_concat is not None else True
    )
    return {"status": "accepted", "message": f"Đã bắt đầu tác vụ Batch Render cho cảnh {req.scene_id} trong nền"}

@app.post("/api/critic/evaluate")
async def trigger_critic_eval(req: CriticEvalRequest):
    """Kích hoạt thẩm định chất lượng shot qua Antigravity Critic Gate (14 Lỗi AI)."""
    if not HAS_CRITIC_GATE or evaluate_shot_gate is None:
        raise HTTPException(status_code=503, detail="Module Antigravity Critic Gate không khả dụng")

    shot_id = req.shot_id.strip()
    video_path_str = req.video_path
    
    # Tự động dò tìm file video nếu chưa truyền
    if not video_path_str:
        v_matches = list(VIDEOS_DIR.glob(f"*{shot_id}*.mp4"))
        if not v_matches:
            v_matches = list(VIDEOS_DIR.glob(f"{shot_id}*.mp4"))
        if v_matches:
            v_matches.sort(key=lambda f: f.stat().st_mtime, reverse=True)
            video_path_str = str(v_matches[0].resolve())
        else:
            raise HTTPException(status_code=404, detail=f"Không tìm thấy video nào tương ứng với shot {shot_id}")

    try:
        verdict = evaluate_shot_gate(
            shot_id=shot_id,
            video_path=video_path_str,
            expected_character=req.expected_character or "",
            prompt=req.prompt or "",
            force_engine=req.force_engine or "auto"
        )
        
        # Lưu kết quả verdict vào CRITIC_QUEUE_DIR
        verdict_data = verdict.model_dump()
        verdict_data["evaluated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        verdict_file = CRITIC_QUEUE_DIR / f"{shot_id}_verdict.json"
        with open(verdict_file, "w", encoding="utf-8") as vf:
            json.dump(verdict_data, vf, ensure_ascii=False, indent=2)

        return {
            "status": "success",
            "shot_id": shot_id,
            "verdict": verdict_data,
            "catalog": FLOWKIT_ERROR_CATALOG,
            "tiers": FLOWKIT_ERROR_TIERS
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Lỗi thẩm định Critic Gate: {str(e)}")

@app.get("/api/critic/shot/{shot_id}")
async def get_critic_shot_verdict(shot_id: str):
    """Lấy chi tiết phán quyết và bảng phân tích 14 lỗi của một shot."""
    verdict_file = CRITIC_QUEUE_DIR / f"{shot_id}_verdict.json"
    if verdict_file.exists():
        try:
            with open(verdict_file, "r", encoding="utf-8") as vf:
                v_data = json.load(vf)
            return {
                "evaluated": True,
                "shot_id": shot_id,
                "verdict": v_data,
                "catalog": FLOWKIT_ERROR_CATALOG,
                "tiers": FLOWKIT_ERROR_TIERS
            }
        except Exception as e:
            return {"evaluated": False, "error": str(e), "catalog": FLOWKIT_ERROR_CATALOG, "tiers": FLOWKIT_ERROR_TIERS}
    
    return {
        "evaluated": False,
        "shot_id": shot_id,
        "message": f"Shot {shot_id} chưa được thẩm định qua Critic Gate",
        "catalog": FLOWKIT_ERROR_CATALOG,
        "tiers": FLOWKIT_ERROR_TIERS
    }

@app.get("/api/critic/summary")
async def get_critic_summary():
    """Báo cáo tổng hợp chất lượng toàn bộ dự án từ Antigravity Critic Gate."""
    verdict_files = list(CRITIC_QUEUE_DIR.glob("*_verdict.json"))
    total_evaluated = len(verdict_files)
    approved_count = 0
    retake_count = 0
    total_score = 0.0
    error_frequencies = {}

    for vf in verdict_files:
        try:
            with open(vf, "r", encoding="utf-8") as f:
                d = json.load(f)
            sc = float(d.get("overall_score", 0.0))
            total_score += sc
            if d.get("approved", False):
                approved_count += 1
            else:
                retake_count += 1
            
            errs = d.get("shot_eval", {}).get("detected_errors", [])
            for e in errs:
                error_frequencies[e] = error_frequencies.get(e, 0) + 1
        except Exception:
            pass

    avg_score = round(total_score / total_evaluated, 3) if total_evaluated > 0 else 0.0
    pass_rate = round((approved_count / total_evaluated) * 100, 1) if total_evaluated > 0 else 0.0

    return {
        "total_evaluated": total_evaluated,
        "approved_count": approved_count,
        "retake_count": retake_count,
        "pass_rate_pct": pass_rate,
        "average_score": avg_score,
        "error_frequencies": error_frequencies,
        "catalog": FLOWKIT_ERROR_CATALOG,
        "tiers": FLOWKIT_ERROR_TIERS
    }

if __name__ == "__main__":
    uvicorn.run(app, host=DEFAULT_HOST, port=DEFAULT_PORT, log_level="info")
