#!/usr/bin/env python3
"""Fail-closed release gate for the isolated Base read-only bootstrap tooling.

This is deliberately separate from ci/release-gates.sh. It requires the global
strict V2 ABI gate to pass independently, while proving the narrowly scoped
read-only bootstrap closure does not reference the excluded legacy contract.
"""
from __future__ import annotations

import argparse
import ast
import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any

DEFAULT_ROOT = Path(__file__).resolve().parents[1]
MANIFEST_PATH = Path("ci/manifests/base-read-only-bootstrap-release-gate.json")
IMPORT = re.compile(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\']\s*;', re.MULTILINE)
ADDRESS = re.compile(r"^0x[0-9a-fA-F]{40}$")


class GateError(RuntimeError):
    pass


def fail(message: str) -> None:
    raise GateError(message)


def load_json(root: Path, relative: str) -> Any:
    path = root / relative
    if not path.is_file():
        fail(f"required evidence file is missing: {relative}")
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as error:
        fail(f"invalid JSON evidence in {relative}: {error}")


def sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def require(condition: bool, message: str) -> None:
    if not condition:
        fail(message)


def relative(root: Path, path: Path) -> str:
    try:
        return path.resolve().relative_to(root.resolve()).as_posix()
    except ValueError:
        fail(f"bootstrap dependency escapes repository: {path}")


def import_closure(root: Path, source_roots: list[str]) -> list[str]:
    contracts = (root / "v3-src/contracts").resolve()
    openzeppelin = (root / "v3-src/lib/openzeppelin-contracts").resolve()
    pending = [root / item for item in source_roots]
    visited: set[Path] = set()
    while pending:
        source = pending.pop()
        if source in visited:
            continue
        if not source.is_file():
            fail(f"bootstrap source root is missing: {relative(root, source)}")
        try:
            source.resolve().relative_to(contracts)
        except ValueError:
            try:
                source.resolve().relative_to(openzeppelin)
            except ValueError:
                fail(f"bootstrap source is outside reviewed contracts/dependency closure: {relative(root, source)}")
        visited.add(source)
        for specifier in IMPORT.findall(source.read_text(encoding="utf-8")):
            if specifier.startswith("."):
                target = (source.parent / specifier).resolve()
            elif specifier.startswith("openzeppelin-contracts/"):
                target = (openzeppelin / specifier.removeprefix("openzeppelin-contracts/")).resolve()
            else:
                fail(f"bootstrap dependency uses an unreviewed import in {relative(root, source)}: {specifier}")
            if target.suffix != ".sol":
                fail(f"bootstrap dependency is not Solidity source in {relative(root, source)}: {specifier}")
            try:
                target.relative_to(contracts)
            except ValueError:
                try:
                    target.relative_to(openzeppelin)
                except ValueError:
                    fail(f"bootstrap dependency escapes reviewed closure from {relative(root, source)}: {specifier}")
            pending.append(target)
    return sorted(relative(root, path) for path in visited)


def source_constant(path: Path, name: str) -> Any:
    module = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    for node in module.body:
        if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == name for target in node.targets):
            try:
                return ast.literal_eval(node.value)
            except (ValueError, TypeError) as error:
                fail(f"generator constant {name} is no longer a literal mapping: {error}")
    fail(f"generator constant {name} is missing")


def verify_global_strict_gate(root: Path, manifest: dict[str, Any]) -> None:
    global_gate = manifest.get("globalStrictGate")
    require(isinstance(global_gate, dict), "bootstrap manifest must declare globalStrictGate")
    release_entrypoint = global_gate.get("releaseEntrypoint")
    strict_script = global_gate.get("strictScript")
    invocation = global_gate.get("requiredInvocation")
    require(all(isinstance(value, str) for value in (release_entrypoint, strict_script, invocation)),
            "globalStrictGate fields must be strings")
    release_text = (root / release_entrypoint).read_text(encoding="utf-8")
    require(invocation in release_text, "full ci/release-gates.sh no longer invokes the strict V2 ABI gate exactly")
    result = subprocess.run(
        [sys.executable, strict_script, "--strict"], cwd=root, text=True, capture_output=True, check=False
    )
    output = result.stdout + result.stderr
    require(result.returncode == 0,
            "global strict V2 ABI gate failed; bootstrap gate cannot substitute for the full release gate\n" + output)


def verify_profile(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
    profile = load_json(root, manifest["bootstrapProfile"])
    require(profile.get("schema") == "vaultfire.base-v3-read-only-bootstrap-profile.v1", "bootstrap profile schema drift")
    require(profile.get("chain") == {"name": "base", "chainId": 8453}, "bootstrap profile must remain pinned to Base chain 8453")
    require(profile.get("status") == "bootstrap-draft", "bootstrap profile must remain incomplete bootstrap-draft")
    require(profile.get("mode") == "read-only-bootstrap", "bootstrap profile mode drift")
    require(profile.get("pilotReady") is False, "bootstrap profile falsely claims pilotReady")
    approval = profile.get("approval")
    require(isinstance(approval, dict) and approval.get("bootstrapDeploymentAuthorized") is False
            and approval.get("mainnetBroadcastApproval") is False and approval.get("independentReviewEvidence") == [],
            "bootstrap profile approval state is not fail-closed")
    roles = profile.get("roles")
    require(isinstance(roles, dict), "bootstrap profile roles are malformed")
    require(roles.get("governanceSigners") == [None] * 5 and roles.get("guardian") is None and roles.get("treasury") is None
            and roles.get("signerCustodyDisclosures") == [], "bootstrap profile invents governance roles")
    for role in ("attesters", "adjudicators", "designPartners", "pilotModules"):
        require(roles.get(role) == [], f"bootstrap profile invents {role}")
    controller = profile.get("controller")
    require(isinstance(controller, dict) and controller.get("deployEnabled") is False and controller.get("writeEnabled") is False
            and controller.get("globalCap") == "0" and controller.get("perPositionCap") == "0"
            and controller.get("designPartners") == [] and controller.get("modules") == [] and controller.get("positions") == [],
            "bootstrap profile enables controller writes or roles")
    factory = profile.get("bondFactory")
    require(isinstance(factory, dict) and factory.get("deployAllowed") is False and factory.get("deployedChildBonds") == [],
            "bootstrap profile enables bond factory deployment")
    deployment = profile.get("deployment")
    expected_labels = [item["label"] for item in manifest["bootstrapContracts"]]
    require(isinstance(deployment, dict) and deployment.get("signed") is False and deployment.get("broadcast") is False
            and deployment.get("addressDerivation") == "CREATE/deployer-nonce" and deployment.get("contracts") == expected_labels,
            "bootstrap deployment shape drift")
    return profile


def verify_generator_and_closure(root: Path, manifest: dict[str, Any], profile: dict[str, Any]) -> list[str]:
    generator_path = root / manifest["generator"]
    generator = generator_path.read_text(encoding="utf-8")
    contract_names = source_constant(generator_path, "CONTRACT_NAMES")
    internal_names = source_constant(generator_path, "INTERNAL_FACTORY_NAMES")
    expected = manifest["bootstrapContracts"]
    require(isinstance(contract_names, dict) and isinstance(internal_names, dict), "bootstrap generator mappings are malformed")
    require([contract_names.get(item["label"]) for item in expected] == [item["contract"] for item in expected],
            "bootstrap generator contract mapping drifted from reviewed release manifest")
    require(list(internal_names.items()) == [(item["label"], item["contract"]) for item in manifest["internalFactoryCreates"]],
            "bootstrap generator internal factory mapping drifted from reviewed release manifest")
    for token in ("initCodeKeccak256", "expectedRuntimeCodeKeccak256", "immutableReferences", "broadcast\": False", "pilotReady\": False"):
        require(token in generator, f"bootstrap generator is missing fail-closed code evidence token: {token}")
    roots = [item["source"] for item in expected] + [item["source"] for item in manifest["internalFactoryCreates"]]
    actual_closure = import_closure(root, roots)
    require(actual_closure == manifest["expectedSourceClosure"],
            "bootstrap dependency closure drifted; update requires explicit review\n"
            f"expected: {manifest['expectedSourceClosure']}\nactual: {actual_closure}")
    return actual_closure


def verify_bootstrap_guide(root: Path, manifest: dict[str, Any]) -> None:
    guide = (root / "pr8/pilot/BASE_READ_ONLY_BOOTSTRAP.md").read_text(encoding="utf-8")
    expected_contracts = [item["contract"] for item in manifest["bootstrapContracts"]]
    deployment_count_label = {5: "five"}.get(len(expected_contracts), str(len(expected_contracts)))
    for sequence, contract in enumerate(expected_contracts, start=1):
        require(
            f"{sequence}. `{contract}`" in guide,
            f"bootstrap guide is not bound to reviewed deployment {sequence}: {contract}",
        )
    require(
        f"{len(expected_contracts) + 1}. `" not in guide,
        "bootstrap guide lists more top-level deployments than the reviewed manifest",
    )
    require(
        "RewardProgramV3" not in guide
        and f"exactly the {deployment_count_label} predicted top-level addresses" in guide
        and f"the {deployment_count_label} top-level deployments" in guide,
        "bootstrap guide deployment count or excluded-module scope drifted from the reviewed manifest",
    )


def verify_excluded_legacy_absent(root: Path, manifest: dict[str, Any], closure: list[str]) -> None:
    scoped = closure + manifest["bootstrapScopeFiles"] + manifest["websiteBootstrapPublicationFiles"] + manifest["activationPathFiles"]
    require(len(scoped) == len(set(scoped)), "bootstrap exclusion scope contains duplicate files")
    for rel in scoped:
        path = root / rel
        require(path.is_file(), f"bootstrap exclusion scope file is missing: {rel}")
    needles: list[str] = []
    for item in manifest["excludedLegacyContracts"]:
        require(isinstance(item, dict) and isinstance(item.get("name"), str) and ADDRESS.fullmatch(item.get("baseAddress", "")),
                "excluded legacy contract declaration is malformed")
        needles.extend((item["name"].lower(), item["baseAddress"].lower()))
    for rel in scoped:
        text = (root / rel).read_text(encoding="utf-8").lower()
        for needle in needles:
            require(needle not in text, f"excluded legacy contract appears in bootstrap closure/scope: {rel} ({needle})")
    # Generated website addresses are global inventory, not bootstrap publication.  A bootstrap package/profile marker
    # anywhere in website source/public files would expand publication scope and must be reviewed explicitly.
    website = root / "pr9/website"
    for directory in (website / "src", website / "public"):
        if not directory.exists():
            continue
        for path in directory.rglob("*"):
            if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".json", ".md", ".html"}:
                text = path.read_text(encoding="utf-8", errors="strict").lower()
                require("vaultfire.base-v3-unsigned-bootstrap-package.v1" not in text
                        and "base_v3_read_only_bootstrap_profile" not in text,
                        f"unreviewed website bootstrap publication surface: {relative(root, path)}")


def find_record(records: list[dict[str, Any]], *, name: str, chain: str, address: str) -> dict[str, Any]:
    matches = [record for record in records if record.get("name") == name and record.get("chain") == chain
               and str(record.get("address", "")).lower() == address.lower()]
    require(len(matches) == 1, f"expected exactly one {name} {chain} {address} record, found {len(matches)}")
    return matches[0]


def functions_from_abi(abi: Any) -> set[str]:
    require(isinstance(abi, list), "canonical ABI must be an array")
    result: set[str] = set()
    for entry in abi:
        if isinstance(entry, dict) and entry.get("type") == "function" and isinstance(entry.get("name"), str):
            inputs = entry.get("inputs", [])
            require(isinstance(inputs, list) and all(isinstance(item, dict) and isinstance(item.get("type"), str) for item in inputs),
                    "canonical ABI function inputs are malformed")
            result.add(entry["name"] + "(" + ",".join(item["type"] for item in inputs) + ")")
    return result


def verify_v2_integrations(root: Path, manifest: dict[str, Any]) -> None:
    integration_manifest = load_json(root, manifest["v2IntegrationManifest"])
    require(integration_manifest.get("chain") == {"name": "base", "chainId": 8453}, "V2 integration manifest is not Base 8453")
    compatibility = integration_manifest.get("v2Compatibility")
    require(
        isinstance(compatibility, dict)
        and compatibility.get("preserved") is True
        and compatibility.get("defaultLiveSurface") == "V2"
        and compatibility.get("v3PaymentRoutesEnabled") is False,
        "V2 compatibility manifest state is malformed",
    )
    integration_values = compatibility.get("integrations")
    require(isinstance(integration_values, dict), "V2 integration manifest integrations are malformed")
    expected_keys = {item["manifestKey"] for item in manifest["v2Integrations"]}
    require(set(integration_values) == expected_keys, "V2 integration closure expanded or shrank")
    deployments = load_json(root, "baseline/deployments.json").get("contracts")
    bytecodes = load_json(root, "ci/manifests/v2-bytecode-manifest.json").get("records")
    fetched = load_json(root, "baseline/onchain_fetch_report.json").get("results")
    v2 = load_json(root, "legacy/v2/manifest.json")
    abi_records = v2.get("records")
    blocked = {item.get("contract") for item in v2.get("blockedDeployedContracts", []) if isinstance(item, dict)}
    require(isinstance(deployments, list) and isinstance(bytecodes, list) and isinstance(fetched, list) and isinstance(abi_records, list),
            "V2 evidence records are malformed")
    for item in manifest["v2Integrations"]:
        name, address = item["contract"], item["address"]
        require(integration_values.get(item["manifestKey"], "").lower() == address.lower(),
                f"V2 integration manifest drift for {item['manifestKey']}")
        require(name not in blocked, f"bootstrap V2 integration {name} is globally blocked")
        deployment = find_record(deployments, name=name, chain="base", address=address)
        require(deployment.get("chainId") == 8453 and deployment.get("version") == "v2", f"deployment provenance drift for {name}")
        onchain = deployment.get("onchain")
        require(isinstance(onchain, dict) and onchain.get("hasCode") is True
                and onchain.get("bytecodeSha256") == item["runtimeBytecodeSha256"]
                and onchain.get("bytecodeBytes") == item["runtimeBytecodeBytes"], f"deployment bytecode evidence drift for {name}")
        bytecode = find_record(bytecodes, name=name, chain="base", address=address)
        require(bytecode.get("runtimeBytecodeSha256") == item["runtimeBytecodeSha256"]
                and bytecode.get("runtimeBytecodeBytes") == item["runtimeBytecodeBytes"], f"pinned runtime manifest drift for {name}")
        fetch = find_record(fetched, name=name, chain="base", address=address)
        require(fetch.get("bytecode_sha256") == item["runtimeBytecodeSha256"]
                and fetch.get("bytecode_bytes") == item["runtimeBytecodeBytes"] and fetch.get("has_code") is True,
                f"frozen on-chain bytecode evidence drift for {name}")
        record = next((record for record in abi_records if record.get("contract") == name), None)
        require(isinstance(record, dict) and record.get("errors") == [], f"provenance-backed ABI evidence is unavailable for {name}")
        provenance = record.get("provenance")
        require(isinstance(provenance, dict) and provenance.get("kind") == "canonical-abi-json"
                and provenance.get("source") == item["abiProvenanceSource"], f"ABI provenance drift for {name}")
        provenance_path = root / item["abiProvenanceSource"]
        require(provenance_path.is_file() and sha256_file(provenance_path) == provenance.get("sourceSha256"),
                f"canonical ABI provenance hash evidence drift for {name}")
        abi_path = root / "legacy/v2" / item["abiPath"]
        require(abi_path.is_file() and sha256_file(abi_path) == record.get("sha256"),
                f"frozen canonical ABI hash evidence drift for {name}")
        abi_functions = functions_from_abi(load_json(root, str(abi_path.relative_to(root))))
        provenance_functions = functions_from_abi(load_json(root, str(provenance_path.relative_to(root))))
        listed = set(record.get("functions", []))
        required_methods = set(item["requiredMethods"])
        require(required_methods <= listed and required_methods <= abi_functions and required_methods <= provenance_functions,
                f"provenance-backed ABI/method evidence is missing for {name}: "
                f"{sorted(required_methods - listed | required_methods - abi_functions | required_methods - provenance_functions)}")


def run(root: Path) -> None:
    manifest = load_json(root, MANIFEST_PATH.as_posix())
    require(manifest.get("schema") == "vaultfire.base-v3-read-only-bootstrap-release-gate.v1", "bootstrap release gate schema drift")
    verify_global_strict_gate(root, manifest)
    profile = verify_profile(root, manifest)
    closure = verify_generator_and_closure(root, manifest, profile)
    verify_bootstrap_guide(root, manifest)
    verify_excluded_legacy_absent(root, manifest, closure)
    verify_v2_integrations(root, manifest)
    print(json.dumps({
        "verified": "Base read-only bootstrap release closure",
        "bootstrapSourceCount": len(closure),
        "v2IntegrationCount": len(manifest["v2Integrations"]),
        "excludedLegacyContracts": [item["name"] for item in manifest["excludedLegacyContracts"]],
        "globalStrictV2AbiGate": "verified independently",
        "pilotReady": False,
    }, indent=2))


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Validate the isolated Base read-only bootstrap release closure.")
    parser.add_argument("--root", type=Path, default=DEFAULT_ROOT, help="Repository root (test support only).")
    args = parser.parse_args(argv)
    try:
        run(args.root.resolve())
    except GateError as error:
        print(f"BOOTSTRAP RELEASE GATE BLOCKED: {error}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
