#!/usr/bin/env python3
"""Offline validator for a Base-mainnet read-only V3 bootstrap profile.

A deployable bootstrap has exactly five named governance signer addresses, a
separate guardian and treasury, but intentionally no operating roles. It is not
a pilot activation and cannot claim readiness. This program reads JSON only; it
never reads keys, signs, deploys, contacts an RPC, or broadcasts.
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any

BASE_CHAIN_ID = 8453
BOOTSTRAP_CONTRACT_SOURCE_COMMIT = None
ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
BOOTSTRAP_DEPLOYMENTS = (
    "governance", "controller", "consensus", "bondFactory",
)
FOUNDER_CUSTODY = "founder-controlled-non-independent"
INDEPENDENT_CUSTODY = "independent-partner"


class BootstrapProfileValidationError(ValueError):
    """Raised for every incomplete or potentially enabling bootstrap profile."""


def _require(errors: list[str], condition: bool, message: str) -> None:
    if not condition:
        errors.append(message)


def _address(value: Any, label: str, errors: list[str], *, required: bool = True) -> None:
    if value is None and not required:
        return
    _require(errors, isinstance(value, str) and ADDRESS_RE.fullmatch(value) is not None, f"{label} must be an EVM address")
    if isinstance(value, str) and ADDRESS_RE.fullmatch(value):
        _require(errors, int(value[2:], 16) != 0, f"{label} must not be the zero address")


def _empty_list(value: Any, label: str, errors: list[str]) -> None:
    _require(errors, value == [], f"{label} must remain an empty list at bootstrap")


def _validate_deployable_roles(profile: dict[str, Any], errors: list[str]) -> None:
    roles = profile.get("roles")
    _require(errors, isinstance(roles, dict), "roles must be an object")
    roles = roles if isinstance(roles, dict) else {}
    signers = roles.get("governanceSigners")
    _require(errors, isinstance(signers, list) and len(signers) == 5, "bootstrap requires exactly five governance signer addresses")
    if isinstance(signers, list):
        for index, signer in enumerate(signers):
            _address(signer, f"roles.governanceSigners[{index}]", errors)
        normalized = [item.lower() for item in signers if isinstance(item, str)]
        _require(errors, len(normalized) == 5 and len(set(normalized)) == 5, "governance signer addresses must be distinct")
    guardian = roles.get("guardian")
    treasury = roles.get("treasury")
    _address(guardian, "roles.guardian", errors)
    _address(treasury, "roles.treasury", errors)
    addresses = [item.lower() for item in signers if isinstance(item, str)] if isinstance(signers, list) else []
    if isinstance(guardian, str):
        _require(errors, guardian.lower() not in addresses, "guardian must be separate from every governance signer")
    if isinstance(treasury, str):
        _require(errors, treasury.lower() not in addresses, "treasury must be separate from every governance signer")
    if isinstance(guardian, str) and isinstance(treasury, str):
        _require(errors, guardian.lower() != treasury.lower(), "treasury must be separate from guardian")

    disclosures = roles.get("signerCustodyDisclosures")
    _require(errors, isinstance(disclosures, list) and len(disclosures) == 5,
             "bootstrap requires one truthful custody disclosure for each signer")
    disclosed: set[str] = set()
    if isinstance(disclosures, list):
        for index, record in enumerate(disclosures):
            _require(errors, isinstance(record, dict), f"roles.signerCustodyDisclosures[{index}] must be an object")
            if not isinstance(record, dict):
                continue
            account = record.get("address")
            _address(account, f"roles.signerCustodyDisclosures[{index}].address", errors)
            custody = record.get("custody")
            _require(errors, custody in (FOUNDER_CUSTODY, INDEPENDENT_CUSTODY),
                     f"roles.signerCustodyDisclosures[{index}].custody is not an allowed truthful classification")
            if isinstance(account, str):
                disclosed.add(account.lower())
            if custody == FOUNDER_CUSTODY:
                _require(errors, record.get("nonIndependent") is True,
                         f"roles.signerCustodyDisclosures[{index}] must explicitly label founder custody non-independent")
                _require(errors, record.get("rotationRequired") is True,
                         f"roles.signerCustodyDisclosures[{index}] must require rotation as partners join")
            if custody == INDEPENDENT_CUSTODY:
                _require(errors, record.get("nonIndependent") is False,
                         f"roles.signerCustodyDisclosures[{index}] cannot label independent custody non-independent")
    _require(errors, disclosed == set(addresses), "custody disclosures must cover exactly the five governance signers")

    for name in ("attesters", "adjudicators", "designPartners", "pilotModules"):
        _empty_list(roles.get(name), f"roles.{name}", errors)


def _validate_draft_roles(profile: dict[str, Any], errors: list[str]) -> None:
    roles = profile.get("roles")
    _require(errors, isinstance(roles, dict), "roles must be an object")
    roles = roles if isinstance(roles, dict) else {}
    _require(errors, roles.get("governanceSigners") == [None] * 5,
             "bootstrap-draft must keep five governance signer slots unset")
    _require(errors, roles.get("guardian") is None, "bootstrap-draft guardian must remain unset")
    _require(errors, roles.get("treasury") is None, "bootstrap-draft treasury must remain unset")
    _empty_list(roles.get("signerCustodyDisclosures"), "roles.signerCustodyDisclosures", errors)
    for name in ("attesters", "adjudicators", "designPartners", "pilotModules"):
        _empty_list(roles.get(name), f"roles.{name}", errors)


def validate_bootstrap_profile(profile: Any, *, deployable: bool = False) -> None:
    errors: list[str] = []
    _require(errors, isinstance(profile, dict), "profile root must be an object")
    if not isinstance(profile, dict):
        raise BootstrapProfileValidationError("; ".join(errors))

    _require(errors, profile.get("schema") == "vaultfire.base-v3-read-only-bootstrap-profile.v1", "unsupported bootstrap profile schema")
    _require(errors, profile.get("sourceCommit") is None,
             "checked-in bootstrap source commit must remain unset pending deterministic finalization")
    chain = profile.get("chain")
    _require(errors, isinstance(chain, dict) and chain.get("name") == "base" and chain.get("chainId") == BASE_CHAIN_ID,
             "bootstrap profile must target Base mainnet chain ID 8453")
    _require(errors, profile.get("mode") == "read-only-bootstrap", "bootstrap mode must be read-only-bootstrap")
    status = profile.get("status")
    _require(errors, status in ("bootstrap-draft", "deployable-read-only"),
             "bootstrap status must be bootstrap-draft or deployable-read-only")
    _require(errors, profile.get("pilotReady") is False, "bootstrap profile must never claim pilotReady")

    consensus = profile.get("consensus")
    _require(errors, isinstance(consensus, dict), "consensus must be an object")
    consensus = consensus if isinstance(consensus, dict) else {}
    _require(errors, consensus.get("threshold") == 3 and consensus.get("minimumThreshold") == 2,
             "ThresholdConsensusV3 bootstrap must deploy at threshold 3 with its minimum threshold 2")
    _empty_list(consensus.get("authorizedAttesters"), "consensus.authorizedAttesters", errors)

    controller = profile.get("controller")
    _require(errors, isinstance(controller, dict), "controller must be an object")
    controller = controller if isinstance(controller, dict) else {}
    _require(errors, controller.get("deployEnabled") is False, "controller.deployEnabled must remain false")
    _require(errors, controller.get("writeEnabled") is False, "controller.writeEnabled must remain false")
    _require(errors, controller.get("globalCap") == "0" and controller.get("perPositionCap") == "0",
             "controller caps must remain zero before pilot activation")
    _require(
        errors,
        controller.get("maxGlobalCap") == "400000000000000000"
        and controller.get("maxPerPositionCap") == "200000000000000000",
        "controller immutable maximum caps must bind the reviewed 0.40/0.20 ETH ceilings",
    )
    for name in ("designPartners", "modules", "positions"):
        _empty_list(controller.get(name), f"controller.{name}", errors)

    factory = profile.get("bondFactory")
    _require(errors, isinstance(factory, dict), "bondFactory must be an object")
    factory = factory if isinstance(factory, dict) else {}
    _require(errors, factory.get("deployAllowed") is False, "bondFactory.deployAllowed must remain false")
    _empty_list(factory.get("deployedChildBonds"), "bondFactory.deployedChildBonds", errors)
    native_caps = factory.get("immutableNativeCapsWei")
    _require(errors, isinstance(native_caps, dict), "bondFactory.immutableNativeCapsWei must be an object")
    native_caps = native_caps if isinstance(native_caps, dict) else {}
    cap_names = (
        "partnershipParty", "partnershipBond", "partnershipAggregate", "accountabilityBond", "accountabilityAggregate",
    )
    cap_values: dict[str, int] = {}
    for name in cap_names:
        raw = native_caps.get(name)
        _require(errors, isinstance(raw, str) and raw.isdecimal() and int(raw) > 0,
                 f"bondFactory.immutableNativeCapsWei.{name} must be a positive decimal wei string")
        if isinstance(raw, str) and raw.isdecimal() and int(raw) > 0:
            cap_values[name] = int(raw)
    if len(cap_values) == len(cap_names):
        _require(errors, cap_values["partnershipParty"] <= cap_values["partnershipBond"] <= cap_values["partnershipAggregate"],
                 "partnership native caps must satisfy party <= bond <= aggregate")
        _require(errors, cap_values["accountabilityBond"] <= cap_values["accountabilityAggregate"],
                 "accountability native caps must satisfy bond <= aggregate")

    discovery = profile.get("discoveryAndPayments")
    _require(errors, isinstance(discovery, dict), "discoveryAndPayments must be an object")
    discovery = discovery if isinstance(discovery, dict) else {}
    for key in ("x402Enabled", "bazaarDiscoveryEnabled", "paymentRoutesEnabled", "publicWritesEnabled", "publicValueAcceptanceEnabled"):
        _require(errors, discovery.get(key) is False, f"discoveryAndPayments.{key} must remain false")
    _empty_list(discovery.get("x402Routes"), "discoveryAndPayments.x402Routes", errors)
    _empty_list(discovery.get("bazaarListings"), "discoveryAndPayments.bazaarListings", errors)

    value = profile.get("valueAcceptance")
    _require(errors, isinstance(value, dict), "valueAcceptance must be an object")
    value = value if isinstance(value, dict) else {}
    _require(errors, value.get("deploymentTransactionValueWei") == "0", "bootstrap deployment transaction value must be zero")
    _empty_list(value.get("fundingCalls"), "valueAcceptance.fundingCalls", errors)
    _empty_list(value.get("payees"), "valueAcceptance.payees", errors)

    deployment = profile.get("deployment")
    _require(errors, isinstance(deployment, dict), "deployment must be an object")
    deployment = deployment if isinstance(deployment, dict) else {}
    _require(errors, deployment.get("broadcast") is False and deployment.get("signed") is False,
             "bootstrap deployment package must be unsigned and non-broadcast")
    _require(errors, deployment.get("addressDerivation") == "CREATE/deployer-nonce", "address derivation must be CREATE/deployer-nonce")
    _require(errors, deployment.get("contracts") == list(BOOTSTRAP_DEPLOYMENTS),
             "bootstrap deployment contract order is fixed and must exclude public value modules")

    activation = profile.get("partnerPilotTransition")
    _require(errors, isinstance(activation, dict), "partnerPilotTransition must be an object")
    activation = activation if isinstance(activation, dict) else {}
    _require(errors, activation.get("requiredThreshold") == "3-of-5" and activation.get("minimumTimelockSeconds") == 86400,
             "later partner pilot activation must require a 3-of-5 proposal and at least a 24-hour timelock")
    _require(errors, activation.get("packageStatus") == "requirements-only-not-authorized",
             "transition package must remain requirements-only and unauthorized at bootstrap")
    _empty_list(activation.get("signedApprovals"), "partnerPilotTransition.signedApprovals", errors)

    approval = profile.get("approval")
    _require(errors, isinstance(approval, dict), "approval must be an object")
    approval = approval if isinstance(approval, dict) else {}
    if status == "bootstrap-draft":
        _require(errors, approval.get("bootstrapDeploymentAuthorized") is False,
                 "bootstrap-draft cannot authorize deployment")
        _validate_draft_roles(profile, errors)
    else:
        _require(errors, approval.get("bootstrapDeploymentAuthorized") is True,
                 "deployable-read-only profile requires explicit bootstrap deployment authorization")
        _validate_deployable_roles(profile, errors)
    _require(errors, approval.get("mainnetBroadcastApproval") is False,
             "bootstrap profile may not authorize signing or broadcasting")
    _empty_list(approval.get("independentReviewEvidence"), "approval.independentReviewEvidence", errors)
    if deployable:
        _require(errors, status == "deployable-read-only", "package generation requires status=deployable-read-only")

    if errors:
        raise BootstrapProfileValidationError("; ".join(errors))


def load_profile(path: Path) -> dict[str, Any]:
    try:
        result = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        raise BootstrapProfileValidationError(f"cannot load bootstrap profile: {exc}") from exc
    if not isinstance(result, dict):
        raise BootstrapProfileValidationError("bootstrap profile root must be an object")
    return result


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Offline Base V3 read-only bootstrap profile validator.")
    parser.add_argument("--profile", type=Path, default=Path(__file__).with_name("base_v3_read_only_bootstrap_profile.json"))
    parser.add_argument("--deployable", action="store_true", help="Require real five-signer bootstrap inputs.")
    args = parser.parse_args(argv)
    try:
        validate_bootstrap_profile(load_profile(args.profile), deployable=args.deployable)
    except BootstrapProfileValidationError as error:
        print(f"FAIL-CLOSED: {error}", file=sys.stderr)
        return 2
    print(json.dumps({"valid": True, "mode": "deployable" if args.deployable else "template", "pilotReady": False}, indent=2))
    return 0


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