#!/usr/bin/env python3
"""Real renderer validation for an Ontology Semantic Atlas (headless Chrome + Mermaid).

Extracts every ```mermaid block from the atlas, checks that each starts with the ELK frontmatter, renders
each block with mermaid.render() under securityLevel 'strict' in headless Chrome for every --mermaid
bundle given, and tallies REAL results (an <svg> was produced AND the page's error element is empty).
When two configurations are given, it also reports which blocks lay out identically.
On Mermaid 12+, pass --layout dagre=dagre to force a true dagre comparison.

Usage:
  render_check.py <atlas.md> --mermaid elk=path/to/mermaid-elk.bundle.js --mermaid dagre=path/to/mermaid.min.js
                  --layout dagre=dagre [--work DIR] [--json render-report.json] [--shots b00,b05] [--jobs 6]

Exit codes: 0 all blocks rendered in every configuration; 2 some block failed; 3 no headless Chrome found
(renderer validation NOT executed — say so, do not claim it).
"""
from __future__ import annotations
import argparse, json, re, shutil, subprocess, sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

ap = argparse.ArgumentParser()
ap.add_argument("atlas", type=Path)
ap.add_argument("--mermaid", action="append", required=True, help="tag=path/to/bundle.js (repeatable)")
ap.add_argument("--layout", action="append", default=[], help="tag=layout override for comparison, e.g. dagre=dagre")
ap.add_argument("--work", type=Path, default=Path("render-check"))
ap.add_argument("--json", type=Path, default=None)
ap.add_argument("--shots", default="")
ap.add_argument("--jobs", type=int, default=6)
a = ap.parse_args()
chrome = next((shutil.which(x) for x in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome") if shutil.which(x)), None)
if not chrome:
    print("No headless Chrome/Chromium found — renderer validation NOT executed.", file=sys.stderr); sys.exit(3)
W = a.work.resolve(); W.mkdir(parents=True, exist_ok=True)
text = a.atlas.read_text(encoding="utf-8")
blocks = re.findall(r"```mermaid\n(.*?)```", text, re.S)
ELK = "---\nconfig:\n  layout: elk\n---\n"
no_elk = [i for i, b in enumerate(blocks) if not b.startswith(ELK)]
bundles = []
for spec in a.mermaid:
    tag, _, path = spec.partition("=")
    if not path: tag, path = f"cfg{len(bundles)}", tag
    bundles.append((tag, Path(path).resolve()))
layout_overrides = {}
for spec in a.layout:
    tag, sep, layout = spec.partition("=")
    if not sep or not tag or not layout:
        ap.error(f"Invalid --layout value: {spec!r}; expected tag=layout")
    layout_overrides[tag] = layout
CHROME = [chrome, "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars", "--virtual-time-budget=90000"]
def page(b, js):
    return f'''<!doctype html><html><head><meta charset="utf-8"><script src="file://{js}"></script>
<style>body{{margin:0;background:#fff}}</style></head><body><pre id="m" style="margin:0"></pre><pre id="log"></pre>
<script>(async()=>{{const src={json.dumps(b)};
mermaid.initialize({{startOnLoad:false,securityLevel:'strict',maxTextSize:900000,maxEdges:5000,logLevel:'warn'}});
try{{const r=await mermaid.render('s',src);document.getElementById('m').innerHTML=r.svg;}}catch(e){{document.getElementById('log').textContent='RENDER ERROR: '+e.message;}}
}})();</script></body></html>'''
jobs = []
for tag, js in bundles:
    for i, b in enumerate(blocks):
        rendered_source = b
        if tag in layout_overrides:
            rendered_source = re.sub(r"(?m)^(\s*layout:\s*)\S+\s*$", rf"\g<1>{layout_overrides[tag]}", b, count=1)
        f = W / f"single_{tag}_b{i:02d}.html"; f.write_text(page(rendered_source, js), encoding="utf-8"); jobs.append((tag, i, f))
def run(job):
    tag, i, f = job
    try: out = subprocess.run(CHROME + ["--dump-dom", f"file://{f}"], capture_output=True, text=True, timeout=240).stdout
    except subprocess.TimeoutExpired: out = ""
    (W / f"dom_{tag}_b{i:02d}.html").write_text(out, encoding="utf-8")
    ok = "<svg" in out and '<pre id="log"></pre>' in out
    err = re.search(r'id="log">([^<]*)', out)
    return tag, i, ok, (err.group(1)[:300] if err and err.group(1) else ("no output" if not out else ""))
with ThreadPoolExecutor(max_workers=a.jobs) as ex: res = list(ex.map(run, jobs))
report = {"atlas": str(a.atlas), "total_blocks": len(blocks), "blocks_without_elk_frontmatter": no_elk, "layout_overrides": layout_overrides, "configs": {}, "identical_layouts": None, "different_layout_count": None}
exit_code = 0
for tag, _ in bundles:
    rows = [r for r in res if r[0] == tag]
    fails = [{"block": f"b{i:02d}", "error": e} for _, i, ok, e in rows if not ok]
    report["configs"][tag] = {"rendered": sum(1 for r in rows if r[2]), "total": len(rows), "failures": fails}
    print(f"{tag}: {report['configs'][tag]['rendered']}/{len(rows)} rendered; failures: {fails}")
    if fails: exit_code = 2
if len(bundles) >= 2:
    t0, t1 = bundles[0][0], bundles[1][0]; same = []
    for i in range(len(blocks)):
        pa = re.findall(r'transform="translate\([^"]*\)"', (W / f"dom_{t0}_b{i:02d}.html").read_text(encoding="utf-8"))
        pb = re.findall(r'transform="translate\([^"]*\)"', (W / f"dom_{t1}_b{i:02d}.html").read_text(encoding="utf-8"))
        if pa == pb: same.append(f"b{i:02d}")
    report["identical_layouts"] = same
    report["different_layout_count"] = len(blocks) - len(same)
    print(f"identical layouts under {t0} and {t1} (expected only for non-graph diagrams such as timelines): {same}")
if no_elk: print(f"WARNING: blocks without ELK frontmatter: {no_elk}")
if a.shots:
    (W / "shots").mkdir(exist_ok=True); tag = bundles[0][0]
    for bid in a.shots.split(","):
        subprocess.run(CHROME + ["--window-size=2200,1600", f"--screenshot={W/'shots'/(bid+'.png')}", f"file://{W/('single_'+tag+'_'+bid+'.html')}"], capture_output=True, timeout=240)
    print("screenshots in", W / "shots")
if a.json: a.json.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
sys.exit(exit_code)
