"""
Historical 2D Studio - Episode 01: Battle of Cannae (216 BC)
HIGH-PERFORMANCE HISTORICAL PROLOGUE GENERATOR (2-5 Minute Narrative Briefing)
Lead Motion & Pipeline Automation Engineer: Elena Rostova
Head of Scriptwriting: Victor Vance | Head of Audio: Arthur Pendelton

Duration: 182.52 seconds (~3.04 minutes) @ 30 FPS (5,475 frames)
Optimized with Pre-Composited Static Canvas Buffers + Hardware Accelerated NVENC (GTX 1660 Super)
Rendering speed: > 120 FPS
"""

import os
import sys
import math
import subprocess
import numpy as np
import cv2

# Tích hợp SevenNguyenActor từ Engine 2026
workspace_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
if workspace_root_dir not in sys.path:
    sys.path.insert(0, workspace_root_dir)
from src.tactical_engine.seven_actor import SevenNguyenActor, AudioLipSyncController

WIDTH = 1920
HEIGHT = 1080
FPS = 30


def overlay_alpha_image(dst_bgr, src_bgra, x, y, scale=1.0, opacity=1.0):
    if src_bgra is None:
        return dst_bgr
    if scale != 1.0:
        new_w = max(1, int(src_bgra.shape[1] * scale))
        new_h = max(1, int(src_bgra.shape[0] * scale))
        src_bgra = cv2.resize(src_bgra, (new_w, new_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
        
    sh, sw, _ = src_bgra.shape
    dh, dw, _ = dst_bgr.shape
    
    x1, y1 = max(0, x), max(0, y)
    x2, y2 = min(dw, x + sw), min(dh, y + sh)
    
    if x1 >= x2 or y1 >= y2:
        return dst_bgr
        
    sx1 = x1 - x
    sy1 = y1 - y
    sx2 = sx1 + (x2 - x1)
    sy2 = sy1 + (y2 - y1)
    
    src_crop = src_bgra[sy1:sy2, sx1:sx2]
    alpha = (src_crop[:, :, 3].astype(np.float32) / 255.0) * opacity
    alpha_3d = np.repeat(alpha[:, :, np.newaxis], 3, axis=2)
    
    dst_crop = dst_bgr[y1:y2, x1:x2].astype(np.float32)
    src_rgb = src_crop[:, :, :3].astype(np.float32)
    
    blended = src_rgb * alpha_3d + dst_crop * (1.0 - alpha_3d)
    dst_bgr[y1:y2, x1:x2] = np.clip(blended, 0, 255).astype(np.uint8)
    return dst_bgr

def create_smooth_spline(pts, num_samples=360):
    """Làm mịn đường bờ biển bằng nội suy Catmull-Rom spline"""
    pts = np.array(pts, dtype=np.float32)
    n = len(pts)
    res = []
    for i in range(n):
        p0 = pts[(i - 1) % n]
        p1 = pts[i]
        p2 = pts[(i + 1) % n]
        p3 = pts[(i + 2) % n]
        steps = max(2, num_samples // n)
        for t in np.linspace(0, 1, steps, endpoint=False):
            t2 = t * t
            t3 = t2 * t
            v = 0.5 * (
                (2 * p1) +
                (-p0 + p2) * t +
                (2*p0 - 5*p1 + 4*p2 - p3) * t2 +
                (-p0 + 3*p1 - 3*p2 + p3) * t3
            )
            res.append((int(v[0]), int(v[1])))
    return np.array(res, dtype=np.int32)

class FastPrologueEngine:
    def __init__(self, workspace_root):
        self.workspace = workspace_root
        
        map_4k_path = os.path.join(workspace_root, "episodes", "EP01_Cannae_216BC", "02_Map_Data", "cannae_terrain_4k.png")
        if os.path.exists(map_4k_path):
            self.terrain_base = cv2.resize(cv2.imread(map_4k_path), (WIDTH, HEIGHT), interpolation=cv2.INTER_AREA)
        else:
            self.terrain_base = np.full((HEIGHT, WIDTH, 3), (22, 26, 32), dtype=np.uint8)
            
        war_room_path = os.path.join(workspace_root, "assets", "Seven Nguyen", "seven_nguyen_tactical_war_room_host.jpg")
        if os.path.exists(war_room_path):
            self.war_room_bg = cv2.resize(cv2.imread(war_room_path), (WIDTH, HEIGHT), interpolation=cv2.INTER_LINEAR)
        else:
            self.war_room_bg = self.terrain_base.copy()
            
        # Sprites
        output_dir = os.path.join(workspace_root, "output")
        self.sprites = {}
        for k in ["p1", "p2", "p3", "p4", "p6"]:
            sp_path = os.path.join(output_dir, f"final_sprite_{k}.png")
            self.sprites[k] = cv2.imread(sp_path, cv2.IMREAD_UNCHANGED) if os.path.exists(sp_path) else None
            
        # 6 Icons
        icons_dir = os.path.join(workspace_root, "episodes", "EP01_Cannae_216BC", "02_Map_Data", "unit_icons")
        self.unit_icons = {}
        for ik in ["roman_legionary", "numidian_cavalry", "celtic_gallic_warrior", "carthaginian_veteran", "iberian_falcata", "roman_cavalry"]:
            ip = os.path.join(icons_dir, f"{ik}.png")
            self.unit_icons[ik] = cv2.imread(ip, cv2.IMREAD_UNCHANGED) if os.path.exists(ip) else None
            
        # Animated Actor & Audio LipSync
        self.actor = SevenNguyenActor(workspace_root)
        audio_file = os.path.join(workspace_root, "episodes", "EP01_Cannae_216BC", "05_Renders", "test_prologue_audio_en.wav")
        self.audio_ctl = AudioLipSyncController(audio_file, fps=FPS, total_sec=180.0)
        
        print("[ENGINE] Pre-compositing static scene buffers with authentic tactical cartography...")
        self.bg_part1 = self._build_part1_buffer()
        self.bg_part2 = self._build_part2_buffer()
        self.bg_part3 = self._build_part3_buffer()
        print("[ENGINE] Pre-composition complete! Ready for dynamic 120+ FPS hardware stream.")
        
    def _create_base_frame(self):
        frame = cv2.addWeighted(self.war_room_bg, 0.40, self.terrain_base, 0.60, 0)
        dark_overlay = np.full((HEIGHT, WIDTH, 3), (10, 12, 16), dtype=np.uint8)
        frame = cv2.addWeighted(frame, 0.35, dark_overlay, 0.65, 0)
        # Header
        cv2.rectangle(frame, (0, 0), (WIDTH, 80), (12, 14, 18), -1)
        cv2.line(frame, (0, 80), (WIDTH, 80), (212, 175, 55), 2, cv2.LINE_AA)
        cv2.putText(frame, "THE HISTORICAL VAULT -- STRATEGIC PROLOGUE (218-216 BC)", (60, 48), cv2.FONT_HERSHEY_TRIPLEX, 0.72, (212, 175, 55), 1, cv2.LINE_AA)
        return frame

    def _draw_bottom_telemetry(self, frame, header, text):
        bx1, by1 = 60, 920
        bw, bh = WIDTH - 120, 110
        cv2.rectangle(frame, (bx1, by1), (bx1 + bw, by1 + bh), (12, 14, 18), -1)
        cv2.rectangle(frame, (bx1, by1), (bx1 + bw, by1 + bh), (212, 175, 55), 1, cv2.LINE_AA)
        cv2.putText(frame, header, (bx1 + 30, by1 + 35), cv2.FONT_HERSHEY_TRIPLEX, 0.52, (212, 175, 55), 1, cv2.LINE_AA)
        cv2.putText(frame, text, (bx1 + 30, by1 + 75), cv2.FONT_HERSHEY_SIMPLEX, 0.40, (230, 240, 250), 1, cv2.LINE_AA)

    def _build_part1_buffer(self):
        frame = self._create_base_frame()
        cv2.putText(frame, "PART I: THE GHOST OF TREBIA & LAKE TRASIMENE", (60, 130), cv2.FONT_HERSHEY_TRIPLEX, 0.65, (245, 215, 115), 1, cv2.LINE_AA)
        cv2.putText(frame, "The Road Across the Alps and the Annihilation of Roman Legions", (60, 160), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (180, 200, 220), 1, cv2.LINE_AA)
        
        # Strategic Map Canvas
        mx, my, mw, mh = 60, 190, 1140, 710
        cv2.rectangle(frame, (mx, my), (mx + mw, my + mh), (15, 18, 24), -1)
        
        # Authentic Cartography of Italy
        italy_raw_pts = [
            (mx + 90, my + 130), (mx + 140, my + 75), (mx + 210, my + 50),
            (mx + 300, my + 45), (mx + 400, my + 50), (mx + 500, my + 65),
            (mx + 590, my + 80), (mx + 640, my + 115), (mx + 595, my + 140),
            (mx + 525, my + 175), (mx + 505, my + 215), (mx + 535, my + 255),
            (mx + 580, my + 300), (mx + 630, my + 360), (mx + 690, my + 420),
            (mx + 775, my + 425), (mx + 785, my + 465), (mx + 825, my + 490),
            (mx + 885, my + 530), (mx + 945, my + 580), (mx + 975, my + 630),
            (mx + 945, my + 655), (mx + 905, my + 635), (mx + 865, my + 600),
            (mx + 835, my + 625), (mx + 805, my + 665), (mx + 785, my + 710),
            (mx + 755, my + 750), (mx + 705, my + 780), (mx + 675, my + 775),
            (mx + 685, my + 740), (mx + 705, my + 690), (mx + 695, my + 630),
            (mx + 665, my + 570), (mx + 635, my + 525), (mx + 595, my + 490),
            (mx + 580, my + 465), (mx + 540, my + 430), (mx + 500, my + 390),
            (mx + 450, my + 345), (mx + 400, my + 300), (mx + 340, my + 260),
            (mx + 280, my + 225), (mx + 230, my + 200), (mx + 170, my + 175)
        ]
        smooth_italy = create_smooth_spline(italy_raw_pts, 360)
        
        # Coastal water glow rings
        cv2.polylines(frame, [smooth_italy], True, (35, 55, 70), 12, cv2.LINE_AA)
        cv2.polylines(frame, [smooth_italy], True, (45, 80, 105), 4, cv2.LINE_AA)
        cv2.fillPoly(frame, [smooth_italy], (28, 35, 46), cv2.LINE_AA)
        cv2.polylines(frame, [smooth_italy], True, (190, 160, 90), 2, cv2.LINE_AA)
        
        # Sicily
        sicily_raw = [
            (mx + 580, my + 760), (mx + 655, my + 800),
            (mx + 555, my + 830), (mx + 465, my + 795), (mx + 495, my + 750)
        ]
        smooth_sicily = create_smooth_spline(sicily_raw, 80)
        cv2.polylines(frame, [smooth_sicily], True, (35, 55, 70), 8, cv2.LINE_AA)
        cv2.fillPoly(frame, [smooth_sicily], (28, 35, 46), cv2.LINE_AA)
        cv2.polylines(frame, [smooth_sicily], True, (190, 160, 90), 2, cv2.LINE_AA)
        cv2.putText(frame, "SICILIA", (mx + 525, my + 795), cv2.FONT_HERSHEY_SIMPLEX, 0.40, (110, 130, 150), 1, cv2.LINE_AA)
        
        # Alps Mountain Shading
        for ax in range(mx + 140, mx + 570, 30):
            ay = my + 65 + int(math.sin(ax * 0.05) * 12)
            tri = np.array([(ax, ay - 14), (ax - 12, ay + 6), (ax + 12, ay + 6)], np.int32)
            cv2.fillPoly(frame, [tri], (40, 50, 62), cv2.LINE_AA)
            cv2.polylines(frame, [tri], True, (100, 125, 145), 1, cv2.LINE_AA)
        cv2.putText(frame, "ALPES (ALPS BARRIER)", (mx + 280, my + 85), cv2.FONT_HERSHEY_TRIPLEX, 0.45, (220, 235, 250), 1, cv2.LINE_AA)
        
        # Apennines Mountains Ridge
        apen_pts = [(mx+380, my+235), (mx+450, my+285), (mx+520, my+340), (mx+610, my+430), (mx+690, my+520), (mx+740, my+610)]
        for apx, apy in apen_pts:
            tri = np.array([(apx, apy - 12), (apx - 10, apy + 6), (apx + 10, apy + 6)], np.int32)
            cv2.fillPoly(frame, [tri], (35, 42, 54), cv2.LINE_AA)
            cv2.polylines(frame, [tri], True, (80, 100, 120), 1, cv2.LINE_AA)
        cv2.putText(frame, "APENNINUS", (mx + 540, my + 380), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (120, 140, 160), 1, cv2.LINE_AA)
        
        # River Po
        po_pts = np.array([(mx+210, my+140), (mx+280, my+145), (mx+360, my+160), (mx+440, my+165), (mx+525, my+175)], np.int32)
        cv2.polylines(frame, [po_pts], False, (85, 130, 160), 2, cv2.LINE_AA)
        cv2.putText(frame, "Padus (River Po)", (mx+320, my+155), cv2.FONT_HERSHEY_SIMPLEX, 0.34, (140, 185, 215), 1, cv2.LINE_AA)
        
        # Rome City
        cv2.circle(frame, (mx+500, my+390), 6, (212, 175, 55), -1, cv2.LINE_AA)
        cv2.circle(frame, (mx+500, my+390), 9, (245, 220, 120), 1, cv2.LINE_AA)
        cv2.putText(frame, "ROMA (ROME)", (mx+515, my+395), cv2.FONT_HERSHEY_TRIPLEX, 0.48, (255, 235, 170), 1, cv2.LINE_AA)
        
        # Sea Labels
        cv2.putText(frame, "MARE ADRIATICUM", (mx + 700, my + 260), cv2.FONT_HERSHEY_TRIPLEX, 0.52, (45, 70, 95), 1, cv2.LINE_AA)
        cv2.putText(frame, "MARE TYRRHENUM", (mx + 260, my + 490), cv2.FONT_HERSHEY_TRIPLEX, 0.52, (45, 70, 95), 1, cv2.LINE_AA)
        cv2.putText(frame, "MARE IONIUM", (mx + 860, my + 720), cv2.FONT_HERSHEY_TRIPLEX, 0.46, (45, 70, 95), 1, cv2.LINE_AA)
        
        # Border
        cv2.rectangle(frame, (mx, my), (mx + mw, my + mh), (212, 175, 55), 1, cv2.LINE_AA)
        
        # Right Sidebar: Tactical Unit Dossier (Positioned to NEVER overlap host)
        rx = 1230
        rw, rh = 630, 240
        cv2.rectangle(frame, (rx, my), (rx + rw, my + rh), (16, 20, 26), -1)
        cv2.rectangle(frame, (rx, my), (rx + rw, my + rh), (212, 175, 55), 1, cv2.LINE_AA)
        cv2.putText(frame, "TACTICAL FORCE INTELLIGENCE", (rx + 25, my + 32), cv2.FONT_HERSHEY_TRIPLEX, 0.46, (212, 175, 55), 1, cv2.LINE_AA)
        
        if self.unit_icons.get("celtic_gallic_warrior") is not None:
            overlay_alpha_image(frame, self.unit_icons["celtic_gallic_warrior"], rx + 25, my + 50, scale=0.55, opacity=1.0)
            cv2.putText(frame, "GALLIC & CELTIC ALLIES (16,000 MEN)", (rx + 110, my + 75), cv2.FONT_HERSHEY_TRIPLEX, 0.40, (245, 215, 115), 1, cv2.LINE_AA)
            cv2.putText(frame, "Crucial elastic crescent center; lure Roman infantry into trap.", (rx + 110, my + 98), cv2.FONT_HERSHEY_SIMPLEX, 0.32, (180, 195, 210), 1, cv2.LINE_AA)
            
        if self.unit_icons.get("roman_legionary") is not None:
            overlay_alpha_image(frame, self.unit_icons["roman_legionary"], rx + 25, my + 140, scale=0.55, opacity=1.0)
            cv2.putText(frame, "ROMAN LEGIONARIES (86,400 MEN TOTAL)", (rx + 110, my + 165), cv2.FONT_HERSHEY_TRIPLEX, 0.40, (230, 80, 80), 1, cv2.LINE_AA)
            cv2.putText(frame, "Super-dense consular battering ram; twice normal battle depth.", (rx + 110, my + 188), cv2.FONT_HERSHEY_SIMPLEX, 0.32, (180, 195, 210), 1, cv2.LINE_AA)
            
        self._draw_bottom_telemetry(frame, "HANNIBAL BARCA -- THE MEDITERRANEAN STORM", 
                                   "After crossing the Alps with war elephants, Hannibal strikes crushing blows at Trebia and Lake Trasimene, obliterating 45,000 Roman troops.")
        return frame

    def _build_part2_buffer(self):
        frame = self._create_base_frame()
        cv2.putText(frame, "PART II: THE DILEMMA OF FABIUS & DIVIDED COMMAND", (60, 130), cv2.FONT_HERSHEY_TRIPLEX, 0.65, (245, 215, 115), 1, cv2.LINE_AA)
        cv2.putText(frame, "The Fabian Strategy of Delay vs The Popular Demand for Decisive Action", (60, 160), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (180, 200, 220), 1, cv2.LINE_AA)
        
        # Dual Political Cards
        cx1, cy1, cw, ch = 80, 200, 520, 560
        cv2.rectangle(frame, (cx1, cy1), (cx1 + cw, cy1 + ch), (18, 22, 30), -1)
        cv2.rectangle(frame, (cx1, cy1), (cx1 + cw, cy1 + ch), (220, 60, 60), 2, cv2.LINE_AA)
        cv2.putText(frame, "GAIUS TERENTIUS VARRO", (cx1 + 25, cy1 + 50), cv2.FONT_HERSHEY_TRIPLEX, 0.58, (240, 100, 100), 1, cv2.LINE_AA)
        cv2.putText(frame, "CONSUL OF ROME -- PLEBEIAN FACTION", (cx1 + 25, cy1 + 80), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (180, 200, 220), 1, cv2.LINE_AA)
        
        varro_bullets = [
            "- Champion of the Plebeian faction and Senate hardliners.",
            "- Vehemently rejects Fabius Cunctator's strategy of delay.",
            "- Absolute conviction: 86,400 heavy infantry will crush Carthage.",
            "- Aggressive, impatient, orders immediate assault on his command days."
        ]
        for idx, b in enumerate(varro_bullets):
            cv2.putText(frame, b, (cx1 + 25, cy1 + 130 + idx * 42), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (220, 225, 235), 1, cv2.LINE_AA)
            
        cx2, cy2 = 640, 200
        cv2.rectangle(frame, (cx2, cy2), (cx2 + cw, cy2 + ch), (18, 22, 30), -1)
        cv2.rectangle(frame, (cx2, cy2), (cx2 + cw, cy2 + ch), (212, 175, 55), 2, cv2.LINE_AA)
        cv2.putText(frame, "LUCIUS AEMILIUS PAULLUS", (cx2 + 25, cy2 + 50), cv2.FONT_HERSHEY_TRIPLEX, 0.58, (212, 175, 55), 1, cv2.LINE_AA)
        cv2.putText(frame, "CONSUL OF ROME -- PATRICIAN ARISTOCRAT", (cx2 + 25, cy2 + 80), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (180, 200, 220), 1, cv2.LINE_AA)
        
        paullus_bullets = [
            "- Battle-hardened patrician veteran, deeply cautious and methodical.",
            "- Warns that the flat Apulian plains favor Hannibal's Numidian cavalry.",
            "- Urges withdrawal into the rugged hills to negate cavalry superiority.",
            "- Powerless against Roman law: alternating daily command rotation!"
        ]
        for idx, b in enumerate(paullus_bullets):
            cv2.putText(frame, b, (cx2 + 25, cy2 + 130 + idx * 42), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (220, 225, 235), 1, cv2.LINE_AA)
            
        # Fatal Flaw Box
        fx, fy, fw, fh = 80, 780, 1080, 110
        cv2.rectangle(frame, (fx, fy), (fx + fw, fy + fh), (28, 16, 18), -1)
        cv2.rectangle(frame, (fx, fy), (fx + fw, fy + fh), (220, 40, 40), 1, cv2.LINE_AA)
        cv2.putText(frame, "THE FATAL ARCHITECTURAL FLAW: ALTERNATING DAILY COMMAND", (fx + 25, fy + 35), cv2.FONT_HERSHEY_TRIPLEX, 0.50, (240, 80, 80), 1, cv2.LINE_AA)
        cv2.putText(frame, "Hannibal weaponized this doctrine. He chose August 2, 216 BC--Varro's command day--to spring his immortal tactical trap.", (fx + 25, fy + 72), cv2.FONT_HERSHEY_SIMPLEX, 0.40, (255, 255, 255), 1, cv2.LINE_AA)
        
        # Host is dynamically drawn in render_frame_fast with pose 'p3' (pointing LEFT towards cards)
        self._draw_bottom_telemetry(frame, "DIVIDED LEADERSHIP -- THE RECIPE FOR DISASTER", 
                                   "The bitter feud between the two Consuls and their alternating daily command handed Hannibal the exact blunder he needed.")
        return frame

    def _build_part3_buffer(self):
        frame = self._create_base_frame()
        cv2.putText(frame, "PART III: THE 86,400-MAN STEAMROLLER & THE APULIAN FURNACE", (60, 130), cv2.FONT_HERSHEY_TRIPLEX, 0.65, (245, 215, 115), 1, cv2.LINE_AA)
        cv2.putText(frame, "Tactical Unit Roster and Equipment of Rome vs Carthage", (60, 160), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (180, 200, 220), 1, cv2.LINE_AA)
        
        # 6 Soldier Cards
        units_data = [
            ("roman_legionary", "ROMAN LEGIONARY", "86,400 Men Total", "Scutum Shield, Lorica Hamata, Pilum", (220, 60, 60)),
            ("roman_cavalry", "ROMAN EQUITES", "6,400 Cavalry", "Patrician Attic Helmet, Parma Shield, Hasta", (220, 80, 80)),
            ("numidian_cavalry", "NUMIDIAN LIGHT CAVALRY", "4,000 Horsemen", "Bareback Berber Steeds, Triple Javelins", (212, 175, 55)),
            ("celtic_gallic_warrior", "CELTIC & GALLIC WARRIORS", "16,000 Center Bait", "Torc Necklace, Iron Spatha, Oval Shield", (60, 160, 220)),
            ("carthaginian_veteran", "LIBYAN VETERAN PHALANX", "10,000 Flank Anvils", "Trophy Scale Armor, Bronze Aspis, Dory", (30, 80, 180)),
            ("iberian_falcata", "IBERIAN FALCATA INFANTRY", "8,000 Shock Troops", "White/Purple Tunic, Caetra, Falcata Blade", (200, 140, 50)),
        ]
        
        card_w = 360
        card_h = 160
        start_x, start_y = 60, 190
        gap_x, gap_y = 25, 18
        
        for idx, (icon_key, uname, ucount, uequip, ucol) in enumerate(units_data):
            row = idx // 3
            col = idx % 3
            bx = start_x + col * (card_w + gap_x)
            by = start_y + row * (card_h + gap_y)
            
            cv2.rectangle(frame, (bx, by), (bx + card_w, by + card_h), (16, 20, 26), -1)
            cv2.rectangle(frame, (bx, by), (bx + card_w, by + card_h), ucol, 1, cv2.LINE_AA)
            
            icon_img = self.unit_icons.get(icon_key)
            if icon_img is not None:
                overlay_alpha_image(frame, icon_img, bx + 12, by + 20, scale=0.48, opacity=1.0)
                
            cv2.putText(frame, uname, (bx + 115, by + 40), cv2.FONT_HERSHEY_TRIPLEX, 0.44, ucol, 1, cv2.LINE_AA)
            cv2.putText(frame, ucount, (bx + 115, by + 68), cv2.FONT_HERSHEY_SIMPLEX, 0.40, (255, 255, 255), 1, cv2.LINE_AA)
            cv2.putText(frame, uequip, (bx + 115, by + 98), cv2.FONT_HERSHEY_SIMPLEX, 0.32, (180, 190, 205), 1, cv2.LINE_AA)
            
        # Bottom Tactical Comparison
        bx, by, bw, bh = 60, 560, 1130, 240
        cv2.rectangle(frame, (bx, by), (bx + bw, by + bh), (14, 18, 24), -1)
        cv2.rectangle(frame, (bx, by), (bx + bw, by + bh), (212, 175, 55), 1, cv2.LINE_AA)
        cv2.putText(frame, "THE FORMATION PARADOX -- CONVEX CRESCENT VS MASSED RAM", (bx + 30, by + 40), cv2.FONT_HERSHEY_TRIPLEX, 0.55, (212, 175, 55), 1, cv2.LINE_AA)
        
        tactics_notes = [
            "1. Rome deploys an 86,400-man battering ram packed twice as dense as regulation, aiming to smash Hannibal's center.",
            "2. Hannibal deliberately places his weakest 20,000 Gallic and Iberian troops forward in a protruding Convex Crescent.",
            "3. Flanks concealed with 10,000 elite Libyan veterans, positioned to execute a 90-degree inward pincer wheel.",
            "4. Decisive blow: 10,000 heavy cavalry under Hasdrubal sweep Roman wings and slam the cauldron shut from behind."
        ]
        for i, note in enumerate(tactics_notes):
            cv2.putText(frame, note, (bx + 30, by + 85 + i * 36), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (230, 235, 245), 1, cv2.LINE_AA)
            
        # Host is dynamically drawn in render_frame_fast with pose 'p6'
        self._draw_bottom_telemetry(frame, "MASS VS GEOMETRY -- THE STAGE IS SET", 
                                   "86,400 Roman legionaries march into the valley of the Aufidus, completely unaware they are stepping into history's greatest meat grinder.")
        return frame

    def render_frame_fast(self, t_sec, frame_idx=0):
        if t_sec < 61.9:
            frame = self.bg_part1.copy()
            mx, my = 60, 190
            pts_route = [
                (mx + 130, my + 105), # Alps
                (mx + 330, my + 175), # Trebia
                (mx + 495, my + 310), # Trasimene
                (mx + 825, my + 490)  # Cannae
            ]
            route_progress = min(1.0, t_sec / 45.0)
            for i in range(len(pts_route) - 1):
                seg_start = i / 3.0
                seg_end = (i + 1) / 3.0
                if route_progress > seg_start:
                    p_seg = min(1.0, (route_progress - seg_start) / (seg_end - seg_start))
                    p0 = pts_route[i]
                    p1 = pts_route[i+1]
                    cur_pt = (int(p0[0] + (p1[0] - p0[0]) * p_seg), int(p0[1] + (p1[1] - p0[1]) * p_seg))
                    cv2.line(frame, p0, cur_pt, (0, 0, 0), 8, cv2.LINE_AA)
                    cv2.line(frame, p0, cur_pt, (30, 45, 190), 4, cv2.LINE_AA)
                    cv2.line(frame, p0, cur_pt, (180, 220, 255), 1, cv2.LINE_AA)
                    cv2.circle(frame, cur_pt, 6, (212, 175, 55), -1, cv2.LINE_AA)
                    
            if t_sec > 2.0:
                ax, ay = pts_route[0]
                cv2.circle(frame, (ax, ay), 10, (212, 175, 55), -1, cv2.LINE_AA)
                cv2.circle(frame, (ax, ay), 13, (255, 255, 255), 1, cv2.LINE_AA)
                cv2.putText(frame, "1. ALPS CROSSING (218 BC)", (ax + 16, ay + 5), cv2.FONT_HERSHEY_TRIPLEX, 0.42, (255, 235, 170), 1, cv2.LINE_AA)
            if t_sec > 12.0:
                tx, ty = pts_route[1]
                cv2.circle(frame, (tx, ty), 10, (60, 80, 240), -1, cv2.LINE_AA)
                cv2.circle(frame, (tx, ty), 13, (255, 255, 255), 1, cv2.LINE_AA)
                cv2.putText(frame, "2. TREBIA (DEC 218 BC)", (tx + 16, ty + 5), cv2.FONT_HERSHEY_TRIPLEX, 0.42, (255, 235, 170), 1, cv2.LINE_AA)
            if t_sec > 28.0:
                tx, ty = pts_route[2]
                cv2.circle(frame, (tx, ty), 10, (60, 80, 240), -1, cv2.LINE_AA)
                cv2.circle(frame, (tx, ty), 13, (255, 255, 255), 1, cv2.LINE_AA)
                cv2.putText(frame, "3. LAKE TRASIMENE (217 BC)", (tx + 16, ty + 5), cv2.FONT_HERSHEY_TRIPLEX, 0.42, (255, 235, 170), 1, cv2.LINE_AA)
            if t_sec > 45.0:
                tx, ty = pts_route[3]
                cv2.circle(frame, (tx, ty), 12, (30, 40, 255), -1, cv2.LINE_AA)
                cv2.circle(frame, (tx, ty), 15, (212, 175, 55), 2, cv2.LINE_AA)
                cv2.putText(frame, "4. CANNAE (AUG 216 BC)", (tx + 18, ty + 5), cv2.FONT_HERSHEY_TRIPLEX, 0.46, (212, 175, 55), 1, cv2.LINE_AA)
                
            # Animated Host Seven Nguyen (Pose 'p4' crossed arms, zero occlusion below dossier)
            self.actor.draw_actor(frame, "p4", 1260, 480, base_scale=1.45, opacity=1.0, t_sec=t_sec, frame_idx=frame_idx, audio_ctl=self.audio_ctl)
            return frame
        elif t_sec < 120.0:
            frame = self.bg_part2.copy()
            # Animated Host Seven Nguyen (Pose 'p3' pointing LEFT directly at the two Consuls!)
            self.actor.draw_actor(frame, "p3", 1240, 340, base_scale=1.50, opacity=1.0, t_sec=t_sec, frame_idx=frame_idx, audio_ctl=self.audio_ctl)
            return frame
        else:
            frame = self.bg_part3.copy()
            # Animated Host Seven Nguyen (Pose 'p6' thinking / leaning)
            self.actor.draw_actor(frame, "p6", 1240, 280, base_scale=1.50, opacity=1.0, t_sec=t_sec, frame_idx=frame_idx, audio_ctl=self.audio_ctl)
            return frame

def generate_prologue_audio(output_audio_path):
    workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
    voice_dir = os.path.join(workspace, "episodes", "EP01_Cannae_216BC", "04_Audio_Voice", "voice_en")
    ambient_bed = os.path.join(workspace, "episodes", "EP01_Cannae_216BC", "04_Audio_Voice", "master_mix", "cannae_ambient_soundbed_30m.wav")
    
    s3 = os.path.abspath(os.path.join(voice_dir, "scene_03_trebia_trasimene_fabius_en.mp3")).replace('\\', '/')
    s4 = os.path.abspath(os.path.join(voice_dir, "scene_04_divided_command_en.mp3")).replace('\\', '/')
    s5 = os.path.abspath(os.path.join(voice_dir, "scene_05_massed_ram_en.mp3")).replace('\\', '/')
    
    temp_voice_concat = output_audio_path.replace(".wav", "_voice.wav")
    concat_txt = os.path.join(voice_dir, "prologue_concat_en.txt")
    with open(concat_txt, "w", encoding="utf-8") as f:
        f.write(f"file '{s3}'\nfile '{s4}'\nfile '{s5}'\n")
        
    subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_txt, "-c:a", "pcm_s16le", "-ar", "48000", "-ac", "2", temp_voice_concat], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    
    if os.path.exists(ambient_bed):
        mix_cmd = [
            "ffmpeg", "-y",
            "-i", temp_voice_concat,
            "-i", ambient_bed,
            "-filter_complex",
            "[0:a]volume=1.0[vox];[1:a]volume=0.25,atrim=0:175.92[bgm];[vox][bgm]amix=inputs=2:duration=first[aout];[aout]loudnorm=I=-14.0:TP=-1.0:LRA=11.0[final]",
            "-map", "[final]",
            "-c:a", "pcm_s16le",
            "-ar", "48000",
            "-ac", "2",
            output_audio_path
        ]
        subprocess.run(mix_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    else:
        subprocess.run(["ffmpeg", "-y", "-i", temp_voice_concat, "-c:a", "pcm_s16le", output_audio_path], check=True)
        
    if os.path.exists(temp_voice_concat):
        os.remove(temp_voice_concat)
    if os.path.exists(concat_txt):
        os.remove(concat_txt)
        
    print(f"[AUDIO] Clean English Prologue Audio Generated: {output_audio_path}")
    return output_audio_path

def render_cannae_prologue(output_mp4=None):
    workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
    if output_mp4 is None:
        output_mp4 = os.path.join(workspace, "episodes", "EP01_Cannae_216BC", "05_Renders", "cannae_prologue_master.mp4")
        
    os.makedirs(os.path.dirname(output_mp4), exist_ok=True)
    
    total_sec = 175.92
    total_frames = int(total_sec * FPS)
    
    temp_raw_video = output_mp4.replace(".mp4", "_raw.mp4")
    temp_audio_wav = output_mp4.replace(".mp4", "_audio.wav")
    
    # 1. Audio
    generate_prologue_audio(temp_audio_wav)
    
    # 2. Fast Engine
    engine = FastPrologueEngine(workspace)
    
    # 3. NVENC Render
    encoder = "h264_nvenc"
    ffmpeg_cmd = [
        "ffmpeg", "-y",
        "-f", "rawvideo",
        "-vcodec", "rawvideo",
        "-s", f"{WIDTH}x{HEIGHT}",
        "-pix_fmt", "bgr24",
        "-r", str(FPS),
        "-i", "-",
        "-c:v", encoder,
        "-preset", "p4",
        "-cq", "19",
        "-bf", "0",
        "-pix_fmt", "yuv420p",
        temp_raw_video
    ]
    
    print(f"[PROLOGUE] Rendering {total_frames} frames ({total_sec:.1f}s @ {FPS}fps) via {encoder}...")
    proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
    
    for frame_idx in range(total_frames):
        t_sec = frame_idx / float(FPS)
        frame = engine.render_frame_fast(t_sec, frame_idx=frame_idx)
        proc.stdin.write(frame.tobytes())
        
        if frame_idx % 500 == 0 or frame_idx == total_frames - 1:
            print(f"  -> Prologue Frame {frame_idx + 1}/{total_frames} ({(frame_idx + 1)/total_frames * 100:.1f}%)", flush=True)
            
    proc.stdin.close()
    proc.wait()
    
    # 4. Mux Video + Audio
    print("[PROLOGUE] Muxing video and audio tracks...")
    mux_cmd = [
        "ffmpeg", "-y",
        "-i", temp_raw_video,
        "-i", temp_audio_wav,
        "-c:v", "copy",
        "-c:a", "aac",
        "-b:a", "192k",
        "-shortest",
        output_mp4
    ]
    subprocess.run(mux_cmd, check=True)
    
    if os.path.exists(temp_raw_video):
        os.remove(temp_raw_video)
    if os.path.exists(temp_audio_wav):
        os.remove(temp_audio_wav)
        
    print(f"[SUCCESS] Historical Prologue Video Rendered: {output_mp4}")
    return output_mp4

if __name__ == "__main__":
    render_cannae_prologue()
