#!/usr/bin/env python3
"""Fail-closed validator for the Base-mainnet bonds-only pre-deployment manifest.

This module is offline only.  It cannot access wallets, private keys, RPCs,
signing services, or broadcast endpoints.  It validates the checked-in
pre-deployment review artifact and the public-address-only intake separately.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import sys
from copy import deepcopy
from pathlib import Path
from typing import Any

SCHEMA = "vaultfire.base-mainnet-bonds-only-manifest.v2"
SCHEMA_VERSION = "2.0.0"
CHAIN_ID = 8453
ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
HASH_RE = re.compile(r"^[0-9a-f]{64}$")
KECCAK_RE = re.compile(r"^0x[0-9a-f]{64}$")
GIT_COMMIT_RE = re.compile(r"^[0-9a-f]{40}$")
ENABLED = (
    "MultisigGovernanceV3", "BasePilotAccessControllerV3", "ThresholdConsensusV3",
    "SafeBondFactoryV3", "PartnershipBondFactoryV3",
    "AccountabilityBondFactoryV3", "PartnershipBondV3Safe", "AccountabilityBondV3Safe",
)
DISABLED = (
    "RewardProgramV3", "TaskEscrowV3", "MutualAidPoolV3", "VaultfireBIPVaultV3",
    "ERC8004ValidationRegistryV3Safe", "StreetCredV3", "LegacyKYAAdapterV3", "ERC8004IdentityAdapterV3",
    "EvidenceRegistryV3", "MandateRegistryV3", "MandateExecutionAdapterV3", "Solana", "cross-chain",
    "DisputeRegistryV3",
)
DEPLOYMENT_ORDER = (
    "governance", "controller", "consensus", "bondFactory",
    "partnershipFactory", "accountabilityFactory",
)
CONTRACT_BY_LABEL = {
    "governance": "MultisigGovernanceV3", "controller": "BasePilotAccessControllerV3",
    "consensus": "ThresholdConsensusV3",
    "bondFactory": "SafeBondFactoryV3", "partnershipFactory": "PartnershipBondFactoryV3",
    "accountabilityFactory": "AccountabilityBondFactoryV3",
}


class ManifestValidationError(ValueError):
    """The supplied artifact is incomplete or deviates from reviewed scope."""


def canonical_json(value: Any) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")


def sha256_canonical(value: Any) -> str:
    return hashlib.sha256(canonical_json(value)).hexdigest()


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


def _address(value: Any, label: str, errors: list[str]) -> None:
    _need(errors, isinstance(value, str) and ADDRESS_RE.fullmatch(value) is not None, f"{label} must be a public EVM address")
    if isinstance(value, str) and ADDRESS_RE.fullmatch(value):
        _need(errors, int(value[2:], 16) != 0, f"{label} must not be zero")


def _hash(value: Any, label: str, errors: list[str], *, keccak: bool = False) -> None:
    regex = KECCAK_RE if keccak else HASH_RE
    _need(errors, isinstance(value, str) and regex.fullmatch(value) is not None, f"{label} must be a lowercase {'0x-prefixed keccak' if keccak else 'SHA-256'} hash")


def _required_constructor_args(caps: dict[str, Any]) -> dict[str, list[Any]]:
    return {
        "governance": [
            "$roles.governanceSigners", 3, 86400, 86400, "$roles.guardian", 86400, [], [], [],
        ],
        "controller": [
            "$expectedAddresses.governance", caps.get("globalWei"), caps.get("perPositionWei"),
        ],
        "consensus": ["$expectedAddresses.governance", 3],
        "bondFactory": [
            "$expectedAddresses.governance", "$expectedAddresses.consensus", None,
            "$expectedAddresses.controller", caps.get("partnershipPartyWei"), caps.get("partnershipBondWei"),
            caps.get("partnershipAggregateWei"), caps.get("accountabilityBondWei"), caps.get("accountabilityAggregateWei"),
            "$runtimeIdentity.expectedPartnershipFactoryRuntimeCodeHash",
            "$runtimeIdentity.expectedAccountabilityFactoryRuntimeCodeHash",
            "$runtimeIdentity.expectedPartnershipBondRuntimeCodeHash",
            "$runtimeIdentity.expectedAccountabilityBondRuntimeCodeHash",
            "$runtimeIdentity.expectedAccountabilityBondInitCodeHash",
        ],
        "partnershipFactory": [
            "$expectedAddresses.bondFactory", "$expectedAddresses.governance", "$expectedAddresses.consensus",
            None, caps.get("partnershipPartyWei"), caps.get("partnershipBondWei"),
            caps.get("partnershipAggregateWei"), "$expectedAddresses.controller",
        ],
        "accountabilityFactory": [
            "$expectedAddresses.bondFactory", "$expectedAddresses.governance", "$expectedAddresses.consensus",
            None, caps.get("accountabilityBondWei"),
            caps.get("accountabilityAggregateWei"), "$expectedAddresses.controller",
            "$runtimeIdentity.accountabilityChildInitCode",
            "$runtimeIdentity.expectedAccountabilityBondRuntimeCodeHash",
        ],
    }


def validate_manifest(manifest: Any) -> None:
    errors: list[str] = []
    _need(errors, isinstance(manifest, dict), "manifest root must be an object")
    if not isinstance(manifest, dict):
        raise ManifestValidationError("; ".join(errors))
    _need(errors, manifest.get("schema") == SCHEMA and manifest.get("schemaVersion") == SCHEMA_VERSION, "unsupported bonds-only schema version")
    status = manifest.get("status")
    _need(errors, status in ("pre-deployment-review", "finalization-candidate"), "status must be pre-deployment-review or finalization-candidate")
    _need(errors, manifest.get("chain") == {"name": "base", "chainId": CHAIN_ID}, "manifest must bind Base mainnet chain 8453")
    _need(errors, manifest.get("scope", {}).get("enabled") == list(ENABLED), "enabled scope must be the reviewed bonds-only allowlist")
    _need(errors, manifest.get("scope", {}).get("disabled") == list(DISABLED), "disabled scope must explicitly exclude rewards, unrelated modules, Solana, and cross-chain")
    _need(errors, manifest.get("scope", {}).get("assets") == ["native-ETH-only"], "bonds-only scope must be native ETH only")
    _need(
        errors,
        manifest.get("v2Compatibility")
        == {
            "preserved": True,
            "defaultLiveSurface": "V2",
            "v3PaymentRoutesEnabled": False,
            "integrations": {
                "erc8004IdentityRegistryV2": "0xa7BD20bf5De63df949cA5Be2F20835978eCba81A",
                "missionEnforcementV2": "0xBc4208A7A59af3B7E438d3308C3dc0d299d739a7",
            },
        },
        "V2 compatibility record must preserve the live V2 default while V3 payment routes remain disabled",
    )
    _need(
        errors,
        manifest.get("missionPolicy")
        == {
            "priorities": [
                "morals-over-metrics",
                "privacy-over-surveillance",
                "freedom-over-control",
            ],
            "economicObjective": "human-thriving-more-profitable-than-extraction",
            "surveillanceRequired": False,
            "metricsAffectEligibility": False,
            "metricsAffectPrincipal": False,
            "metricsAffectSlashing": False,
            "flourishingMeasurements": {
                "optional": True,
                "privacyMinimized": True,
                "custodial": False,
                "slashable": False,
                "principalLinked": False,
                "breachAdjudicationLinked": False,
            },
        },
        "mission policy must keep measurements optional, privacy-minimized, non-custodial, non-slashable, and separate from eligibility, principal, and breach adjudication",
    )

    integrity = manifest.get("integrity") if isinstance(manifest.get("integrity"), dict) else {}
    _need(errors, integrity.get("canonicalization") == "json-rfc8785-compatible-sorted-keys-utf8", "canonicalization declaration missing")
    _need(errors, integrity.get("schemaVersionSha256") == hashlib.sha256(SCHEMA_VERSION.encode()).hexdigest(), "schema version cryptographic binding mismatch")
    material = deepcopy(manifest)
    material.pop("integrity", None)
    _need(errors, integrity.get("manifestPayloadSha256") == sha256_canonical(material), "manifest payload cryptographic binding mismatch")

    provenance = manifest.get("provenance") if isinstance(manifest.get("provenance"), dict) else {}
    provenance_fields = (
        "sourceCommit", "sourceTree", "contractsTree", "evidenceCommit", "evidenceTree",
        "sourceArchiveSha256", "evidenceArchiveSha256", "reviewerBundleSha256", "validationArchiveSha256",
    )
    _need(errors, set(provenance) == set(provenance_fields), "provenance fields are incomplete")
    if status == "pre-deployment-review":
        _need(errors, all(provenance.get(field) is None for field in provenance_fields),
              "checked-in provenance must remain null until deterministic finalization")
    else:
        for field in ("sourceCommit", "sourceTree", "contractsTree", "evidenceCommit", "evidenceTree"):
            _need(errors, isinstance(provenance.get(field), str) and GIT_COMMIT_RE.fullmatch(provenance[field]) is not None,
                  f"provenance.{field} must be a full lowercase git object ID")
        for field in ("sourceArchiveSha256", "evidenceArchiveSha256", "reviewerBundleSha256", "validationArchiveSha256"):
            _hash(provenance.get(field), f"provenance.{field}", errors)

    compiler = manifest.get("compiler") if isinstance(manifest.get("compiler"), dict) else {}
    _need(errors, compiler == {"solc": "0.8.24", "viaIR": True, "optimizer": {"enabled": True, "runs": 200}, "evmVersion": "paris", "bytecodeHash": "none"}, "compiler/optimizer binding drifted")

    caps = manifest.get("caps") if isinstance(manifest.get("caps"), dict) else {}
    expected_caps = {"globalWei": "400000000000000000", "perPositionWei": "200000000000000000", "partnershipPartyWei": "10000000000000000", "partnershipBondWei": "20000000000000000", "partnershipAggregateWei": "200000000000000000", "accountabilityBondWei": "20000000000000000", "accountabilityAggregateWei": "200000000000000000"}
    _need(errors, caps == expected_caps, "native-ETH caps drifted from reviewed 0.40/0.20 ETH ceilings")
    for name, value in caps.items():
        _need(errors, isinstance(value, str) and value.isdecimal() and int(value) > 0, f"caps.{name} must be a positive wei string")
    if all(isinstance(v, str) and v.isdecimal() for v in caps.values()):
        _need(errors, int(caps["globalWei"]) <= 400000000000000000 and int(caps["perPositionWei"]) <= 200000000000000000, "global/per-position caps exceed hard ceiling")
        _need(errors, int(caps["perPositionWei"]) <= int(caps["globalWei"]), "per-position cap exceeds global cap")
        _need(errors, int(caps["partnershipPartyWei"]) <= int(caps["partnershipBondWei"]) <= int(caps["partnershipAggregateWei"]) <= int(caps["perPositionWei"]), "partnership caps must not exceed position cap")
        _need(errors, int(caps["accountabilityBondWei"]) <= int(caps["accountabilityAggregateWei"]) <= int(caps["perPositionWei"]), "accountability caps must not exceed position cap")

    roles = manifest.get("roles") if isinstance(manifest.get("roles"), dict) else {}
    _need(errors, roles == {"governanceSigners": [None] * 5, "guardian": None, "treasury": None, "attesters": [], "adjudicators": [], "designPartner": None}, "owner-controlled human fields must remain fail-closed null/empty")
    governance = manifest.get("governance")
    _need(errors, governance == {"threshold": 3, "signerCount": 5, "timelockSeconds": 86400, "minimumTimelockSeconds": 86400, "guardianRotationDelaySeconds": 86400}, "governance must be immutable 3-of-5 with 24-hour timelock")

    expected = manifest.get("expectedAddresses") if isinstance(manifest.get("expectedAddresses"), dict) else {}
    _need(errors, set(expected) == set(DEPLOYMENT_ORDER) and all(value is None for value in expected.values()), "pre-deployment expected addresses must be unavailable/null, not placeholders")
    derivation = manifest.get("addressDerivation")
    _need(errors, derivation == {"method": "CREATE/deployer-nonce", "deployer": None, "startingNonce": None, "salt": None}, "address derivation must state unresolved CREATE nonce assumptions without placeholders")

    deployments = manifest.get("deployments")
    _need(errors, isinstance(deployments, list) and [item.get("label") for item in deployments if isinstance(item, dict)] == list(DEPLOYMENT_ORDER), "deployment order drifted")
    if isinstance(deployments, list):
        wanted_args = _required_constructor_args(caps)
        for sequence, item in enumerate(deployments):
            if not isinstance(item, dict):
                continue
            label = item.get("label")
            _need(errors, item.get("sequence") == sequence and item.get("contract") == CONTRACT_BY_LABEL.get(label), f"deployment {sequence} must bind its reviewed contract")
            _need(errors, item.get("constructorArgs") == wanted_args.get(label), f"{label} constructor args drifted")
            _need(errors, item.get("valueWei") == "0" and item.get("creationCodeKeccak256") is None and item.get("runtimeCodeKeccak256") is None, f"{label} must be zero-value and have no invented bytecode hash")

    pause_flow = manifest.get("guardianPauseFlow")
    _need(errors, isinstance(pause_flow, dict), "guardian pause flow is required")
    if isinstance(pause_flow, dict):
        _need(errors, pause_flow.get("constructorTargets") == [], "governance constructor targets must be empty; no non-code or placeholder addresses")
        _need(errors, pause_flow.get("method") == "post-deploy-governance-addGuardianPauseTarget", "pause targets must use reviewed post-deploy governance flow")
        _need(errors, pause_flow.get("targets") == ["PartnershipBondV3Safe", "AccountabilityBondV3Safe"], "pause target scope must be exactly the two bond types")
        _need(
            errors,
            pause_flow.get("requirements")
            == [
                "actual-child-code-exists",
                "3-of-5-confirmed",
                "timelock-elapsed",
                "assert-target-owner-is-governance",
                "assert-target-is-paused-capable",
                "assert-controller-guardianPauseCoverageRequired-is-true",
                "assert-controller-hasGuardianPauseCoverage-is-true-before-value-allocation",
            ],
            "pause target flow requirements drifted",
        )

    child_policy = manifest.get("childBondPolicy")
    _need(
        errors,
        child_policy == {
            "rewardProgramArgument": "address(0)",
            "rewardTerms": "disabled",
            "maximumBondWei": "20000000000000000",
            "exactlyOneActiveAllowlistedPartner": True,
        },
        "child bond policy must disable rewards, cap each bond at 0.02 ETH, and require exactly one partner",
    )

    factory_configuration = manifest.get("factoryConfiguration")
    _need(
        errors,
        factory_configuration
        == {
            "method": "post-deploy-governance-proposal",
            "target": "$expectedAddresses.bondFactory",
            "signature": "configureFactories(address,address)",
            "arguments": [
                "$expectedAddresses.partnershipFactory",
                "$expectedAddresses.accountabilityFactory",
            ],
            "directCallForbidden": True,
            "requiredBeforeDeployEnablement": True,
        },
        "specialized factory configuration must be a one-time governed call before deployment enablement",
    )

    runtime_identity = manifest.get("runtimeIdentity")
    _need(
        errors,
        runtime_identity
        == {
            "method": "immutable-patched-runtime-code-keccak256",
            "expectedPartnershipFactoryRuntimeCodeHash": None,
            "expectedAccountabilityFactoryRuntimeCodeHash": None,
            "expectedPartnershipBondRuntimeCodeHash": None,
            "expectedAccountabilityBondRuntimeCodeHash": None,
            "expectedAccountabilityBondInitCodeHash": None,
            "accountabilityChildInitCode": None,
            "pinnedBy": "AccountabilityBondFactoryV3 constructor pins exact child initcode hash; SafeBondFactoryV3 verifies child runtime and critical getters",
        },
        "runtime identity policy must require constructor-pinned immutable-patched factory and child code hashes",
    )

    evidence = manifest.get("evidence") if isinstance(manifest.get("evidence"), dict) else {}
    for key in ("audit", "sepolia", "x402"):
        record = evidence.get(key)
        _need(errors, isinstance(record, dict) and record.get("satisfied") is False, f"evidence.{key} must remain unsatisfied")
        if isinstance(record, dict):
            _need(errors, record.get("path") is None and record.get("sha256") is None,
                  f"evidence.{key} must remain null until independently finalized")
    approval = manifest.get("approval")
    _need(errors, approval == {"explicitMainnetApproval": False, "approvalEvidence": None, "deploymentAuthorized": False}, "approval must remain explicit and fail-closed")

    assertions = manifest.get("postDeployAssertions")
    required_assertions = [
        "chainId-is-8453",
        "all-runtime-hashes-match-package",
        "all-constructor-args-match-package",
        "governance-is-3-of-5-and-24h",
        "controller-disabled-and-caps-unset-before-approval",
        "consensus-has-no-attesters",
        "factory-deployAllowed-is-false",
        "coordinator-constructor-pinned-specialized-factory-and-child-runtime-hashes",
        "specialized-factory-addresses-and-immutable-bindings-match-package",
        "factory-configuration-is-confirmed-executed-before-deploy-enable",
        "no-reward-or-disabled-module-code-deployed",
        "no-design-partner-or-position",
        "factory-children-require-registered-currently-allowlisted-design-partner-in-signed-terms",
        "controller-rejects-value-allocation-without-live-governance-pause-target",
        "retired-modules-are-not-enabled-deployed-configured-or-controller-authorized",
    ]
    _need(errors, assertions == required_assertions, "post-deploy assertions drifted")
    if errors:
        raise ManifestValidationError("; ".join(errors))


def validate_public_intake(intake: Any) -> None:
    errors: list[str] = []
    _need(errors, isinstance(intake, dict), "intake root must be an object")
    if not isinstance(intake, dict):
        raise ManifestValidationError("; ".join(errors))
    _need(errors, intake.get("schema") == "vaultfire.base-mainnet-bonds-only-public-intake.v1", "unsupported public intake schema")
    _need(errors, intake.get("chain") == {"name": "base", "chainId": CHAIN_ID}, "public intake must bind Base")
    _need(errors, intake.get("authorization") == {"signing": False, "broadcast": False, "deployment": False}, "public intake must not authorize operations")
    roles = intake.get("roles") if isinstance(intake.get("roles"), dict) else {}
    _need(errors, roles.get("governanceSigners") == [None] * 5 and roles.get("guardian") is None and roles.get("treasury") is None and roles.get("designPartner") is None, "checked-in intake must contain public-address fields only and remain null")
    _need(errors, roles.get("attesters") == [] and roles.get("adjudicators") == [], "checked-in intake must not invent operating roles")
    def contains_forbidden_field(value: Any) -> bool:
        if isinstance(value, dict):
            for key, child in value.items():
                normalized = str(key).lower().replace("_", "").replace("-", "").replace(" ", "")
                if normalized in {"privatekey", "seedphrase", "mnemonic", "keystore", "password", "signature", "recoverymaterial"}:
                    return True
                if contains_forbidden_field(child):
                    return True
        elif isinstance(value, list):
            return any(contains_forbidden_field(item) for item in value)
        return False

    _need(errors, not contains_forbidden_field(intake), "intake must never contain secret/signing-material fields")
    if errors:
        raise ManifestValidationError("; ".join(errors))


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


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Offline Base-mainnet bonds-only manifest validator.")
    parser.add_argument("--manifest", type=Path, default=Path(__file__).with_name("base_v3_deployment_manifest.json"))
    parser.add_argument("--intake", type=Path, default=Path(__file__).with_name("base_v3_bonds_only_public_address_intake.template.json"))
    args = parser.parse_args(argv)
    try:
        validate_manifest(load_json(args.manifest))
        validate_public_intake(load_json(args.intake))
    except ManifestValidationError as exc:
        print(f"FAIL-CLOSED: {exc}", file=sys.stderr)
        return 2
    print(json.dumps({"valid": True, "chainId": CHAIN_ID, "mode": "pre-deployment-review", "operations": "none"}))
    return 0


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