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

import uvicorn
from fastapi import FastAPI, BackgroundTasks, HTTPException, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
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.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"
FILMMAKER_DIR = BASE_DIR / "FilmMaker"
TEMPLATES_DIR = BASE_DIR / "web_review" / "templates"
DOWNLOADS_DIR = Path(os.path.expanduser("~")) / "Downloads"

# Ensure pipeline directory is 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)

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")

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

if sys.platform == "win32":
    try:
        sys.stdout.reconfigure(encoding="utf-8")
        sys.stderr.reconfigure(encoding="utf-8")
    except Exception:
        pass

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

    @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

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

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

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() -> Dict[str, Any]:
    global _library_cache, _cache_time
    now = time.time()
    if _library_cache and (now - _cache_time < 3.0):
        return _library_cache

    all_exports = list(EXPORTS_DIR.rglob("*.mp4"))
    all_raw = list(VIDEOS_DIR.glob("*.mp4"))

    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:
        stat = p.stat()
        name = p.name
        lower = name.lower()
        rel_to_exports = p.relative_to(EXPORTS_DIR).as_posix()
        size_mb = round(stat.st_size / (1024 * 1024), 2)
        mtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime))
        url = f"/assets/exports/{rel_to_exports}"
        abs_path = str(p.resolve())
        v_m = re.search(r"_v(\d+)\.mp4$", lower)
        version_num = int(v_m.group(1)) if v_m else 1

        # 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
            title = name.replace(".mp4", "").replace("_9x16", "").replace("short_", "Short ").replace("_", " ").title()
            
            # Enrich title based on ep
            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, title)}"

            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": version_num
            })
            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 = "Grand Roadshow Master"
            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 = "Roadshow Cut"

            if "ep01_to_ep06" in lower:
                title = "ĐẠI BẢN MASTER TOÀN BỘ 6 TẬP (FULL FEATURE CUT)"
                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 = "Full Feature Master (17m40s)"
            elif "ep01_to_ep05" in lower:
                title = "Roadshow Tuyển Tập: Tập 1 đến Tập 5"
                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 = "Roadshow Tập 1-5 (16m20s)"
            elif "ep01_to_ep04" in lower:
                title = "Roadshow Tuyển Tập: Tập 1 đến Tập 4"
                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 = "Roadshow Tập 1-4 (15m)"
            elif "ep01_ep02_ep03" in lower:
                title = "Roadshow Tuyển Tập: Tập 1 - 2 - 3"
                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 = "Roadshow Tập 1-3 (13m)"
            elif "ep01_ep02" in lower:
                title = "Roadshow Tuyển Tập: Tập 1 & Tập 2"
                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 = "Roadshow Tập 1-2 (10m40s)"

            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
            })
            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})"
            badge = "Continuous Cut"
            priority = 40

            if "intro_prologue_scenes01_02" in lower:
                title = "Liên Hoàn Intro + Prologue + Cảnh 1 & Cảnh 2"
                badge = "Liên Hoàn 160s"
                priority = 45
            elif "intro_prologue_scene01" in lower:
                title = "Liên Hoàn Intro + Prologue + Cảnh 1"
                badge = "Liên Hoàn 110s"
                priority = 45

            feature_masters.append({
                "name": name,
                "title": title,
                "description": f"Bản ghép nối liền mạch âm thanh & hình ảnh thời lượng {dur_str}.",
                "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
            })
            continue

        # Episode Grand Master (hỗ trợ cả grand_feature_master và grand_master_v*)
        if "grand_feature_master" in lower or "grand_master" 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)", 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")

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

            episode_masters.append({
                "name": name,
                "episode": f"ep0{ep_num}",
                "episode_num": ep_num,
                "title": f"{ep_name} — Bản Grand Master",
                "description": 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.",
                "url": url,
                "path": abs_path,
                "size_mb": size_mb,
                "modified": mtime,
                "aspect": "16:9",
                "duration_str": dur_str or f"{size_mb} MB",
                "badge": f"Master Tập {ep_num}",
                "category": "episode_master",
                "version": version_num
            })
            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} (Cinematic Master)",
                "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}",
                "category": "scene_master",
                "version": version_num
            })
            continue

    # Sort
    feature_masters.sort(key=lambda x: (x.get("priority", 0), x["size_mb"]), reverse=True)
    episode_masters.sort(key=lambda x: (x["episode_num"], -x["size_mb"]))
    scene_masters.sort(key=lambda x: (x["episode_num"], x["scene_num"]))
    shorts.sort(key=lambda x: x["episode_num"])

    # Raw shots in 04_Assets/videos
    raw_files_list = []
    for p in all_raw:
        stat = p.stat()
        name = p.name
        lower = name.lower()
        size_mb = round(stat.st_size / (1024 * 1024), 2)
        mtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime))
        url = f"/assets/videos/{name}"
        abs_path = str(p.resolve())
        v_m = re.search(r"_v(\d+)\.mp4$", lower)
        version_num = int(v_m.group(1)) if v_m else 1

        ep_m = re.search(r"ep0?(\d+)", lower)
        sc_m = re.search(r"scene0?(\d+)", lower)
        sh_m = re.search(r"shot0?(\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
        sh_num = int(sh_m.group(1)) if sh_m else 0

        scene_key = f"ep0{ep_num}_scene{sc_num:02d}"
        if scene_key not in raw_shots_by_scene:
            raw_shots_by_scene[scene_key] = []

        is_shot = "shot" in lower
        v_tag = f" v{version_num}" if v_m else ""
        item_title = f"Tập {ep_num} • Cảnh {sc_num:02d} • Shot {sh_num:02d}{v_tag} (10s)" if is_shot else name.replace(".mp4", "").replace("_", " ")

        item = {
            "name": name,
            "episode": f"ep0{ep_num}",
            "episode_num": ep_num,
            "scene": 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
        }
        raw_shots_by_scene[scene_key].append(item)
        raw_files_list.append(item)

    for sc in raw_shots_by_scene:
        raw_shots_by_scene[sc].sort(key=lambda x: (x["shot_num"], x["version"], x["name"]))

    # 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

        sorted_raw_scenes.append({
            "scene_key": sc_key,
            "episode_num": ep_num,
            "scene_num": sc_num,
            "title": f"Tập {ep_num} — Cảnh {sc_num:02d} ({len(items)} shots)",
            "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_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
    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.get("/api/videos")
async def list_videos(category: Optional[str] = None):
    lib = build_library_data()
    # Backward compatible flat list, but ordered by priority
    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"]}

@app.get("/api/characters")
async def list_characters():
    groups = {}
    if CHARACTERS_DIR.exists():
        for group_dir in sorted(CHARACTERS_DIR.iterdir()):
            if group_dir.is_dir():
                group_name = group_dir.name
                items = []
                for img in sorted(group_dir.glob("*.png")):
                    is_720p = "720p" in img.name
                    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)
                    })
                groups[group_name] = items
    return {"groups": groups}

@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/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/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():
    return gen_state

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'}")

        # Step 1: Fill prompt and submit via agent-browser safely without shell interpolation
        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")

        # Step 2: Poll status
        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 prompt nếu tùy chỉnh."""
    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")

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

            success = render_single_shot(
                shot_id,
                input_frame=req.input_frame,
                prompt_override=prompt
            )
            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:
        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"}

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