#!/usr/bin/env python3
"""Burn an ASS track, proving the bundled fonts were the ones actually used.

Two guards, both learned from silent failures:

  * `libass` does not recurse into sub-directories of `fontsdir`. Pointed at
    `assets/fonts` (which holds one folder per family) it loads nothing and falls
    back to a system font without a single warning. This refuses to start unless
    the directory contains font files directly.

  * Even with a correct directory, an ASS style naming a family that is not in the
    pack is silently substituted. The render runs at verbose level, libass's own
    `fontselect:` decisions are read back, and any substitution fails the step —
    after the render, so the log shows exactly which style was wrong.

Encoder choice comes from `render_core`, so it is probed once and shared with every
other render step instead of each script guessing.
"""
from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from render_core import PROFILES, capabilities, decode_arguments, output_arguments  # noqa: E402
from skill_config import FONT_PACK, emit, ffmpeg_filter_path, utf8_stdout  # noqa: E402

FONTSELECT = re.compile(r"fontselect: \(([^)]*)\) -> ([^,\n]+)")


def font_pack_faces(fonts_dir: Path) -> set[str]:
    """Normalised names of the faces libass can see in this directory."""
    names: set[str] = set()
    for path in list(fonts_dir.glob("*.ttf")) + list(fonts_dir.glob("*.otf")):
        names.add(path.stem.casefold().replace(" ", "").replace("-", "").replace("_", ""))
    return names


def styles_in(ass_path: Path) -> list[str]:
    families: list[str] = []
    for line in ass_path.read_text(encoding="utf-8-sig", errors="replace").splitlines():
        if line.startswith("Style:"):
            parts = line[len("Style:"):].split(",")
            if len(parts) >= 2:
                families.append(parts[1].strip())
    return list(dict.fromkeys(families))


def main() -> None:
    utf8_stdout()
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("input")
    parser.add_argument("captions")
    parser.add_argument("output")
    parser.add_argument("--fonts-dir", default=str(FONT_PACK),
                        help="Flat directory of font files (default: assets/fonts/all)")
    parser.add_argument("--profile", default="delivery", choices=list(PROFILES))
    parser.add_argument("--codec", default="h264", choices=["h264", "hevc"])
    parser.add_argument("--cpu", action="store_true", help="Force the software encoder")
    parser.add_argument("--hwaccel", action="store_true",
                        help="Use GPU decode if it probed faster than the CPU path")
    parser.add_argument("--audio-bitrate", default="192k")
    parser.add_argument("--allow-font-substitution", action="store_true",
                        help="Do not fail when libass falls back to a system font")
    parser.add_argument("--log", help="Write the full ffmpeg log here")
    args = parser.parse_args()

    source = Path(args.input).resolve()
    captions = Path(args.captions).resolve()
    fonts = Path(args.fonts_dir).resolve()
    output = Path(args.output).resolve()
    for required in (source, captions, fonts):
        if not required.exists():
            raise SystemExit(f"Missing input: {required}")

    direct = font_pack_faces(fonts)
    if not direct:
        nested = len(list(fonts.rglob("*.ttf")))
        raise SystemExit(
            f"{fonts} has no font files directly inside"
            + (f" ({nested} found in sub-directories, which libass ignores). "
               f"Run: python scripts/install_fonts.py and pass assets/fonts/all"
               if nested else ". Run: python scripts/install_fonts.py")
        )

    caps = capabilities()
    encoder_arguments, selection = output_arguments(
        args.codec, args.profile, prefer_gpu=not args.cpu,
        audio_bitrate=args.audio_bitrate, caps=caps,
    )
    output.parent.mkdir(parents=True, exist_ok=True)
    video_filter = (
        f"ass=filename='{ffmpeg_filter_path(captions)}':"
        f"fontsdir='{ffmpeg_filter_path(fonts)}'"
    )
    command = [
        "ffmpeg", "-y", "-v", "verbose", "-nostdin",
        *decode_arguments(args.hwaccel, caps),
        "-i", str(source),
        "-vf", video_filter,
        *encoder_arguments,
        str(output),
    ]
    completed = subprocess.run(command, capture_output=True, text=True, errors="replace")
    log = (completed.stderr or "") + (completed.stdout or "")
    if args.log:
        Path(args.log).parent.mkdir(parents=True, exist_ok=True)
        Path(args.log).write_text(log, encoding="utf-8")

    decisions = []
    substituted = []
    for asked, chosen in FONTSELECT.findall(log):
        family = asked.split(",")[0].strip()
        picked = chosen.strip()
        key = picked.casefold().replace(" ", "").replace("-", "").replace("_", "")
        from_pack = any(key.startswith(name) or name.startswith(key) for name in direct if name)
        decisions.append({"requested": family, "resolved": picked, "from_pack": from_pack})
        if not from_pack:
            substituted.append(f"{family} -> {picked}")

    if completed.returncode:
        emit({
            "status": "render_failed",
            "returncode": completed.returncode,
            "encoder": selection,
            "tail": log.strip()[-1500:],
        })
        raise SystemExit(completed.returncode)

    report = {
        "status": "ok" if not substituted or args.allow_font_substitution else "font_substituted",
        "input": str(source),
        "captions": str(captions),
        "output": str(output),
        "fonts_dir": str(fonts),
        "faces_visible_to_libass": len(direct),
        "styles_in_ass": styles_in(captions),
        "font_decisions": decisions,
        "substituted": substituted,
        "encoder": selection,
        "audio": f"aac {args.audio_bitrate} 48kHz stereo",
        "gpu_note": None if selection["hardware"] else (caps.get("rejected") or [None])[0],
    }
    emit(report)
    if substituted and not args.allow_font_substitution:
        print(
            "error: libass подставил системный шрифт вместо пака — субтитры "
            "выглядят не так, как в превью. Проверь Fontname в ASS и вывод "
            "scripts/font_audit.py",
            file=sys.stderr,
        )
        raise SystemExit(3)


if __name__ == "__main__":
    main()
