import os
import sys
import re
import time
import argparse
import subprocess
import shutil
import cv2
import numpy as np
from pathlib import Path
from typing import Optional, List, Dict, Tuple

# Đảm bảo UTF-8
if sys.platform == "win32":
    try:
        sys.stdout.reconfigure(encoding="utf-8")
        sys.stderr.reconfigure(encoding="utf-8")
    except Exception:
        pass

BASE_DIR = Path(__file__).resolve().parent.parent
PIPELINE_DIR = Path(__file__).resolve().parent
if str(PIPELINE_DIR) not in sys.path:
    sys.path.insert(0, str(PIPELINE_DIR))
ASSETS_DIR = BASE_DIR / "04_Assets"
VIDEOS_DIR = ASSETS_DIR / "videos"
KEYFRAMES_DIR = ASSETS_DIR / "keyframes"
DEFAULT_DOWNLOADS_DIR = Path.home() / "Downloads"

try:
    from muse_api_client import MuseApiClient, render_shot_via_api
    HAS_MUSE_API = True
except ImportError:
    try:
        from .muse_api_client import MuseApiClient, render_shot_via_api
        HAS_MUSE_API = True
    except Exception:
        HAS_MUSE_API = False
        MuseApiClient = None
        render_shot_via_api = None

def get_ffmpeg() -> str:
    exe = Path(sys.executable).parent / "ffmpeg.exe"
    if exe.exists():
        return str(exe)
    if shutil.which("ffmpeg"):
        return "ffmpeg"
    return "ffmpeg"

AUDIO_GUARD_CANONICAL = " Quy tắc âm thanh: Tuyệt đối KHÔNG sinh nhạc nền (no music/BGM), không âm thanh điện tử, không tạp âm rè nhiễu. Chỉ sinh âm thanh môi trường tự nhiên (foley, ambience) và thoại nhân vật chân thực."

def sanitize_prompt(prompt: str) -> str:
    """Thanh lọc từ khóa nhạy cảm kích hoạt bộ lọc kiểm duyệt Meta/Muse."""
    if not prompt:
        return prompt
    replacements = [
        (r'\b(nấm mồ|mồ mả|nghĩa địa|mộ địa|mộ phần|ngôi mộ|ngôi mả)\b', 'gò đất phủ cỏ úa xưa cũ'),
        (r'\b(âm khí|tử khí|âm khí nặng nề)\b', 'gió lạnh chiều tà se sắt hoang vu'),
        (r'\b(khóc người đời xưa|khóc than bạc mệnh)\b', 'nghiêng mình trầm tư đượm buồn'),
        (r'\b(rải vàng mã|tiền vàng mã|vàng mã)\b', 'thắp nén hương trầm phảng phất'),
        (r'\b(xương tàn|xương cốt|xác chết|chết chóc)\b', 'dấu tích xưa cũ'),
        (r'\b(bạc mệnh)\b', 'số phận long đong'),
    ]
    cleaned = prompt
    for pattern, repl in replacements:
        cleaned = re.sub(pattern, repl, cleaned, flags=re.IGNORECASE)
    return cleaned

def ensure_audio_guard(prompt: str) -> str:
    """Đảm bảo prompt tuân thủ chỉ thị Audio Guard cấm nhạc nền và được thanh lọc từ khóa nhạy cảm."""
    if not prompt:
        return AUDIO_GUARD_CANONICAL.strip()
    prompt = sanitize_prompt(prompt)
    if "Tuyệt đối KHÔNG sinh nhạc nền" not in prompt and "Strictly NO background music" not in prompt:
        return prompt.rstrip() + AUDIO_GUARD_CANONICAL
    return prompt

CLOSED_LIPS_CANONICAL = " [KHÓA KHẨU HÌNH BẮT BUỘC / LIPS CLOSED]: Đôi môi các nhân vật trong khung hình khép chặt tự nhiên (lips closed), không mấp máy."
CLOSED_LIPS_TRIGGERS = [
    "v.o.", "ngâm thơ", "thuyết minh", "dẫn chuyện", "ngoài khung", 
    "từ xa", "off-screen", "nghe tiếng", "tiếng gọi", "tiếng khóc từ xa", "tiếng đàn từ xa"
]

def ensure_closed_lips_guard(prompt: str, force: bool = False) -> str:
    """Đảm bảo prompt tuân thủ Closed Lips Guard khi có thoại từ xa, VO, ngâm thơ hoặc lời gọi ngoài khung hình."""
    if not prompt:
        return prompt
    prompt_lower = prompt.lower()
    has_guard = any(w in prompt_lower for w in ["khép chặt", "khép môi", "lips closed", "không mấp máy", "khóa khẩu hình"])
    if has_guard:
        return prompt
    if force or any(t in prompt_lower for t in CLOSED_LIPS_TRIGGERS):
        if "Âm thanh:" in prompt:
            parts = prompt.split("Âm thanh:", 1)
            return parts[0].rstrip() + CLOSED_LIPS_CANONICAL + " Âm thanh:" + parts[1]
        elif "Quy tắc âm thanh:" in prompt:
            parts = prompt.split("Quy tắc âm thanh:", 1)
            return parts[0].rstrip() + CLOSED_LIPS_CANONICAL + " Quy tắc âm thanh:" + parts[1]
        else:
            return prompt.rstrip() + CLOSED_LIPS_CANONICAL
    return prompt

CLOSE_KEYWORDS = [
    'cận cảnh', 'close-up', 'mcu', 'cu', 'ecu', 'trung cận', 'bàn tay', 
    'ánh mắt', 'khuôn mặt', 'gương mặt', 'giọt lệ', 'chén trà', 'ngón tay', 
    'tóc bay', 'đặc tả', 'portrait', 'nụ cười', 'micro-expression', 'previs'
]
WIDE_KEYWORDS = [
    'toàn cảnh', 'wide shot', 'extreme wide', 'aerial', 'đại cảnh', 
    'kinh thành', 'bầu trời', 'dòng sông', 'sông biếc', 'cảnh rộng', 
    'khung cảnh', 'phố thị', 'quân lính', 'hành động'
]

def classify_shot_engine(shot_id: str, shot_data: Optional[Dict] = None, prompt: Optional[str] = None) -> str:
    """
    Phân loại động công cụ render video cho Shot:
    - Cảnh toàn (Wide / Establishing / Action / Foley): Trả về 'muse'
    - Cận cảnh / Đặc tả / Chân dung / Previs: Trả về 'gradio_ltx'
    """
    if shot_data is None:
        try:
            from production_orchestrator import get_all_shots
            shots = get_all_shots()
            shot_data = shots.get(shot_id, {})
        except Exception:
            shot_data = {}
            
    if shot_data and shot_data.get("video_engine"):
        return shot_data["video_engine"].lower()
        
    title = shot_data.get("scene_title", "") if shot_data else ""
    camera = shot_data.get("camera_motion", "") if shot_data else ""
    motion = prompt or (shot_data.get("motion_prompt", "") if shot_data else "")
    combined = f"{shot_id} {title} {camera} {motion}".lower()
    
    is_wide = any(kw in combined for kw in WIDE_KEYWORDS)
    is_close = any(kw in combined for kw in CLOSE_KEYWORDS)
    
    if is_wide:
        return "muse"
    if is_close:
        return "gradio_ltx"
    return "muse"

def _execute_gradio_ltx(shot_id: str, prompt: str, input_frame: str, reverse_motion: bool = False) -> Tuple[bool, Optional[str]]:
    """Thực thi sinh video I2V qua Gradio LTX-Video API (Hugging Face ZeroGPU Free)."""
    print("      ⚡ Kích hoạt Gradio LTX-Video Engine (Hugging Face ZeroGPU Free)...")
    try:
        if str(BASE_DIR) not in sys.path:
            sys.path.insert(0, str(BASE_DIR))
        from gradio_version.hf_client import HuggingFaceVideoClient

        hf_client = HuggingFaceVideoClient()

        pattern = re.compile(rf"^{re.escape(shot_id)}_10s(?:_v(\d+))?\.mp4$", re.IGNORECASE)
        highest_v = 0
        has_unversioned = False
        for f in VIDEOS_DIR.glob(f"{shot_id}_10s*.mp4"):
            m = pattern.match(f.name)
            if m:
                v_num = m.group(1)
                if v_num is not None:
                    highest_v = max(highest_v, int(v_num))
                else:
                    has_unversioned = True
        next_v = max(highest_v + 1, 2 if has_unversioned and highest_v == 0 else highest_v + 1)
        target_video_name = f"{shot_id}_10s_v{next_v}.mp4" if (highest_v > 0 or has_unversioned) else f"{shot_id}_10s.mp4"
        target_video_path = VIDEOS_DIR / target_video_name

        ok, generated_path = hf_client.generate_i2v(
            image_path=input_frame,
            prompt=prompt,
            model_key="ltx",
            duration=4.0,
            output_filename=target_video_name
        )

        if ok and generated_path and Path(generated_path).exists():
            shutil.copy2(generated_path, str(target_video_path))
            print(f"      ✓ Đã lưu video LTX vào: {target_video_path}")

            if reverse_motion:
                temp_reversed = target_video_path.with_name(f"{target_video_path.stem}_rev_temp.mp4")
                ffmpeg_exe = get_ffmpeg()
                cmd_rev = [
                    ffmpeg_exe, "-y",
                    "-i", str(target_video_path),
                    "-vf", "reverse",
                    "-c:v", "libx264", "-crf", "18", "-preset", "slow",
                    str(temp_reversed)
                ]
                res_rev = subprocess.run(cmd_rev, capture_output=True, text=True)
                if res_rev.returncode == 0 and temp_reversed.exists():
                    shutil.move(str(temp_reversed), str(target_video_path))
                    print(f"      ✓ Đã đảo ngược video LTX thành công!")

            # Tự động gắn âm thanh SFX / Voice nếu có sẵn file riêng cho shot
            audio_cand = None
            for a_dir in [ASSETS_DIR / "audio_sfx", ASSETS_DIR / "audio_voice", ASSETS_DIR / "audio"]:
                if not a_dir.exists(): continue
                for ext in [".wav", ".m4a", ".mp3", ".aac"]:
                    af = a_dir / f"{shot_id}{ext}"
                    if af.exists():
                        audio_cand = af
                        break
                if audio_cand:
                    break

            if audio_cand:
                print(f"      🎵 Phát hiện âm thanh chuyên biệt: {audio_cand.name}. Đang tích hợp vào video LTX...")
                temp_muxed = target_video_path.with_name(f"{target_video_path.stem}_mux_temp.mp4")
                ffmpeg_exe = get_ffmpeg()
                cmd_mux = [
                    ffmpeg_exe, "-y",
                    "-i", str(target_video_path),
                    "-i", str(audio_cand),
                    "-c:v", "copy",
                    "-c:a", "aac", "-b:a", "192k", "-ar", "48000",
                    "-shortest",
                    str(temp_muxed)
                ]
                res_mux = subprocess.run(cmd_mux, capture_output=True, text=True)
                if res_mux.returncode == 0 and temp_muxed.exists() and temp_muxed.stat().st_size > 1000:
                    shutil.move(str(temp_muxed), str(target_video_path))
                    print(f"      ✓ Đã gắn âm thanh vào video LTX thành công!")
                elif temp_muxed.exists():
                    temp_muxed.unlink()

            kf_dir = KEYFRAMES_DIR / shot_id
            kf_dir.mkdir(parents=True, exist_ok=True)
            tail_path = kf_dir / "clean_frame_239.jpg"

            extracted = HuggingFaceVideoClient.extract_tail_frame(str(target_video_path), str(tail_path))
            if not extracted or not Path(tail_path).exists():
                cap = cv2.VideoCapture(str(target_video_path))
                tot = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
                cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, tot - 1))
                ret, tf = cap.read()
                cap.release()
                if ret and tf is not None:
                    cv2.imwrite(str(tail_path), tf)

            print(f"\n[✓] HOÀN TẤT THÀNH CÔNG (LTX Engine): {target_video_name} -> Tail: {tail_path.name}\n")
            return True, str(tail_path)
        else:
            print(f"      [!] LTX Engine không trả về video hợp lệ.")
            return False, None
    except Exception as e:
        print(f"      [!] LTX Engine error: {e}")
        return False, None

def run_shot_pipeline(
    shot_id: str, 
    input_frame: Optional[str] = None, 
    prompt: Optional[str] = None, 
    prev_video: str = None, 
    session_name: str = "muse", 
    download_dir: Optional[Path] = None,
    reverse_motion: Optional[bool] = None,
    dry_run: Optional[bool] = None,
    engine: str = "auto"
):
    resolved_engine = classify_shot_engine(shot_id, prompt=prompt) if engine == "auto" else engine.lower()
    print(f"\n=======================================================")
    print(f"🎬 BẮT ĐẦU CHU TRÌNH TỰ ĐỘNG CHO SHOT: {shot_id}")
    print(f"   • Assigned Engine: {resolved_engine.upper()} ({'Tự động phân loại' if engine == 'auto' else 'Chỉ định thủ công'})")
    print(f"   • Worker Session:  {session_name}")
    print(f"=======================================================")
    
    # Auto-resolve reverse_motion if not provided
    if reverse_motion is None:
        try:
            from production_orchestrator import get_all_shots
            shots = get_all_shots()
            shot_data = shots.get(shot_id, {})
            reverse_motion = bool(shot_data.get("reverse_motion", False))
        except Exception:
            prompts_file = BASE_DIR / "02_AI_Prompts" / "muse_ai_video_prompts.json"
            if prompts_file.exists():
                import json
                try:
                    p_data = json.loads(prompts_file.read_text(encoding="utf-8"))
                    p_info = p_data.get("motion_prompts", {}).get(shot_id, {})
                    if isinstance(p_info, dict):
                        reverse_motion = bool(p_info.get("reverse_motion", False))
                except Exception:
                    pass
    if reverse_motion is None:
        reverse_motion = False

    # Auto-resolve prompt and input_frame if not provided
    if not prompt or not input_frame:
        try:
            from production_orchestrator import get_all_shots, resolve_start_frame
            shots = get_all_shots()
            shot_data = shots.get(shot_id, {})
            if not prompt:
                prompt = shot_data.get("motion_prompt")
            if not input_frame:
                input_frame = resolve_start_frame(shot_id, shot_data)
        except Exception:
            prompts_file = BASE_DIR / "02_AI_Prompts" / "muse_ai_video_prompts.json"
            if prompts_file.exists():
                import json
                try:
                    p_data = json.loads(prompts_file.read_text(encoding="utf-8"))
                    motion_prompts = p_data.get("motion_prompts", {})
                    p_info = motion_prompts.get(shot_id, {})
                    if not prompt:
                        if isinstance(p_info, dict):
                            prompt = p_info.get("motion_prompt")
                        elif isinstance(p_info, str):
                            prompt = p_info
                except Exception:
                    pass

    if not prompt:
        print(f"[!] Lỗi: Không thể tìm thấy motion prompt cho shot: {shot_id}")
        return False, None

    # Đảm bảo prompt có Audio Guard và Closed Lips Guard trước khi xử lý
    prompt = ensure_audio_guard(prompt)
    prompt = ensure_closed_lips_guard(prompt)

    # Hard Gate 16:9 Cinema Aspect Ratio: đảm bảo input_frame luôn đạt 16:9
    if input_frame:
        try:
            from production_orchestrator import ensure_16x9_start_frame
            input_frame = ensure_16x9_start_frame(input_frame, shot_id=shot_id)
        except Exception:
            pass

    # Chế độ Mock / Dry-Run (MUSE_DRY_RUN=1 hoặc dry_run=True)
    if dry_run is None:
        dry_run = os.environ.get("MUSE_DRY_RUN", "0").lower() in ("1", "true", "yes")

    if dry_run:
        print(f"      ⚡ [DRY-RUN] MUSE_DRY_RUN kích hoạt. Đang sinh synthetic 720p video cho {shot_id}...")
        VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
        pattern = re.compile(rf"^{re.escape(shot_id)}_10s(?:_v(\d+))?\.mp4$", re.IGNORECASE)
        highest_v = 0
        has_unversioned = False
        for f in VIDEOS_DIR.glob(f"{shot_id}_10s*.mp4"):
            m = pattern.match(f.name)
            if m:
                v_num = m.group(1)
                if v_num is not None:
                    highest_v = max(highest_v, int(v_num))
                else:
                    has_unversioned = True
        next_v = max(highest_v + 1, 2 if has_unversioned and highest_v == 0 else highest_v + 1)
        target_video = VIDEOS_DIR / f"{shot_id}_10s_v{next_v}.mp4"

        ffmpeg_bin = get_ffmpeg()
        cmd_synth = [
            ffmpeg_bin, "-y",
            "-f", "lavfi", "-i", "testsrc=duration=10:size=1280x720:rate=24",
            "-f", "lavfi", "-i", "sine=f=440:d=10:r=48000",
            "-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-pix_fmt", "yuv420p",
            "-c:a", "aac", "-b:a", "192k", "-ar", "48000",
            str(target_video)
        ]
        res = subprocess.run(cmd_synth, capture_output=True, text=True)
        if res.returncode != 0 or not target_video.exists():
            print(f"[!] Lỗi khi sinh dry-run video: {res.stderr}")
            return False, None

        kf_dir = KEYFRAMES_DIR / shot_id
        kf_dir.mkdir(parents=True, exist_ok=True)
        tail_path = kf_dir / "clean_frame_239.jpg"

        cap = cv2.VideoCapture(str(target_video))
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, total_frames - 1))
        ret, frame = cap.read()
        cap.release()
        if ret and frame is not None:
            cv2.imwrite(str(tail_path), frame)
        else:
            blank = np.zeros((720, 1280, 3), dtype=np.uint8)
            cv2.imwrite(str(tail_path), blank)

        print(f"\n[✓] [DRY-RUN] HOÀN TẤT THÀNH CÔNG: {target_video.name} -> Tail: {tail_path.name}\n")
        return True, str(tail_path)

    # 1. Nhánh xử lý Gradio LTX-Video Engine (Hugging Face ZeroGPU Free API)
    if resolved_engine == "gradio_ltx":
        ok, tail_path = _execute_gradio_ltx(shot_id, prompt, input_frame, reverse_motion)
        if ok:
            return True, tail_path
        print(f"      [!] LTX Engine không trả về video, tiếp tục fallback sang Muse2API Gateway...")

    # 2. Ưu tiên hàng đầu: Cổng dịch vụ Muse2API Gateway (FastAPI REST API http://127.0.0.1:18610)
    if HAS_MUSE_API and MuseApiClient is not None and render_shot_via_api is not None:
        try:
            api_client = MuseApiClient()
            if api_client.is_available():
                print("      ⚡ Kích hoạt Muse2API Gateway (FastAPI REST API http://127.0.0.1:18610)...")
                ok, tail_path = render_shot_via_api(
                    shot_id=shot_id,
                    prompt=prompt,
                    input_frame=input_frame,
                    reverse_motion=reverse_motion,
                    duration=5,
                    client=api_client
                )
                if ok:
                    return True, tail_path
                else:
                    print(f"      [!] Muse2API Gateway thất bại cho shot {shot_id}, tiếp tục fallback sang trình duyệt...")
            else:
                print("      [~] Muse2API Gateway chưa sẵn sàng, tiếp tục fallback sang browser automation...")
        except Exception as e:
            print(f"      [!] Muse2API exception: {e}")

    # 3. Fallback: InvisiblePlaywright Engine (Camoufox Stealth Engine)
    try:
        from muse_invpw_driver import render_shot_invpw, HAS_INVPW, check_login_status
        if HAS_INVPW:
            print("      ⚡ Kích hoạt InvisiblePlaywright Engine (Camoufox Stealth)...")
            ok, tail_path = render_shot_invpw(
                shot_id=shot_id,
                prompt=prompt,
                input_frame=input_frame,
                reverse_motion=reverse_motion
            )
            if ok:
                print(f"\n[✓] HOÀN TẤT THÀNH CÔNG: {shot_id}!\n")
                return True, tail_path
            else:
                print(f"\n[!] Muse.ai InvisiblePlaywright không phản hồi/thất bại cho shot {shot_id}.")
                if resolved_engine == "gradio_ltx":
                    print(f"      ⚡ Chuyển hướng sang Gradio LTX-Video Engine theo cấu hình...")
                    ok_fb, tail_fb = _execute_gradio_ltx(shot_id, prompt, input_frame, reverse_motion)
                    if ok_fb:
                        return True, tail_fb
                return False, None
    except Exception as e:
        print(f"      [!] InvisiblePlaywright error: {e}")
        if resolved_engine == "gradio_ltx":
            ok_fb, tail_fb = _execute_gradio_ltx(shot_id, prompt, input_frame, reverse_motion)
            if ok_fb:
                return True, tail_fb
        return False, None

    dl_dir = Path(download_dir).resolve() if download_dir else DEFAULT_DOWNLOADS_DIR
    dl_dir.mkdir(parents=True, exist_ok=True)
    browser_env = os.environ.copy()
    browser_env["AGENT_BROWSER_DOWNLOAD_PATH"] = str(dl_dir)

    initial_downloads = set(dl_dir.glob("*.mp4"))

    # 1. Upload ảnh đầu vào (Start Frame)
    if input_frame and os.path.exists(input_frame):
        print(f"[1/5] Đang upload Start Frame: {Path(input_frame).name}...")
        cmd_upload = ["agent-browser", "--session", session_name, "upload", "input[type=file]", str(input_frame)]
        res_up = subprocess.run(cmd_upload, capture_output=True, text=True, encoding="utf-8", env=browser_env)
        time.sleep(3)
        print(f"      ✓ Đã nạp ảnh thành công!")
    else:
        print(f"[1/5] Bỏ qua upload (không có ảnh đính kèm)")

    # 2. Chuẩn bị prompt với Audio Guard chống rác (chuẩn AGENTS.md §3)
    prompt = ensure_audio_guard(prompt)

    # 3. Điền prompt vào ô chat (tìm ref động để chống trôi ref)
    snap_before_fill = subprocess.run(["agent-browser", "--session", session_name, "snapshot", "-i"], capture_output=True, text=True, encoding="utf-8", env=browser_env).stdout
    tb_match = re.search(r'textbox "Nhắn tin"\s*\[ref=([^\]]+)\]', snap_before_fill) or re.search(r'textbox.*\[ref=([^\]]+)\]', snap_before_fill)
    tb_ref = tb_match.group(1) if tb_match else "e14"

    print(f"[2/5] Đang điền Prompt vào ô chat (@{tb_ref})...")
    cmd_fill = ["agent-browser", "--session", session_name, "fill", f"@{tb_ref}", prompt]
    subprocess.run(cmd_fill, capture_output=True, text=True, encoding="utf-8", env=browser_env)
    time.sleep(2)

    # 4. Ghi nhận các nút tải xuống đã có từ trước để tránh bấm nhầm video cũ
    pre_snap = subprocess.run(["agent-browser", "--session", session_name, "snapshot", "-i"], capture_output=True, text=True, encoding="utf-8", env=browser_env).stdout
    pre_dl_refs = set(re.findall(r'button "Tải video xuống" \[ref=([^\]]+)\]', pre_snap))

    pre_stop_count = pre_snap.count("Dừng nhiệm vụ") + pre_snap.count('button "Dừng"')

    # 5. Tìm ref nút Gửi sau khi đã nhập nội dung
    send_ref = None
    for line in pre_snap.splitlines():
        if 'button "Gửi"' in line and "[ref=" in line:
            send_ref = line.split("[ref=")[-1].split("]")[0]
            break

    if send_ref:
        print(f"      Phát hiện nút Gửi (@{send_ref}), đang bấm gửi...")
        cmd_send = ["agent-browser", "--session", session_name, "click", f"@{send_ref}"]
        subprocess.run(cmd_send, capture_output=True, text=True, encoding="utf-8", env=browser_env)
        time.sleep(2)
    else:
        print(f"      Không thấy nút Gửi rõ ràng, gửi lệnh focus và nhấn Enter...")
        subprocess.run(["agent-browser", "--session", session_name, "focus", f"@{tb_ref}"], capture_output=True, text=True, env=browser_env)
        subprocess.run(["agent-browser", "--session", session_name, "press", "Enter"], capture_output=True, text=True, env=browser_env)
        time.sleep(2)

    # Double check if send button is still there (meaning click didn't register)
    post_send_snap = subprocess.run(["agent-browser", "--session", session_name, "snapshot", "-i"], capture_output=True, text=True, encoding="utf-8", env=browser_env).stdout
    if 'button "Gửi"' in post_send_snap:
        print(f"      Phát hiện tin nhắn chưa gửi đi, kích hoạt Enter trực tiếp...")
        subprocess.run(["agent-browser", "--session", session_name, "focus", f"@{tb_ref}"], capture_output=True, text=True, env=browser_env)
        subprocess.run(["agent-browser", "--session", session_name, "press", "Enter"], capture_output=True, text=True, env=browser_env)
        time.sleep(2)

    print(f"      ✓ Đã gửi lệnh render sang Muse.ai!")

    # 6. Giám sát tiến độ render và tải video
    print(f"[3/5] Đang giám sát tiến độ render ngầm (chờ Muse khởi tạo task)...")
    time.sleep(10)
    # Tự động xác định version kế tiếp (_v1, _v2, _v3...) để chống ghi đè và phân biệt re-takes
    pattern = re.compile(rf"^{re.escape(shot_id)}_10s(?:_v(\d+))?\.mp4$", re.IGNORECASE)
    highest_v = 0
    has_unversioned = False
    for f in VIDEOS_DIR.glob(f"{shot_id}_10s*.mp4"):
        m = pattern.match(f.name)
        if m:
            v_num = m.group(1)
            if v_num is not None:
                highest_v = max(highest_v, int(v_num))
            else:
                has_unversioned = True
    next_v = max(highest_v + 1, 2 if has_unversioned and highest_v == 0 else highest_v + 1)
    target_video_path = VIDEOS_DIR / f"{shot_id}_10s_v{next_v}.mp4"
    task_started = False
    video_saved = False
    for attempt in range(100): # 100 * 5s = 500s (~8.3 phút)
        snap = subprocess.run(["agent-browser", "--session", session_name, "snapshot", "-i"], capture_output=True, text=True, encoding="utf-8", env=browser_env).stdout
        current_dl_refs = re.findall(r'button "Tải video xuống" \[ref=([^\]]+)\]', snap)
        new_dl_refs = [r for r in current_dl_refs if r not in pre_dl_refs]
        
        curr_stop_count = snap.count("Dừng nhiệm vụ") + snap.count('button "Dừng"')
        is_busy = (curr_stop_count > pre_stop_count or "Đang dựng" in snap or "Đang tạo" in snap) and not new_dl_refs
        if is_busy:
            task_started = True

        status_msg = "Đang render..." if is_busy else ("Đã xong!" if new_dl_refs else "Đang khởi tạo task...")
        print(f"      [{(attempt + 2) * 5}s] Trạng thái: {status_msg}", flush=True)

        # Kiểm tra từ chối hoặc thất bại MỚI từ Muse AI (chỉ khi có lỗi mới phát sinh)
        if not new_dl_refs and attempt >= 6:
            pre_err_count = pre_snap.count("không dựng được") + pre_snap.count("vẫn không dựng được") + pre_snap.count("thất bại") + pre_snap.count("không thể tạo được")
            curr_err_count = snap.count("không dựng được") + snap.count("vẫn không dựng được") + snap.count("thất bại") + snap.count("không thể tạo được")
            if curr_err_count > pre_err_count:
                print("      [!] Phát hiện Muse AI từ chối tạo video với prompt này!", flush=True)
                return False, None
        
        # Chỉ bấm tải khi CÓ nút 'Tải video xuống' MỚI
        if new_dl_refs:
            last_dl_ref = new_dl_refs[-1]
            print(f"      [✓] Phát hiện video mới hoàn tất! Bấm tải xuống (@{last_dl_ref})...", flush=True)
            subprocess.run(["agent-browser", "--session", session_name, "click", f"@{last_dl_ref}"], capture_output=True, text=True, env=browser_env)
            time.sleep(6)
            
            # Tìm file mới nhất trong dl_dir
            current_downloads = set(dl_dir.glob("*.mp4"))
            new_files = current_downloads - initial_downloads
            if not new_files:
                recent = sorted(dl_dir.glob("*.mp4"), key=lambda f: f.stat().st_mtime, reverse=True)
                if recent and (time.time() - recent[0].stat().st_mtime < 180):
                    new_files = [recent[0]]
            
            if new_files:
                downloaded = list(new_files)[0]
                shutil.copy2(downloaded, target_video_path)
                print(f"      ✓ Đã lưu video vào: {target_video_path}")
                video_saved = True
                if dl_dir != DEFAULT_DOWNLOADS_DIR:
                    try:
                        downloaded.unlink()
                    except Exception:
                        pass
                break
        time.sleep(5)
            
    if not video_saved:
        print(f"[!] Lỗi: Hết thời gian chờ render.")
        return False, None

    # Áp dụng Reverse Motion bằng FFmpeg nếu có cờ reverse_motion
    if reverse_motion:
        print(f"[3.5/5] 🔄 Kích hoạt Reverse Motion cho {shot_id} via FFmpeg (-vf reverse -af areverse)...")
        temp_reversed = target_video_path.with_name(f"{target_video_path.stem}_rev_temp.mp4")
        ffmpeg_exe = get_ffmpeg()
        ffmpeg_cmd = [
            ffmpeg_exe, "-y",
            "-i", str(target_video_path),
            "-vf", "reverse",
            "-af", "areverse",
            "-c:v", "libx264",
            "-crf", "18",
            "-preset", "slow",
            "-c:a", "aac",
            "-b:a", "192k",
            "-ar", "48000",
            str(temp_reversed)
        ]
        res_rev = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
        if res_rev.returncode == 0 and temp_reversed.exists() and temp_reversed.stat().st_size > 1000:
            shutil.move(str(temp_reversed), str(target_video_path))
            print(f"      ✓ Đã đảo ngược video và audio thành công cho {target_video_path.name}!")
        else:
            print(f"      [!] Lỗi khi thực hiện reverse motion FFmpeg: {res_rev.stderr}")
            if temp_reversed.exists():
                try:
                    temp_reversed.unlink()
                except Exception:
                    pass

    # 5. Trích xuất Tail Frame (Frame cuối)
    print(f"[4/5] Đang trích xuất Tail Frame bằng OpenCV...")
    cap = cv2.VideoCapture(str(target_video_path))
    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()
    
    shot_kf_dir = KEYFRAMES_DIR / shot_id
    tail_frame_path = shot_kf_dir / "clean_frame_239.jpg"
    if ret and tail_frame is not None:
        shot_kf_dir.mkdir(parents=True, exist_ok=True)
        cv2.imwrite(str(tail_frame_path), tail_frame)
        print(f"      ✓ Đã lưu Tail Frame: {tail_frame_path}")
    
    # 6. Kiểm tra Continuity với Shot trước
    if prev_video and os.path.exists(prev_video):
        print(f"[5/5] Đang kiểm tra tính liên tục với shot trước ({Path(prev_video).name})...")
        from continuity_checker import check_shot_continuity
        is_smooth, score = check_shot_continuity(prev_video, str(target_video_path))
        if is_smooth:
            print(f"      [✓] MẠCH PHIM LIỀN MẠCH TUYỆT ĐỐI: Điểm khớp {score:.2%}")
        else:
            print(f"      ⚠️ GỢI Ý ĐẠO DIỄN: Điểm khớp {score:.2%}. Cần chèn 1 transition bridge 1s giữa {Path(prev_video).name} và {shot_id}_10s.mp4")
    else:
        print(f"[5/5] Hoàn tất chu trình shot.")

    print(f"\n[✓] HOÀN TẤT THÀNH CÔNG: {shot_id}!\n")
    return True, str(tail_frame_path)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Chạy render tự động cho 1 shot đơn lẻ qua Muse.ai")
    parser.add_argument("--shot", "--shot-id", dest="shot_id", required=True, help="Mã định danh shot (vd: ep01_scene01_shot01)")
    parser.add_argument("--input-frame", default=None, help="Đường dẫn Start Frame (tự động resolve nếu bỏ trống)")
    parser.add_argument("--prompt", default=None, help="Prompt mô tả chuyển động (tự động load nếu bỏ trống)")
    parser.add_argument("--prev-video", default=None, help="Đường dẫn video shot trước để kiểm tra continuity")
    parser.add_argument("--session", default="muse", help="Tên session agent-browser (mặc định: muse)")
    parser.add_argument("--download-dir", default=None, help="Thư mục download cô lập cho worker")
    parser.add_argument("--reverse", "--reverse-motion", dest="reverse_motion", action="store_true", default=None, help="Kích hoạt đảo ngược chuyển động video qua FFmpeg (-vf reverse -af areverse)")
    parser.add_argument("--engine", default="auto", choices=["auto", "muse", "muse_api", "gradio_ltx"], help="Động cơ render (mặc định: auto - tự động dùng Muse2API Gateway nếu khả dụng)")
    args = parser.parse_args()
    
    run_shot_pipeline(
        args.shot_id, 
        args.input_frame, 
        args.prompt, 
        args.prev_video,
        session_name=args.session,
        download_dir=Path(args.download_dir) if args.download_dir else None,
        reverse_motion=args.reverse_motion,
        engine=args.engine
    )
