"""Рендер моушн-ролика прямо на сервере: кадры через Chrome, сборка через ffmpeg.

На ПК Кирилла есть GPU и pm_final_v.cmd, но когда его нет за компом, ролик всё
равно нужен. Здесь тот же принцип: страница рисует кадр как чистую функцию
времени (window.__seek), мы снимаем кадры и клеим.

    python3 tools/render_server.py --v lime --fps 30 --dur 30 --vo charlie

Аудио: музыка проекта плюс выбранная озвучка. Голос идёт громче музыки.
"""
import argparse
import os
import subprocess
import sys
import time
from pathlib import Path

from playwright.sync_api import sync_playwright

ROOT = Path(__file__).resolve().parent.parent
FRAMES = ROOT / "out" / "frames"
OUT = ROOT / "out" / "final"
PAGE = "http://127.0.0.1:3070/web_v/index.html"


def grab(variant: str, fps: int, dur: float) -> int:
    FRAMES.mkdir(parents=True, exist_ok=True)
    for old in FRAMES.glob("*.png"):
        old.unlink()

    total = int(fps * dur)
    started = time.time()
    with sync_playwright() as p:
        browser = p.chromium.launch(args=["--no-sandbox", "--enable-unsafe-swiftshader"])
        page = browser.new_page(viewport={"width": 1920, "height": 1080})
        page.goto(f"{PAGE}?v={variant}", wait_until="load")
        page.wait_for_function("window.__ready === true", timeout=90000)

        for i in range(total):
            page.evaluate(f"window.__seek({i / fps})")
            page.screenshot(path=str(FRAMES / f"f_{i:05d}.png"))
            if i % 60 == 0:
                done = i + 1
                speed = done / max(time.time() - started, 0.1)
                left = (total - done) / max(speed, 0.01)
                print(f"  кадр {done}/{total}, осталось ~{left/60:.1f} мин", flush=True)
        page.close()
        browser.close()
    return total


def assemble(variant: str, fps: int, vo: str, music: str, dur: float = 30.0) -> Path:
    OUT.mkdir(parents=True, exist_ok=True)
    silent = OUT / f"v3_{variant}_silent.mp4"
    final = OUT / f"pk_motion_v3_{variant}.mp4"

    subprocess.run(
        ["ffmpeg", "-v", "error", "-y", "-framerate", str(fps), "-i", str(FRAMES / "f_%05d.png"),
         "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", "-preset", "medium", str(silent)],
        check=True,
    )

    vo_path = ROOT / "audio" / "vo" / f"vo_{vo}.mp3"
    music_path = ROOT / "audio" / f"music_{music}.mp3"
    if not vo_path.exists():
        raise SystemExit(f"нет озвучки: {vo_path}")

    # Музыка тише голоса и слегка приседает под него, иначе слова тонут.
    subprocess.run(
        ["ffmpeg", "-v", "error", "-y", "-i", str(silent), "-i", str(vo_path), "-i", str(music_path),
         "-filter_complex",
         # Голос нужен дважды: как управляющий сигнал для приседания музыки и как сам голос.
         # Без asplit ffmpeg падает: один выход фильтра нельзя подключить к двум входам.
         "[1:a]volume=1.0,asplit=2[v1][v2];[2:a]volume=0.20[m];"
         "[m][v1]sidechaincompress=threshold=0.04:ratio=8:attack=20:release=320[duck];"
         "[duck][v2]amix=inputs=2:duration=longest:dropout_transition=0[a]",
         "-map", "0:v", "-map", "[a]", "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-t", f"{dur:.3f}", str(final)],
        check=True,
    )
    return final


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--v", default="lime")
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--dur", type=float, default=30.0)
    ap.add_argument("--vo", default="charlie")
    ap.add_argument("--music", default="C")
    ap.add_argument("--skip-frames", action="store_true", help="склеить уже снятые кадры")
    args = ap.parse_args()

    if not args.skip_frames:
        print(f"Снимаю кадры: стиль {args.v}, {args.fps} fps, {args.dur} c")
        grab(args.v, args.fps, args.dur)
    final = assemble(args.v, args.fps, args.vo, args.music, args.dur)
    size = final.stat().st_size / 1024 / 1024
    print(f"ГОТОВО: {final} ({size:.1f} МБ)")


if __name__ == "__main__":
    main()
