#!/usr/bin/env python3
"""Fail closed on unreviewed or explicitly uncontrolled Solidity loops.

This is intentionally a narrow static review gate, not a Solidity parser or a
claim of formal gas boundedness. Every source-level `for`/`while` loop must be
listed in the manifest with its controlling invariant. Entries marked
`uncontrolled` block a release until a contract change supplies a real bound.
"""
from __future__ import annotations

import json
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CONTRACTS = ROOT / "v3-src/contracts"
MANIFEST = ROOT / "ci/manifests/v3-loop-bounds.json"
LOOP = re.compile(r"\b(for|while)\s*\(")


def actual_loops() -> dict[tuple[str, int], str]:
    loops: dict[tuple[str, int], str] = {}
    for path in sorted(CONTRACTS.rglob("*.sol")):
        relative = path.relative_to(ROOT).as_posix()
        for line_number, line in enumerate(path.read_text().splitlines(), start=1):
            if LOOP.search(line):
                loops[(relative, line_number)] = " ".join(line.strip().split())
    return loops


def main() -> None:
    expected = json.loads(MANIFEST.read_text())["loops"]
    declared = {(item["file"], item["line"]): item for item in expected}
    actual = actual_loops()

    missing = sorted(set(actual) - set(declared))
    stale = sorted(set(declared) - set(actual))
    changed = sorted(key for key in set(actual) & set(declared) if actual[key] != declared[key]["statement"])
    if missing or stale or changed:
        messages = []
        if missing:
            messages.append("unreviewed loops: " + ", ".join(f"{path}:{line}" for path, line in missing))
        if stale:
            messages.append("stale loop manifest entries: " + ", ".join(f"{path}:{line}" for path, line in stale))
        if changed:
            messages.append("changed loop statements: " + ", ".join(f"{path}:{line}" for path, line in changed))
        raise SystemExit("; ".join(messages))

    uncontrolled = [item for item in expected if item["status"] != "bounded"]
    if uncontrolled:
        details = "; ".join(
            f"{item['file']}:{item['line']} ({item['bound']})" for item in uncontrolled
        )
        raise SystemExit(f"uncontrolled V3 loops block release: {details}")

    print(f"verified {len(actual)} V3 loops have reviewed bounds")


if __name__ == "__main__":
    main()
