#!/usr/bin/env python3
"""Generate an unsigned Base bonds-only package from reviewed artifacts.

No private key, wallet, RPC endpoint, signing primitive, deployment action, or
broadcast operation is implemented or accepted by this program.
"""
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

sys.path.insert(0, str(Path(__file__).resolve().parent))

from deployment_manifest import (
    ADDRESS_RE, CONTRACT_BY_LABEL, DEPLOYMENT_ORDER, ManifestValidationError, canonical_json,
    load_json, validate_manifest,
)
from generate_bootstrap_package import (
    _attach_build_info_asts,
    _constructor_types,
    _expected_runtime,
    _hex_bytes,
    _keccak,
    abi_encode,
    compute_create_address,
)

CONSTRUCTOR_TYPES = {
    "governance": [
        "address[]", "uint256", "uint256", "uint256", "address", "uint256",
        "address[]", "address[]", "bytes32[]",
    ],
    "controller": ["address", "uint256", "uint256"], "consensus": ["address", "uint8"],
    "bondFactory": [
        "address", "address", "address", "address", "uint256", "uint256", "uint256", "uint256", "uint256",
        "bytes32", "bytes32", "bytes32", "bytes32", "bytes32",
    ],
    "partnershipFactory": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "address"],
    "accountabilityFactory": [
        "address", "address", "address", "address", "uint256", "uint256", "address", "bytes", "bytes32",
    ],
}
RUNTIME_IDENTITY_CONTRACTS = {
    "partnershipBond": "PartnershipBondV3Safe",
    "accountabilityBond": "AccountabilityBondV3Safe",
}


def _find_artifacts(directory: Path) -> dict[str, dict[str, Any]]:
    found: dict[str, dict[str, Any]] = {}
    wanted = set(CONTRACT_BY_LABEL.values()) | set(RUNTIME_IDENTITY_CONTRACTS.values())
    for path in directory.rglob("*.json"):
        try:
            value = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        name = value.get("contractName") if isinstance(value, dict) else None
        if name is None and path.stem in wanted and path.parent.name == f"{path.stem}.sol":
            name = path.stem
        if name in wanted:
            if name in found:
                raise ManifestValidationError(f"multiple artifacts found for {name}")
            found[name] = value
    missing = wanted - set(found)
    if missing:
        raise ManifestValidationError("missing Foundry artifacts: " + ", ".join(sorted(missing)))
    _attach_build_info_asts(directory, found)
    return found


def _intake_values(intake: dict[str, Any]) -> tuple[list[str], str, str, list[str], list[str], str]:
    roles = intake.get("roles") if isinstance(intake.get("roles"), dict) else {}
    signers, guardian, treasury = roles.get("governanceSigners"), roles.get("guardian"), roles.get("treasury")
    attesters, adjudicators, partner = roles.get("attesters"), roles.get("adjudicators"), roles.get("designPartner")
    if not isinstance(signers, list) or len(signers) != 5:
        raise ManifestValidationError("generation requires exactly five public governance signer addresses")
    for index, value in enumerate(signers):
        if not isinstance(value, str) or ADDRESS_RE.fullmatch(value) is None or int(value[2:], 16) == 0:
            raise ManifestValidationError(f"governance signer {index} must be a non-zero public EVM address")
    if len({x.lower() for x in signers}) != 5:
        raise ManifestValidationError("governance signer addresses must be distinct")
    if not isinstance(guardian, str) or ADDRESS_RE.fullmatch(guardian) is None or int(guardian[2:], 16) == 0 or guardian.lower() in {x.lower() for x in signers}:
        raise ManifestValidationError("guardian must be a non-zero public address separate from every signer")
    if not isinstance(treasury, str) or ADDRESS_RE.fullmatch(treasury) is None or int(treasury[2:], 16) == 0 or treasury.lower() in {x.lower() for x in signers + [guardian]}:
        raise ManifestValidationError("treasury must be a non-zero public address separate from every signer and guardian")
    for label, collection in (("attesters", attesters), ("adjudicators", adjudicators)):
        minimum = 3
        if not isinstance(collection, list) or len(collection) < minimum:
            raise ManifestValidationError(f"generation requires at least {minimum} reviewed {label}")
        for value in collection:
            if not isinstance(value, str) or ADDRESS_RE.fullmatch(value) is None or int(value[2:], 16) == 0:
                raise ManifestValidationError(f"{label} must contain only non-zero public EVM addresses")
        if len({value.lower() for value in collection}) != len(collection):
            raise ManifestValidationError(f"{label} must contain distinct public addresses")
    if len(attesters) != 3:
        raise ManifestValidationError("generation requires exactly three operational attesters")
    if not isinstance(partner, str) or ADDRESS_RE.fullmatch(partner) is None or int(partner[2:], 16) == 0:
        raise ManifestValidationError("generation requires exactly one reviewed public designPartner address")
    privileged = {value.lower() for value in signers + [guardian, treasury] + attesters + adjudicators}
    if partner.lower() in privileged:
        raise ManifestValidationError("designPartner must be separate from governance, guardian, treasury, attesters, and adjudicators")
    approval = intake.get("approval")
    if approval != {"explicitMainnetApproval": True, "approvalEvidence": "owner-supplied-out-of-band"}:
        raise ManifestValidationError("generation requires explicit owner approval record; never infer approval")
    return signers, guardian, treasury, attesters, adjudicators, partner


def generate_package(manifest: dict[str, Any], intake: dict[str, Any], artifacts_dir: Path, deployer: str, starting_nonce: int) -> dict[str, Any]:
    validate_manifest(manifest)
    if manifest.get("status") != "finalization-candidate":
        raise ManifestValidationError(
            "package generation requires a deterministic finalization-candidate manifest; the checked-in template is not deployable"
        )
    signers, guardian, treasury, attesters, adjudicators, partner = _intake_values(intake)
    if not isinstance(deployer, str) or ADDRESS_RE.fullmatch(deployer) is None or int(deployer[2:], 16) == 0:
        raise ManifestValidationError("deployer must be an existing non-zero public address")
    if not isinstance(starting_nonce, int) or starting_nonce < 0:
        raise ManifestValidationError("starting nonce must be a non-negative integer")
    artifacts = _find_artifacts(artifacts_dir)
    addresses = {label: compute_create_address(deployer, starting_nonce + sequence) for sequence, label in enumerate(DEPLOYMENT_ORDER)}
    caps = manifest["caps"]
    compat = {"roles": {"governanceSigners": signers, "guardian": guardian}, "bondFactory": {"immutableNativeCapsWei": {"partnershipParty": caps["partnershipPartyWei"], "partnershipBond": caps["partnershipBondWei"], "partnershipAggregate": caps["partnershipAggregateWei"], "accountabilityBond": caps["accountabilityBondWei"], "accountabilityAggregate": caps["accountabilityAggregateWei"]}}}
    runtime_profile = {
        "caps": caps,
        "roles": {"governanceSigners": signers, "guardian": guardian},
        "runtimeIdentity": {},
    }
    for label, contract in RUNTIME_IDENTITY_CONTRACTS.items():
        runtime = _expected_runtime(artifacts[contract], contract, label, runtime_profile, addresses)
        runtime_profile["runtimeIdentity"][f"expected{label[0].upper()}{label[1:]}RuntimeCodeHash"] = "0x" + _keccak(runtime).hex()
    accountability_constructor = [
        addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000",
        int(caps["accountabilityBondWei"]), int(caps["accountabilityAggregateWei"]), addresses["controller"],
    ]
    accountability_types = _constructor_types(artifacts["AccountabilityBondV3Safe"], "AccountabilityBondV3Safe")
    if not accountability_types:
        accountability_types = ["address", "address", "address", "uint256", "uint256", "address"]
    accountability_init = (
        _hex_bytes(artifacts["AccountabilityBondV3Safe"]["bytecode"]["object"], "AccountabilityBondV3Safe.bytecode.object")
        + abi_encode(accountability_types, accountability_constructor)
    )
    runtime_profile["runtimeIdentity"]["accountabilityChildInitCode"] = "0x" + accountability_init.hex()
    runtime_profile["runtimeIdentity"]["expectedAccountabilityBondInitCodeHash"] = "0x" + _keccak(accountability_init).hex()
    for label, contract in (
        ("partnershipFactory", "PartnershipBondFactoryV3"),
        ("accountabilityFactory", "AccountabilityBondFactoryV3"),
    ):
        runtime = _expected_runtime(artifacts[contract], contract, label, runtime_profile, addresses)
        runtime_profile["runtimeIdentity"][f"expected{label[0].upper()}{label[1:]}RuntimeCodeHash"] = "0x" + _keccak(runtime).hex()
    canonical_policy_targets = [
        addresses["controller"], addresses["consensus"], addresses["bondFactory"],
    ]
    canonical_policy_hashes = [
        "0x" + _keccak(_expected_runtime(artifacts["BasePilotAccessControllerV3"], "BasePilotAccessControllerV3", "controller", runtime_profile, addresses)).hex(),
        "0x" + _keccak(_expected_runtime(artifacts["ThresholdConsensusV3"], "ThresholdConsensusV3", "consensus", runtime_profile, addresses)).hex(),
        "0x" + _keccak(_expected_runtime(artifacts["SafeBondFactoryV3"], "SafeBondFactoryV3", "bondFactory", runtime_profile, addresses)).hex(),
    ]
    values = {
        "governance": [
            signers, 3, 86400, 86400, guardian, 86400, [],
            canonical_policy_targets, canonical_policy_hashes,
        ],
        "controller": [addresses["governance"], int(caps["globalWei"]), int(caps["perPositionWei"])],
        "consensus": [addresses["governance"], 3],
        "bondFactory": [
            addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000", addresses["controller"],
            int(caps["partnershipPartyWei"]), int(caps["partnershipBondWei"]), int(caps["partnershipAggregateWei"]),
            int(caps["accountabilityBondWei"]), int(caps["accountabilityAggregateWei"]),
            runtime_profile["runtimeIdentity"]["expectedPartnershipFactoryRuntimeCodeHash"],
            runtime_profile["runtimeIdentity"]["expectedAccountabilityFactoryRuntimeCodeHash"],
            runtime_profile["runtimeIdentity"]["expectedPartnershipBondRuntimeCodeHash"],
            runtime_profile["runtimeIdentity"]["expectedAccountabilityBondRuntimeCodeHash"],
            runtime_profile["runtimeIdentity"]["expectedAccountabilityBondInitCodeHash"],
        ],
        "partnershipFactory": [addresses["bondFactory"], addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000", int(caps["partnershipPartyWei"]), int(caps["partnershipBondWei"]), int(caps["partnershipAggregateWei"]), addresses["controller"]],
        "accountabilityFactory": [addresses["bondFactory"], addresses["governance"], addresses["consensus"], "0x0000000000000000000000000000000000000000", int(caps["accountabilityBondWei"]), int(caps["accountabilityAggregateWei"]), addresses["controller"], runtime_profile["runtimeIdentity"]["accountabilityChildInitCode"], runtime_profile["runtimeIdentity"]["expectedAccountabilityBondRuntimeCodeHash"]],
    }
    transactions: list[dict[str, Any]] = []
    for sequence, label in enumerate(DEPLOYMENT_ORDER):
        contract = CONTRACT_BY_LABEL[label]
        artifact = artifacts[contract]
        types = _constructor_types(artifact, contract)
        if types != CONSTRUCTOR_TYPES[label]:
            raise ManifestValidationError(f"{contract} constructor ABI drifted from the reviewed bonds-only binding")
        init = _hex_bytes(artifact.get("bytecode", {}).get("object"), f"{contract}.bytecode.object") + abi_encode(types, values[label])
        runtime = _expected_runtime(artifact, contract, label, runtime_profile, addresses)
        transactions.append({"sequence": sequence, "label": label, "contract": contract, "from": deployer, "to": None, "nonce": starting_nonce + sequence, "valueWei": "0", "constructorArgs": values[label], "predictedAddress": addresses[label], "creationCodeKeccak256": "0x" + _keccak(init).hex(), "runtimeCodeKeccak256": "0x" + _keccak(runtime).hex(), "signed": False, "broadcast": False})
    inner = _keccak(b"configureFactories(address,address)")[:4] + abi_encode(
        ["address", "address"], [addresses["partnershipFactory"], addresses["accountabilityFactory"]]
    )
    package = {"schema": "vaultfire.base-mainnet-bonds-only-unsigned-package.v2", "chain": manifest["chain"], "manifestSchema": manifest["schema"], "manifestSchemaVersion": manifest["schemaVersion"], "manifestPayloadSha256": manifest["integrity"]["manifestPayloadSha256"], "provenance": manifest["provenance"], "compiler": manifest["compiler"], "unsigned": True, "broadcast": False, "forbiddenOperations": ["wallet-access", "private-key-input", "signing", "rpc", "broadcast", "funding", "activation"], "addressDerivation": {"method": "CREATE/deployer-nonce", "deployer": deployer, "startingNonce": starting_nonce, "salt": None}, "transactions": transactions, "runtimeIdentity": runtime_profile["runtimeIdentity"], "factoryConfiguration": {"governance": addresses["governance"], "target": addresses["bondFactory"], "valueWei": "0", "signature": "configureFactories(address,address)", "arguments": [addresses["partnershipFactory"], addresses["accountabilityFactory"]], "calldata": "0x" + inner.hex(), "directCallForbidden": True, "requiresProposalConfirmationTimelockAndExecution": True, "signed": False, "broadcast": False}, "roles": {"treasury": treasury, "attesters": attesters, "adjudicators": adjudicators, "designPartner": partner}, "childBondPolicy": manifest["childBondPolicy"], "guardianPauseFlow": manifest["guardianPauseFlow"], "postDeployAssertions": manifest["postDeployAssertions"]}
    package["packageSha256"] = hashlib.sha256(canonical_json(package)).hexdigest()
    return package


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Generate an unsigned, non-broadcast bonds-only Base package.")
    parser.add_argument("--manifest", type=Path, default=Path(__file__).with_name("base_v3_deployment_manifest.json"))
    parser.add_argument("--intake", type=Path, required=True, help="Owner-supplied public-address-only intake; no secrets accepted.")
    parser.add_argument("--artifacts-dir", type=Path, required=True)
    parser.add_argument("--deployer", required=True)
    parser.add_argument("--starting-nonce", type=int, required=True)
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args(argv)
    try:
        package = generate_package(load_json(args.manifest), load_json(args.intake), args.artifacts_dir, args.deployer, args.starting_nonce)
    except ManifestValidationError as exc:
        parser.error(f"FAIL-CLOSED: {exc}")
    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, "deployments": len(package["transactions"])}))
    return 0


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