#!/usr/bin/env python3
"""Fail closed if a runtime-CLUT fit has timed out a required fabric branch."""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path


DEFAULT_OUTPUT = Path(
    "synth/de25_nano/top_psmct32_raster_demo/output_files"
)
REVISION = "de25_nano_psmct32_raster_demo_top"


def read(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except OSError as exc:
        raise SystemExit(f"FAIL: cannot read {path}: {exc}") from exc


def summary_value(text: str, label: str) -> int:
    match = re.search(rf"^{re.escape(label)}\s*:\s*([0-9,]+)\s*/", text, re.M)
    if not match:
        raise SystemExit(f"FAIL: missing '{label}' in fit summary")
    return int(match.group(1).replace(",", ""))


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
    args = parser.parse_args()

    fit_summary = args.output_dir / f"{REVISION}.fit.summary"
    fit_report = args.output_dir / f"{REVISION}.fit.rpt"
    sta_summary = args.output_dir / f"{REVISION}.sta.summary"
    summary = read(fit_summary)
    fit = read(fit_report)
    sta = read(sta_summary)

    failures: list[str] = []
    if "Fitter Status : Successful" not in summary:
        failures.append("fitter did not report success")

    alms = summary_value(summary, "Logic utilization (in ALMs)")
    rams = summary_value(summary, "Total RAM Blocks")
    # A 640x480 persistent-Z runtime scene normally consumes roughly 79% ALMs
    # and 74% M20Ks. These deliberately loose floors catch path pruning, not
    # healthy placement variation.
    if alms < 20_000:
        failures.append(f"only {alms:,} ALMs; runtime raster fabric appears pruned")
    if rams < 150:
        failures.append(f"only {rams} RAM blocks; runtime texture/Z fabric appears pruned")

    for name in ("u_demo|g_feeder", "u_texcache|tex_mem", "u_zc_emit|u_req"):
        if name not in fit:
            failures.append(f"required fitted hierarchy missing: {name}")

    for warning in (
        "feeder staging RAM dest EMPTY",
        "tex_mem dst matched 0 keepers",
    ):
        if warning in fit:
            failures.append(f"runtime SDC endpoint missing: {warning}")

    emif_setup = re.search(
        r"Type\s*:\s*Setup 'u_emif_lpddr4b\|iopll\|iopll_0_outclk0'\s*\n"
        r"Slack\s*:\s*([+-]?[0-9.]+)\s*\n"
        r"TNS\s*:\s*([+-]?[0-9.]+)",
        sta,
    )
    if not emif_setup:
        failures.append("missing 310 MHz EMIF setup summary")
    else:
        setup_slack, setup_tns = map(float, emif_setup.groups())
        if setup_slack < 0 or setup_tns < 0:
            failures.append(
                f"310 MHz setup fails: slack {setup_slack:+.3f} ns, TNS {setup_tns:+.3f} ns"
            )

    if failures:
        print("RUNTIME FABRIC FIT: FAIL")
        for failure in failures:
            print(f"- {failure}")
        return 1

    print(
        "RUNTIME FABRIC FIT: PASS "
        f"({alms:,} ALMs, {rams} RAM blocks, feeder/texture/Z present, 310 MHz setup met)"
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())
