#!/usr/bin/env python3
"""Generate an unsigned, non-broadcast Base V3 bootstrap deployment package.

The generator accepts a completed read-only bootstrap profile and Foundry JSON
artifacts. It produces deterministic CREATE addresses, constructor init code,
and runtime-bytecode hashes. It never accepts keys, creates wallets, signs,
contacts an RPC, deploys, or broadcasts.
"""
from __future__ import annotations

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

from bootstrap_profile import (
    BOOTSTRAP_DEPLOYMENTS,
    BootstrapProfileValidationError,
    load_profile,
    validate_bootstrap_profile,
)

try:
    from Crypto.Hash import keccak
except ImportError as exc:  # pragma: no cover - documented environment requirement
    raise SystemExit("FAIL-CLOSED: pycryptodome keccak support is required") from exc

ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
CONTRACT_NAMES = {
    "consensus": "ThresholdConsensusV3",
    "controller": "BasePilotAccessControllerV3",
    "governance": "MultisigGovernanceV3",
    "bondFactory": "SafeBondFactoryV3",
    "partnershipFactory": "PartnershipBondFactoryV3",
    "accountabilityFactory": "AccountabilityBondFactoryV3",
    "partnershipBond": "PartnershipBondV3Safe",
    "accountabilityBond": "AccountabilityBondV3Safe",
}
INTERNAL_FACTORY_NAMES = {"accountabilityBond": "AccountabilityBondV3Safe"}
EXPECTED_CONSTRUCTOR_TYPES = {
    "consensus": ["address", "uint8"],
    "controller": ["address", "uint256", "uint256"],
    "governance": [
        "address[]", "uint256", "uint256", "uint256", "address", "uint256",
        "address[]", "address[]", "bytes32[]",
    ],
    "bondFactory": [
        "address", "address", "address", "address", "uint256", "uint256", "uint256", "uint256", "uint256",
        "bytes32", "bytes32", "bytes32", "bytes32", "bytes32",
    ],
}


def _keccak(data: bytes) -> bytes:
    digest = keccak.new(digest_bits=256)
    digest.update(data)
    return digest.digest()


# Bind inherited EIP712BaseV3 immutables from the source version expression,
# rather than reproducing a hand-copied digest in deployment tooling.
EIP712_V3_VERSION_HASH = "0x" + _keccak(b"3.0.0").hex()


def _hex_bytes(value: str, label: str) -> bytes:
    if not isinstance(value, str):
        raise BootstrapProfileValidationError(f"{label} must be hexadecimal bytecode")
    value = value[2:] if value.startswith("0x") else value
    if not value or len(value) % 2 or re.fullmatch(r"[0-9a-fA-F]+", value) is None:
        raise BootstrapProfileValidationError(f"{label} must be nonempty even-length hexadecimal bytecode")
    return bytes.fromhex(value)


def _rlp_bytes(value: bytes) -> bytes:
    if len(value) == 1 and value[0] < 0x80:
        return value
    if len(value) <= 55:
        return bytes([0x80 + len(value)]) + value
    length = len(value).to_bytes((len(value).bit_length() + 7) // 8, "big")
    return bytes([0xB7 + len(length)]) + length + value


def _rlp_list(items: list[bytes]) -> bytes:
    payload = b"".join(items)
    if len(payload) <= 55:
        return bytes([0xC0 + len(payload)]) + payload
    length = len(payload).to_bytes((len(payload).bit_length() + 7) // 8, "big")
    return bytes([0xF7 + len(length)]) + length + payload


def compute_create_address(deployer: str, nonce: int) -> str:
    if not isinstance(nonce, int) or nonce < 0:
        raise BootstrapProfileValidationError("starting nonce must be a non-negative integer")
    if not isinstance(deployer, str) or ADDRESS_RE.fullmatch(deployer) is None or int(deployer[2:], 16) == 0:
        raise BootstrapProfileValidationError("deployer must be a non-zero EVM address")
    nonce_bytes = b"" if nonce == 0 else nonce.to_bytes((nonce.bit_length() + 7) // 8, "big")
    encoded = _rlp_list([_rlp_bytes(bytes.fromhex(deployer[2:])), _rlp_bytes(nonce_bytes)])
    return "0x" + _keccak(encoded)[-20:].hex()


def _word_uint(value: int, label: str) -> bytes:
    if not isinstance(value, int) or value < 0 or value >= 2**256:
        raise BootstrapProfileValidationError(f"{label} must be a uint256-compatible non-negative integer")
    return value.to_bytes(32, "big")


def _word_address(value: str, label: str) -> bytes:
    if not isinstance(value, str) or ADDRESS_RE.fullmatch(value) is None:
        raise BootstrapProfileValidationError(f"{label} must be an EVM address")
    return b"\x00" * 12 + bytes.fromhex(value[2:])


def _word_immutable_address(value: str, label: str) -> bytes:
    """Encode an immutable address, which may intentionally be address(0)."""
    if not isinstance(value, str) or ADDRESS_RE.fullmatch(value) is None:
        raise BootstrapProfileValidationError(f"{label} must be an EVM address")
    return b"\x00" * 12 + bytes.fromhex(value[2:])


def _encode_static(type_name: str, value: Any, label: str) -> bytes:
    if type_name == "address":
        return _word_address(value, label)
    if type_name.startswith("uint"):
        bits = int(type_name[4:] or "256")
        if bits <= 0 or bits > 256 or bits % 8:
            raise BootstrapProfileValidationError(f"unsupported ABI integer type {type_name}")
        if not isinstance(value, int) or value < 0 or value >= 2**bits:
            raise BootstrapProfileValidationError(f"{label} is outside {type_name}")
        return _word_uint(value, label)
    if type_name == "bool":
        if not isinstance(value, bool):
            raise BootstrapProfileValidationError(f"{label} must be boolean")
        return _word_uint(1 if value else 0, label)
    if type_name == "bytes32":
        if not isinstance(value, str) or re.fullmatch(r"0x[0-9a-fA-F]{64}", value) is None:
            raise BootstrapProfileValidationError(f"{label} must be a 32-byte hexadecimal value")
        return bytes.fromhex(value[2:])
    raise BootstrapProfileValidationError(f"unsupported static ABI type {type_name}")


def _encode_dynamic(type_name: str, value: Any, label: str) -> bytes:
    if type_name == "address[]":
        if not isinstance(value, list):
            raise BootstrapProfileValidationError(f"{label} must be an address array")
        return _word_uint(len(value), label) + b"".join(_word_address(item, f"{label}[{index}]") for index, item in enumerate(value))
    if type_name == "bytes32[]":
        if not isinstance(value, list):
            raise BootstrapProfileValidationError(f"{label} must be a bytes32 array")
        return _word_uint(len(value), label) + b"".join(
            _encode_static("bytes32", item, f"{label}[{index}]") for index, item in enumerate(value)
        )
    if type_name == "bytes":
        raw = _hex_bytes(value, label)
        return _word_uint(len(raw), label) + raw + b"\x00" * ((32 - len(raw) % 32) % 32)
    raise BootstrapProfileValidationError(f"unsupported dynamic ABI type {type_name}")


def abi_encode(types: list[str], values: list[Any]) -> bytes:
    if len(types) != len(values):
        raise BootstrapProfileValidationError("constructor ABI/value length mismatch")
    head: list[bytes | None] = []
    tails: list[bytes] = []
    for index, (type_name, value) in enumerate(zip(types, values)):
        if type_name.endswith("[]") or type_name == "bytes":
            head.append(None)
            tails.append(_encode_dynamic(type_name, value, f"constructor[{index}]"))
        else:
            head.append(_encode_static(type_name, value, f"constructor[{index}]"))
    offset = 32 * len(types)
    tail_index = 0
    encoded_head: list[bytes] = []
    for item in head:
        if item is None:
            tail = tails[tail_index]
            encoded_head.append(_word_uint(offset, "dynamic offset"))
            offset += len(tail)
            tail_index += 1
        else:
            encoded_head.append(item)
    return b"".join(encoded_head + tails)


def _artifact_contracts(artifacts_dir: Path) -> dict[str, dict[str, Any]]:
    found: dict[str, dict[str, Any]] = {}
    for path in artifacts_dir.rglob("*.json"):
        try:
            artifact = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        name = artifact.get("contractName") if isinstance(artifact, dict) else None
        if (
            name is None
            and path.stem in (*CONTRACT_NAMES.values(), *INTERNAL_FACTORY_NAMES.values())
            and path.parent.name == f"{path.stem}.sol"
        ):
            # Foundry's standard `out/Contract.sol/Contract.json` artifacts do
            # not carry a contractName field. Bind only the canonical output
            # path, never an arbitrary JSON filename.
            name = path.stem
        if isinstance(name, str) and name in (*CONTRACT_NAMES.values(), *INTERNAL_FACTORY_NAMES.values()):
            if name in found:
                raise BootstrapProfileValidationError(f"multiple artifacts found for {name}; provide a clean Foundry out directory")
            found[name] = artifact
    missing = [name for name in (*CONTRACT_NAMES.values(), *INTERNAL_FACTORY_NAMES.values()) if name not in found]
    if missing:
        raise BootstrapProfileValidationError("missing Foundry artifacts: " + ", ".join(missing))
    _attach_build_info_asts(artifacts_dir, found)
    return found


def _ast_immutable_names(ast: Any) -> dict[str, str]:
    names: dict[str, str] = {}

    def walk(node: Any) -> None:
        if isinstance(node, dict):
            if node.get("nodeType") == "VariableDeclaration" and node.get("mutability") == "immutable":
                node_id = node.get("id")
                name = node.get("name")
                if isinstance(node_id, int) and isinstance(name, str) and name:
                    names[str(node_id)] = name
            for value in node.values():
                walk(value)
        elif isinstance(node, list):
            for value in node:
                walk(value)

    walk(ast)
    return names


def _attach_build_info_asts(artifacts_dir: Path, artifacts: dict[str, dict[str, Any]]) -> None:
    """Fill Foundry artifact AST gaps from standard build-info output."""
    candidates: list[dict[str, Any]] = []
    for path in (artifacts_dir / "build-info").glob("*.json"):
        try:
            build_info = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        sources = build_info.get("output", {}).get("sources", {}) if isinstance(build_info, dict) else {}
        if isinstance(sources, dict):
            candidates.extend(
                source["ast"] for source in sources.values()
                if isinstance(source, dict) and isinstance(source.get("ast"), dict)
            )

    available_names: dict[str, str] = {}
    for ast in candidates:
        for node_id, immutable_name in _ast_immutable_names(ast).items():
            existing = available_names.get(node_id)
            if existing is not None and existing != immutable_name:
                raise BootstrapProfileValidationError("build-info immutable declaration IDs are ambiguous")
            available_names[node_id] = immutable_name

    for name, artifact in artifacts.items():
        deployed = artifact.get("deployedBytecode")
        references = deployed.get("immutableReferences", {}) if isinstance(deployed, dict) else {}
        if not isinstance(references, dict) or not references:
            continue

        # Foundry 0.8.24 places the child source unit AST in the contract
        # artifact. Its immutableReferences can additionally name inherited
        # declarations from a different source unit (EIP712BaseV3._VERSION_HASH
        # for both bond children). Therefore an artifact AST is evidence for
        # its local declarations, not proof that build-info is unnecessary.
        local_names = _ast_immutable_names(artifact.get("ast")) if isinstance(artifact.get("ast"), dict) else {}
        if not set(local_names).issubset(references):
            raise BootstrapProfileValidationError(
                f"{name} artifact immutable declarations are not exactly represented in runtime references"
            )
        for node_id, immutable_name in local_names.items():
            build_info_name = available_names.get(node_id)
            if build_info_name is not None and build_info_name != immutable_name:
                raise BootstrapProfileValidationError("build-info immutable declaration IDs are ambiguous")

        missing = set(references) - set(local_names)
        if not missing.issubset(available_names):
            raise BootstrapProfileValidationError(
                f"{name} immutable declarations are unavailable or ambiguous; run forge build --build-info with a clean out directory"
            )
        bound_names = {
            node_id: local_names[node_id] if node_id in local_names else available_names[node_id]
            for node_id in references
        }
        if len(set(bound_names.values())) != len(bound_names):
            raise BootstrapProfileValidationError(
                f"{name} immutable runtime references cannot be uniquely bound to reviewed declarations"
            )

        # Preserve only declarations referenced by the particular runtime.
        # This composes the child artifact AST with inherited build-info source
        # units while retaining the exact set-equality check in
        # _immutable_names below.
        artifact["_bootstrapBuildInfoAst"] = {
            "nodeType": "SourceUnit",
            "nodes": [
                {
                    "nodeType": "VariableDeclaration",
                    "id": int(node_id),
                    "name": bound_names[node_id],
                    "mutability": "immutable",
                }
                for node_id in references
            ],
        }


def _constructor_types(artifact: dict[str, Any], contract_name: str) -> list[str]:
    abi = artifact.get("abi")
    if not isinstance(abi, list):
        raise BootstrapProfileValidationError(f"{contract_name} artifact ABI is missing")
    constructors = [entry for entry in abi if isinstance(entry, dict) and entry.get("type") == "constructor"]
    if len(constructors) != 1 or not isinstance(constructors[0].get("inputs"), list):
        raise BootstrapProfileValidationError(f"{contract_name} artifact must have exactly one constructor ABI")
    types = []
    for item in constructors[0]["inputs"]:
        if not isinstance(item, dict) or not isinstance(item.get("type"), str):
            raise BootstrapProfileValidationError(f"{contract_name} constructor ABI input is malformed")
        types.append(item["type"])
    return types


def _require_expected_constructor_types(label: str, types: list[str]) -> None:
    expected = EXPECTED_CONSTRUCTOR_TYPES.get(label)
    if expected is None:
        raise BootstrapProfileValidationError(f"unsupported bootstrap deployment label {label}")
    if types != expected:
        raise BootstrapProfileValidationError(
            f"{label} constructor ABI drifted from the reviewed bootstrap binding"
        )


def _constructor_values(label: str, profile: dict[str, Any], addresses: dict[str, str]) -> list[Any]:
    roles = profile["roles"]
    caps = profile["bondFactory"]["immutableNativeCapsWei"]
    identity = profile.get("runtimeIdentity")
    if label == "consensus":
        return [addresses["governance"], 3]
    if label == "controller":
        max_global_cap, max_per_position_cap = _controller_max_caps(profile)
        return [addresses["governance"], max_global_cap, max_per_position_cap]
    if label == "governance":
        return [
            roles["governanceSigners"],
            3,
            86400,
            86400,
            roles["guardian"],
            86400,
            [],
            [addresses["controller"], addresses["consensus"], addresses["bondFactory"]],
            [
                identity["expectedControllerRuntimeCodeHash"],
                identity["expectedConsensusRuntimeCodeHash"],
                identity["expectedBondFactoryRuntimeCodeHash"],
            ],
        ]
    if label == "bondFactory":
        if not isinstance(identity, dict):
            raise BootstrapProfileValidationError("bond factory runtime identities are required")
        return [
            addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000", addresses["controller"],
            int(caps["partnershipParty"]), int(caps["partnershipBond"]), int(caps["partnershipAggregate"]),
            int(caps["accountabilityBond"]), int(caps["accountabilityAggregate"]),
            identity["expectedPartnershipFactoryRuntimeCodeHash"],
            identity["expectedAccountabilityFactoryRuntimeCodeHash"],
            identity["expectedPartnershipBondRuntimeCodeHash"],
            identity["expectedAccountabilityBondRuntimeCodeHash"],
            identity["expectedAccountabilityBondInitCodeHash"],
        ]
    if label == "partnershipFactory":
        return [
            addresses["bondFactory"], addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000",
            int(caps["partnershipParty"]), int(caps["partnershipBond"]), int(caps["partnershipAggregate"]),
            addresses["controller"],
        ]
    if label == "accountabilityFactory":
        if not isinstance(identity, dict):
            raise BootstrapProfileValidationError("accountability factory initcode identity is required")
        return [
            addresses["bondFactory"], addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000",
            int(caps["accountabilityBond"]), int(caps["accountabilityAggregate"]), addresses["controller"],
            identity["accountabilityChildInitCode"], identity["expectedAccountabilityBondRuntimeCodeHash"],
        ]
    raise BootstrapProfileValidationError(f"unsupported bootstrap deployment label {label}")


def _immutable_names(artifact: dict[str, Any], contract_name: str) -> dict[str, str]:
    deployed = artifact.get("deployedBytecode")
    if not isinstance(deployed, dict):
        raise BootstrapProfileValidationError(f"{contract_name} deployed bytecode is missing")
    references = deployed.get("immutableReferences", {})
    if not isinstance(references, dict):
        raise BootstrapProfileValidationError(f"{contract_name} immutable references are malformed")
    if not references:
        return {}
    ast = artifact.get("_bootstrapBuildInfoAst", artifact.get("ast"))
    if not isinstance(ast, dict):
        raise BootstrapProfileValidationError(
            f"{contract_name} immutable AST is required for runtime evidence; run forge build --build-info first"
        )
    names = _ast_immutable_names(ast)
    if set(references) != set(names):
        raise BootstrapProfileValidationError(
            f"{contract_name} immutable runtime references cannot be bound to reviewed declarations"
        )
    return names


def _controller_max_caps(profile: dict[str, Any]) -> tuple[int, int]:
    controller = profile.get("controller")
    if isinstance(controller, dict):
        global_cap = controller.get("maxGlobalCap")
        per_position_cap = controller.get("maxPerPositionCap")
    else:
        caps = profile.get("caps")
        global_cap = caps.get("globalWei") if isinstance(caps, dict) else None
        per_position_cap = caps.get("perPositionWei") if isinstance(caps, dict) else None
    try:
        maximums = int(global_cap), int(per_position_cap)
    except (TypeError, ValueError) as exc:
        raise BootstrapProfileValidationError("controller immutable maximum caps are required") from exc
    if maximums[0] <= 0 or maximums[1] <= 0 or maximums[1] > maximums[0]:
        raise BootstrapProfileValidationError("controller immutable maximum caps are invalid")
    return maximums


def _immutable_values(label: str, profile: dict[str, Any], addresses: dict[str, str]) -> dict[str, Any]:
    caps = profile.get("bondFactory", {}).get("immutableNativeCapsWei")
    if caps is None:
        raw = profile["caps"]
        caps = {
            "partnershipParty": raw["partnershipPartyWei"],
            "partnershipBond": raw["partnershipBondWei"],
            "partnershipAggregate": raw["partnershipAggregateWei"],
            "accountabilityBond": raw["accountabilityBondWei"],
            "accountabilityAggregate": raw["accountabilityAggregateWei"],
        }
    roles = profile.get("roles", {})
    owner = addresses["governance"]
    identity = profile.get("runtimeIdentity", {})
    values: dict[str, dict[str, Any]] = {
        "controller": {"governance": addresses["governance"]},
        "governance": {"minimumTimelockSeconds": 86400, "guardianRotationDelay": 86400},
        "bondFactory": {
            "consensus": addresses["consensus"],
            "disputeRegistry": "0x0000000000000000000000000000000000000000",
            "pilotController": addresses["controller"],
            "partnershipMaxPartyNativePrincipal": int(caps["partnershipParty"]),
            "partnershipMaxBondNativePrincipal": int(caps["partnershipBond"]),
            "partnershipMaxAggregateNativeLiability": int(caps["partnershipAggregate"]),
            "accountabilityMaxBondNativePrincipal": int(caps["accountabilityBond"]),
            "accountabilityMaxAggregateNativeLiability": int(caps["accountabilityAggregate"]),
        },
        "partnershipFactory": {
            "coordinator": addresses["bondFactory"], "bondOwner": owner, "consensus": addresses["consensus"],
            "disputeRegistry": "0x0000000000000000000000000000000000000000", "pilotController": addresses["controller"],
            "maxPartyNativePrincipal": int(caps["partnershipParty"]),
            "maxBondNativePrincipal": int(caps["partnershipBond"]),
            "maxAggregateNativeLiability": int(caps["partnershipAggregate"]),
        },
        "accountabilityFactory": {
            "coordinator": addresses["bondFactory"], "bondOwner": owner, "consensus": addresses["consensus"],
            "disputeRegistry": "0x0000000000000000000000000000000000000000", "pilotController": addresses["controller"],
            "maxBondNativePrincipal": int(caps["accountabilityBond"]),
            "maxAggregateNativeLiability": int(caps["accountabilityAggregate"]),
            "childInitCodeHash": identity.get("expectedAccountabilityBondInitCodeHash") if isinstance(identity, dict) else None,
            "expectedChildRuntimeCodeHash": identity.get("expectedAccountabilityBondRuntimeCodeHash") if isinstance(identity, dict) else None,
        },
        "partnershipBond": {
            "_VERSION_HASH": EIP712_V3_VERSION_HASH,
            "rewardProgram": "0x0000000000000000000000000000000000000000",
            "consensus": addresses["consensus"], "disputeRegistry": "0x0000000000000000000000000000000000000000",
            "pilotController": addresses["controller"], "maxPartyNativePrincipal": int(caps["partnershipParty"]),
            "maxBondNativePrincipal": int(caps["partnershipBond"]),
            "maxAggregateNativeLiability": int(caps["partnershipAggregate"]),
        },
        "accountabilityBond": {
            "_VERSION_HASH": EIP712_V3_VERSION_HASH,
            "consensus": addresses["consensus"], "disputeRegistry": "0x0000000000000000000000000000000000000000",
            "pilotController": addresses["controller"], "maxBondNativePrincipal": int(caps["accountabilityBond"]),
            "maxAggregateNativeLiability": int(caps["accountabilityAggregate"]),
        },
    }
    if label == "controller":
        max_global_cap, max_per_position_cap = _controller_max_caps(profile)
        values["controller"].update({"maxGlobalCap": max_global_cap, "maxPerPositionCap": max_per_position_cap})
    if label == "bondFactory" and identity:
        for key in (
            "expectedPartnershipFactoryRuntimeCodeHash",
            "expectedAccountabilityFactoryRuntimeCodeHash",
            "expectedPartnershipBondRuntimeCodeHash",
            "expectedAccountabilityBondRuntimeCodeHash",
            "expectedAccountabilityBondInitCodeHash",
        ):
            if key in identity:
                values["bondFactory"][key] = identity[key]
    return values.get(label, {})


def _expected_runtime(
    artifact: dict[str, Any], contract_name: str, label: str, profile: dict[str, Any], addresses: dict[str, str]
) -> bytes:
    deployed = artifact.get("deployedBytecode")
    if not isinstance(deployed, dict):
        raise BootstrapProfileValidationError(f"{contract_name} deployed bytecode is missing")
    runtime = bytearray(_hex_bytes(deployed.get("object"), f"{contract_name}.deployedBytecode.object"))
    references = deployed.get("immutableReferences", {})
    if not isinstance(references, dict):
        raise BootstrapProfileValidationError(f"{contract_name} immutable references are malformed")
    names = _immutable_names(artifact, contract_name)
    values = _immutable_values(label, profile, addresses)
    if references and set(names.values()) != set(values):
        raise BootstrapProfileValidationError(f"{label} immutable declarations drifted from reviewed bootstrap bindings")
    for node_id, locations in references.items():
        name = names[node_id]
        value = values[name]
        if isinstance(value, str):
            encoded = (
                bytes.fromhex(value[2:])
                if re.fullmatch(r"0x[0-9a-fA-F]{64}", value)
                else _word_immutable_address(value, f"{label}.{name}")
            )
        else:
            encoded = _word_uint(value, f"{label}.{name}")
        if not isinstance(locations, list):
            raise BootstrapProfileValidationError(f"{contract_name} immutable runtime locations are malformed")
        for location in locations:
            if not isinstance(location, dict) or location.get("length") != 32 or not isinstance(location.get("start"), int):
                raise BootstrapProfileValidationError(f"{contract_name} immutable runtime location is malformed")
            start = location["start"]
            if start < 0 or start + 32 > len(runtime):
                raise BootstrapProfileValidationError(f"{contract_name} immutable runtime location is out of bounds")
            runtime[start:start + 32] = encoded
    return bytes(runtime)


def generate_package(profile: dict[str, Any], artifacts_dir: Path, deployer: str, starting_nonce: int) -> dict[str, Any]:
    validate_bootstrap_profile(profile, deployable=True)
    artifacts = _artifact_contracts(artifacts_dir)
    addresses = {label: compute_create_address(deployer, starting_nonce + index) for index, label in enumerate(BOOTSTRAP_DEPLOYMENTS)}
    identity_profile = deepcopy(profile)
    identity_profile["runtimeIdentity"] = {}
    for label in ("partnershipBond", "accountabilityBond"):
        name = CONTRACT_NAMES[label]
        runtime = _expected_runtime(artifacts[name], name, label, identity_profile, addresses)
        identity_profile["runtimeIdentity"][f"expected{label[0].upper()}{label[1:]}RuntimeCodeHash"] = "0x" + _keccak(runtime).hex()
    accountability_types = _constructor_types(artifacts["AccountabilityBondV3Safe"], "AccountabilityBondV3Safe")
    if not accountability_types:
        accountability_types = ["address", "address", "address", "uint256", "uint256", "address"]
    accountability_values = [
        addresses["governance"],
        addresses["consensus"],
        "0x0000000000000000000000000000000000000000",
        int(profile["bondFactory"]["immutableNativeCapsWei"]["accountabilityBond"]),
        int(profile["bondFactory"]["immutableNativeCapsWei"]["accountabilityAggregate"]),
        addresses["controller"],
    ]
    accountability_init = (
        _hex_bytes(
            artifacts["AccountabilityBondV3Safe"]["bytecode"]["object"],
            "AccountabilityBondV3Safe.bytecode.object",
        )
        + abi_encode(accountability_types, accountability_values)
    )
    identity_profile["runtimeIdentity"]["accountabilityChildInitCode"] = "0x" + accountability_init.hex()
    identity_profile["runtimeIdentity"]["expectedAccountabilityBondInitCodeHash"] = (
        "0x" + _keccak(accountability_init).hex()
    )
    for label in ("partnershipFactory", "accountabilityFactory"):
        name = CONTRACT_NAMES[label]
        runtime = _expected_runtime(artifacts[name], name, label, identity_profile, addresses)
        identity_profile["runtimeIdentity"][f"expected{label[0].upper()}{label[1:]}RuntimeCodeHash"] = "0x" + _keccak(runtime).hex()
    for label in ("controller", "consensus", "bondFactory"):
        name = CONTRACT_NAMES[label]
        runtime = _expected_runtime(artifacts[name], name, label, identity_profile, addresses)
        identity_profile["runtimeIdentity"][f"expected{label[0].upper()}{label[1:]}RuntimeCodeHash"] = "0x" + _keccak(runtime).hex()
    transactions = []
    for index, label in enumerate(BOOTSTRAP_DEPLOYMENTS):
        name = CONTRACT_NAMES[label]
        artifact = artifacts[name]
        types = _constructor_types(artifact, name)
        _require_expected_constructor_types(label, types)
        values = _constructor_values(label, identity_profile, addresses)
        init_code = _hex_bytes(artifact.get("bytecode", {}).get("object"), f"{name}.bytecode.object") + abi_encode(types, values)
        runtime = _expected_runtime(artifact, name, label, identity_profile, addresses)
        transactions.append({
            "sequence": index,
            "label": label,
            "contract": name,
            "from": deployer,
            "to": None,
            "nonce": starting_nonce + index,
            "valueWei": "0",
            "data": "0x" + init_code.hex(),
            "constructorArgs": values,
            "predictedAddress": addresses[label],
            "initCodeKeccak256": "0x" + _keccak(init_code).hex(),
            "expectedRuntimeCodeKeccak256": "0x" + _keccak(runtime).hex(),
            "signed": False,
            "broadcast": False,
        })
    return {
        "schema": "vaultfire.base-v3-unsigned-bootstrap-package.v1",
        "chain": {"name": "base", "chainId": 8453},
        "sourceCommit": profile["sourceCommit"],
        "mode": "read-only-bootstrap",
        "unsigned": True,
        "broadcast": False,
        "deployer": deployer,
        "startingNonce": starting_nonce,
        "pilotReady": False,
        "forbiddenOperations": ["private-key-input", "wallet-generation", "signing", "rpc", "broadcast", "funding", "activation"],
        "transactions": transactions,
        "constructorInternalCreates": [],
        "postDeployExpectedState": {
            "controller.deployEnabled": False,
            "controller.writeEnabled": False,
            "controller.globalCap": "0",
            "controller.perPositionCap": "0",
            "controller.maxGlobalCap": str(_controller_max_caps(profile)[0]),
            "controller.maxPerPositionCap": str(_controller_max_caps(profile)[1]),
            "consensus.threshold": 3,
            "consensus.attesterCount": 0,
            "bondFactory.deployAllowed": False,
            "bondFactory.partnershipFactory": "0x0000000000000000000000000000000000000000",
            "bondFactory.accountabilityFactory": "0x0000000000000000000000000000000000000000",
            "runtimeIdentity": identity_profile["runtimeIdentity"],
            "pilotReady": False,
        },
    }


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Create an unsigned, non-broadcast Base V3 bootstrap package.")
    parser.add_argument("--profile", required=True, type=Path)
    parser.add_argument("--artifacts-dir", required=True, type=Path)
    parser.add_argument("--deployer", required=True, help="Existing public deployer address only; no key is accepted.")
    parser.add_argument("--starting-nonce", required=True, type=int)
    parser.add_argument("--out", required=True, type=Path)
    args = parser.parse_args(argv)
    try:
        package = generate_package(load_profile(args.profile), args.artifacts_dir, args.deployer, args.starting_nonce)
    except BootstrapProfileValidationError as error:
        parser.error(f"FAIL-CLOSED: {error}")
    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(json.dumps(package, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"written": str(args.out), "unsigned": True, "broadcast": False, "pilotReady": False}, indent=2))
    return 0


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