#!/usr/bin/env python3
"""
Vaultfire Protocol — Contract Verification Script
Python 3.10+ | stdlib only

Verifies that all Vaultfire contracts have deployed bytecode on Base and
Avalanche by querying each chain's RPC endpoint. Optionally calls known view
functions to confirm ABI compatibility.

Usage:
    python3 scripts/verify_contracts.py             # verify all chains
    python3 scripts/verify_contracts.py --chain base
    python3 scripts/verify_contracts.py --chain avalanche
    python3 scripts/verify_contracts.py --json       # machine-readable output
    python3 scripts/verify_contracts.py --chain base --json
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from typing import Any

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ADDRESSES_DIR = os.path.join(REPO_ROOT, "addresses")

# View-function spot checks: (contract_name, function_name, encoded_call_data)
# eth_call with no arguments: 4-byte keccak selector
# getTotalAgents()  -> keccak256("getTotalAgents()")  -> 0x3731a16f
# nextBondId()      -> keccak256("nextBondId()")      -> 0xee53a423
# owner()           -> keccak256("owner()")           -> 0x8da5cb5b
SPOT_CHECKS: list[tuple[str, str, str]] = [
    ("ERC8004IdentityRegistry", "getTotalAgents()", "0x3731a16f"),
    ("AIPartnershipBondsV2",    "nextBondId()",     "0xee53a423"),
    ("AIAccountabilityBondsV2", "nextBondId()",     "0xee53a423"),
    ("MultisigGovernance",      "owner()",          "0x8da5cb5b"),
    ("FlourishingMetricsOracle","owner()",          "0x8da5cb5b"),
]

# Avalanche does not have VaultfireNameService
AVALANCHE_SKIP = {"VaultfireNameService"}

# ---------------------------------------------------------------------------
# RPC helpers
# ---------------------------------------------------------------------------

def rpc_call(rpc_url: str, method: str, params: list) -> Any:
    payload = json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": method,
        "params": params,
    }).encode()
    req = urllib.request.Request(
        rpc_url,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=15) as resp:
        data = json.loads(resp.read())
    if "error" in data:
        raise RuntimeError(f"RPC error: {data['error']}")
    return data.get("result")


def get_bytecode(rpc_url: str, address: str) -> str:
    return rpc_call(rpc_url, "eth_getCode", [address, "latest"])


def eth_call(rpc_url: str, to: str, data: str) -> str:
    return rpc_call(rpc_url, "eth_call", [{"to": to, "data": data}, "latest"])


# ---------------------------------------------------------------------------
# Verification logic
# ---------------------------------------------------------------------------

def load_chain_config(chain: str) -> dict:
    path = os.path.join(ADDRESSES_DIR, f"{chain}.json")
    with open(path) as f:
        return json.load(f)


def verify_chain(chain: str, verbose: bool = True) -> dict:
    cfg = load_chain_config(chain)
    rpc = cfg["rpc"]
    contracts = cfg["contracts"]
    results = {}

    for name, address in contracts.items():
        if chain == "avalanche" and name in AVALANCHE_SKIP:
            continue

        result: dict[str, Any] = {"address": address, "bytecode": False, "spot_check": None, "error": None}

        try:
            code = get_bytecode(rpc, address)
            has_code = bool(code and code != "0x" and len(code) > 2)
            result["bytecode"] = has_code
            if not has_code:
                result["error"] = "No bytecode at address"
        except Exception as e:
            result["error"] = str(e)

        # Spot check
        for sc_contract, sc_func, sc_data in SPOT_CHECKS:
            if sc_contract == name and result["bytecode"]:
                try:
                    ret = eth_call(rpc, address, sc_data)
                    result["spot_check"] = {
                        "function": sc_func,
                        "success": True,
                        "raw_result": ret,
                    }
                except Exception as e:
                    result["spot_check"] = {
                        "function": sc_func,
                        "success": False,
                        "error": str(e),
                    }

        results[name] = result

    return {
        "chain": chain,
        "chainId": cfg["chainId"],
        "rpc": rpc,
        "results": results,
    }


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

COL_NAME  = 42
COL_ADDR  = 44
COL_BYTEC = 9
COL_CHECK = 12

def print_header(chain_name: str, chain_id: int, rpc: str) -> None:
    line = "=" * 115
    print(f"\n{line}")
    print(f"  Chain: {chain_name}  (chainId {chain_id})")
    print(f"  RPC:   {rpc}")
    print(line)
    print(
        f"  {'Contract':<{COL_NAME}} {'Address':<{COL_ADDR}} {'Bytecode':<{COL_BYTEC}} {'Spot Check':<{COL_CHECK}}"
    )
    print("-" * 115)


def status_sym(ok: bool) -> str:
    return "PASS" if ok else "FAIL"


def print_results(chain_data: dict) -> None:
    cfg = load_chain_config(chain_data["chain"])
    print_header(cfg["chainName"], chain_data["chainId"], chain_data["rpc"])
    failed = 0
    for name, r in chain_data["results"].items():
        bc = r["bytecode"]
        sc = r.get("spot_check")
        sc_str = ""
        if sc is not None:
            sc_str = f"{sc['function']} {'OK' if sc['success'] else 'ERR'}"
        addr_short = r["address"]
        err_note = f"  <- {r['error']}" if r["error"] else ""

        line = (
            f"  {name:<{COL_NAME}} {addr_short:<{COL_ADDR}}"
            f" {status_sym(bc):<{COL_BYTEC}} {sc_str:<{COL_CHECK}}{err_note}"
        )
        print(line)
        if not bc:
            failed += 1

    print("-" * 115)
    total = len(chain_data["results"])
    passed = total - failed
    status = "ALL PASS" if failed == 0 else f"{failed} FAILED"
    print(f"  Result: {passed}/{total} contracts verified  [{status}]\n")
    return failed


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> int:
    parser = argparse.ArgumentParser(description="Vaultfire contract verification tool")
    parser.add_argument("--chain", choices=["base", "avalanche"], help="Verify a single chain only")
    parser.add_argument("--json", action="store_true", help="Output JSON instead of table")
    args = parser.parse_args()

    chains = [args.chain] if args.chain else ["base", "avalanche"]

    all_data = []
    for chain in chains:
        if not args.json:
            print(f"Connecting to {chain}...", end=" ", flush=True)
        try:
            data = verify_chain(chain)
            all_data.append(data)
            if not args.json:
                print("OK")
        except Exception as e:
            if not args.json:
                print(f"ERROR: {e}")
            all_data.append({"chain": chain, "error": str(e), "results": {}})

    if args.json:
        print(json.dumps(all_data, indent=2))
        # Exit 1 if any contract failed
        for chain_data in all_data:
            for r in chain_data.get("results", {}).values():
                if not r["bytecode"]:
                    return 1
        return 0

    total_failed = 0
    for chain_data in all_data:
        if "error" in chain_data and not chain_data.get("results"):
            print(f"\nChain {chain_data['chain']} unreachable: {chain_data['error']}")
            total_failed += 1
        else:
            total_failed += print_results(chain_data)

    return 0 if total_failed == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
