"""
Is anything on this layout BLACK?  —  ask OBS for a picture of every source and say so.

A browser source that fails renders a black rectangle. So does a camera whose boat has no clip in the
current window, a source whose origin stopped listening, a page that never got its feed, and a scene
laid out for a canvas it is not being shown against. On the monitor they are the same picture, none
of them writes to any log, and the only way anyone has ever told them apart is by reading URLs out of
OBS by hand and screenshotting sources one at a time. That is this file.

    python -m v3.broadcast.live.obs_verify                    # every source in every scene
    python -m v3.broadcast.live.obs_verify --scene "Race cam"  # one scene, and its sources
    python -m v3.broadcast.live.obs_verify --save out/         # write the PNGs too

A source is reported BLANK when almost every pixel is within `--tol` of the darkest ones — a real
camera picture, a 3D render and even the fallback card all carry furniture, so anything with content
fails this test comfortably.

WHAT IT CANNOT DO, and why that is stated rather than worked around: OBS renders only what is
ACTIVE. A source sitting in a scene nobody is showing has never produced a frame, so it screenshots
black however healthy it is. `--activate` therefore takes each scene to program in turn, waits
`--settle` seconds for its pages to load, shoots it, and puts the original scene back — which is
honest but visible on the output, so it refuses to run while streaming or recording unless forced.
"""
from __future__ import annotations

import argparse
import base64
import io
import sys
import time
from pathlib import Path

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))


def blankness(png: bytes) -> tuple[float, int]:
    """(fraction of pixels that match the darkest level, number of distinct levels) for a PNG.

    Deliberately crude and dependency-light: a picture is "blank" when it is nearly all one dark
    tone. Uses Pillow when it is there and falls back to the byte histogram when it is not, because
    this has to run on a show machine, not only where the dev extras are installed.
    """
    try:
        from PIL import Image
    except ImportError:                       # no Pillow — a coarse but usable proxy
        levels = len(set(png[i] for i in range(0, len(png), 97)))
        return (1.0 if levels < 8 else 0.0), levels
    im = Image.open(io.BytesIO(png)).convert("L").resize((160, 90))
    px = list(im.getdata())
    hist: dict[int, int] = {}
    for v in px:
        hist[v >> 3] = hist.get(v >> 3, 0) + 1     # 32 buckets: ignore encoder noise
    top = max(hist.values())
    darkest = min(hist)
    return (top / len(px) if darkest <= 2 else 0.0), len(hist)


def shoot(client, name: str, width: int, height: int) -> bytes | None:
    try:
        r = client.get_source_screenshot(name, "png", width, height, 90)
    except Exception:                          # noqa: BLE001 — a source that cannot be rendered
        return None
    data = getattr(r, "image_data", "") or ""
    if "," not in data:
        return None
    return base64.b64decode(data.split(",", 1)[1])


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
    ap.add_argument("--obs-host", default="localhost")
    ap.add_argument("--obs-port", type=int, default=4455)
    ap.add_argument("--obs-password", default="")
    ap.add_argument("--scene", action="append", help="only these scenes (repeatable); default all")
    ap.add_argument("--save", type=Path, help="also write each PNG here")
    ap.add_argument("--tol", type=float, default=0.98,
                    help="fraction of one dark tone above which a source counts as blank (0.98)")
    ap.add_argument("--activate", action="store_true",
                    help="take each scene to program before shooting it, so its sources actually "
                         "render. Visible on the output — refused while live unless --force")
    ap.add_argument("--settle", type=float, default=12.0,
                    help="seconds to let a scene's pages load once activated (--activate)")
    ap.add_argument("--force", action="store_true", help="--activate even while streaming/recording")
    a = ap.parse_args(argv)

    from director_obs import OBSController
    obs = OBSController(host=a.obs_host, port=a.obs_port, password=a.obs_password).connect()
    cl = obs.client

    if a.activate and not a.force:
        live = (getattr(cl.get_stream_status(), "output_active", False)
                or getattr(cl.get_record_status(), "output_active", False))
        if live:
            print("refusing to --activate while streaming/recording — it changes the program scene "
                  "(pass --force if you mean it)")
            return 2

    scenes = [s["sceneName"] for s in (cl.get_scene_list().scenes or [])]
    if a.scene:
        want = set(a.scene)
        scenes = [s for s in scenes if s in want]
    original = cl.get_scene_list().current_program_scene_name
    if a.save:
        a.save.mkdir(parents=True, exist_ok=True)

    blanks: list[str] = []
    checked = 0
    try:
        for scene in scenes:
            if a.activate and scene != cl.get_scene_list().current_program_scene_name:
                cl.set_current_program_scene(scene)
                time.sleep(a.settle)
            targets = [scene] + [it["sourceName"] for it in (cl.get_scene_item_list(scene).scene_items or [])]
            for name in targets:
                png = shoot(cl, name, 640, 360)
                checked += 1
                if png is None:
                    print(f"  {scene:<18} {name:<26} NO PICTURE (source cannot be rendered)")
                    blanks.append(f"{scene}/{name}")
                    continue
                frac, levels = blankness(png)
                verdict = "BLANK" if frac >= a.tol else "ok"
                if verdict == "BLANK":
                    blanks.append(f"{scene}/{name}")
                print(f"  {scene:<18} {name:<26} {verdict:<5} ({frac * 100:.0f}% one tone, {levels} levels)")
                if a.save:
                    (a.save / f"{scene}__{name}".replace(":", "_").replace(" ", "_")).with_suffix(".png").write_bytes(png)
    finally:
        if a.activate and original:
            cl.set_current_program_scene(original)

    print(f"\n{checked} source(s) checked, {len(blanks)} blank"
          + (f": {', '.join(blanks)}" if blanks else ""))
    if blanks and not a.activate:
        print("NOTE: OBS only renders ACTIVE sources — a scene nobody is showing shoots black however "
              "healthy it is. Re-run with --activate to judge those fairly.")
    obs.disconnect()
    return 1 if blanks else 0


if __name__ == "__main__":
    raise SystemExit(main())
