#!/usr/bin/env python3
"""
Fetch onchain state for every V2 contract:
  - deployed bytecode (sha256 hash for cross-chain equivalence)
  - native balance
  - EIP-1967 proxy implementation slot (to detect upgradable proxies)
  - EIP-1967 admin slot
  - owner() call (best-effort, ownership standard)
  - paused() call (best-effort)

This is the ground truth for the compatibility baseline. Bytecode hashes
tell us which contracts are byte-identical across chains and which are forks.
"""
import json
import hashlib
import concurrent.futures as cf
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError

BASELINE = Path(__file__).parent
DEP = json.load(open(BASELINE / "deployments.json"))

# Multiple RPCs per chain for fallback
RPCS = {
    "base": [
        "https://mainnet.base.org",
        "https://base.llamarpc.com",
        "https://1rpc.io/base",
    ],
    "avalanche": [
        "https://api.avax.network/ext/bc/C/rpc",
        "https://avalanche.public-rpc.com",
        "https://1rpc.io/avax/c",
    ],
    "arbitrum": [
        "https://arb1.arbitrum.io/rpc",
        "https://arbitrum.llamarpc.com",
        "https://1rpc.io/arb",
    ],
    "polygon": [
        "https://polygon.llamarpc.com",
        "https://polygon-bor-rpc.publicnode.com",
        "https://polygon.drpc.org",
        "https://1rpc.io/matic",
    ],
}

# EIP-1967 storage slots
IMPL_SLOT   = "0x360894a13ba1a3210667c828492db98dcbf83a3c93e0f0e6de74e10e00c50fb2"  # bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
ADMIN_SLOT  = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"

# 4-byte selectors
SEL_OWNER   = "0x8da5cb5b"  # owner()
SEL_PAUSED  = "0x5c975abb"  # paused()

def rpc_call(rpc_url: str, method: str, params: list, timeout=25):
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    req = Request(rpc_url, data=body,
                  headers={"content-type": "application/json",
                           "user-agent": "vaultfire-baseline/1.0"})
    with urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())

def rpc_with_fallback(chain: str, method: str, params: list):
    last_err = None
    for url in RPCS[chain]:
        try:
            r = rpc_call(url, method, params)
            if "result" in r:
                return r["result"], url
            last_err = r.get("error", "no result")
        except Exception as e:
            last_err = str(e)
            continue
    return None, f"all RPCs failed: {last_err}"

def fetch_one(entry: dict) -> dict:
    chain = entry["chain"]
    addr  = entry["address"]
    out = {
        "name": entry["name"], "chain": chain, "address": addr,
        "bytecode_bytes": 0, "bytecode_sha256": None, "has_code": False,
        "native_balance_wei": None,
        "eip1967_impl": None, "eip1967_admin": None, "eip1967_beacon": None,
        "is_proxy": False,
        "owner": None, "paused": None,
        "errors": [],
    }

    code, err = rpc_with_fallback(chain, "eth_getCode", [addr, "latest"])
    if code is None:
        out["errors"].append(f"eth_getCode: {err}")
        return out
    if code == "0x":
        out["errors"].append("no bytecode at address")
        return out
    raw = bytes.fromhex(code[2:])
    out["bytecode_bytes"] = len(raw)
    out["bytecode_sha256"] = hashlib.sha256(raw).hexdigest()
    out["has_code"] = True

    bal, err = rpc_with_fallback(chain, "eth_getBalance", [addr, "latest"])
    if bal:
        out["native_balance_wei"] = int(bal, 16)
    else:
        out["errors"].append(f"eth_getBalance: {err}")

    # EIP-1967 slots
    for slot_name, slot in (("eip1967_impl", IMPL_SLOT), ("eip1967_admin", ADMIN_SLOT), ("eip1967_beacon", BEACON_SLOT)):
        val, err = rpc_with_fallback(chain, "eth_getStorageAt", [addr, slot, "latest"])
        if val and int(val, 16) != 0:
            # last 20 bytes = address
            out[slot_name] = "0x" + val[-40:]
            out["is_proxy"] = True

    # owner()
    call, err = rpc_with_fallback(chain, "eth_call", [{"to": addr, "data": SEL_OWNER}, "latest"])
    if call and call != "0x" and len(call) >= 66:
        out["owner"] = "0x" + call[-40:]

    # paused()
    call, err = rpc_with_fallback(chain, "eth_call", [{"to": addr, "data": SEL_PAUSED}, "latest"])
    if call and call != "0x" and len(call) >= 66:
        out["paused"] = int(call, 16) != 0

    return out

targets = list({(c["chainId"], c["address"]): c for c in DEP["contracts"]}.values())
print(f"Fetching onchain state for {len(targets)} contracts...")

results = []
with cf.ThreadPoolExecutor(max_workers=16) as pool:
    futs = {pool.submit(fetch_one, e): e for e in targets}
    for i, f in enumerate(cf.as_completed(futs), 1):
        r = f.result()
        results.append(r)
        proxy = "PROXY" if r["is_proxy"] else "-----"
        owned = r["owner"][:10] + "..." if r["owner"] else "no-owner"
        bal = r["native_balance_wei"]
        bal_s = f"{bal/1e18:.4f}E" if bal is not None else "??"
        code = r["bytecode_bytes"] if r["has_code"] else 0
        err = f" ERR:{r['errors'][0][:40]}" if r["errors"] else ""
        print(f"  [{i:3d}/{len(targets)}] {r['chain']:10s} {r['name']:38s} code={code:5d} {proxy} owner={owned} bal={bal_s}{err}")

# Summary + cross-chain equivalence table
by_name_hashes = {}
for r in results:
    if r["bytecode_sha256"]:
        by_name_hashes.setdefault(r["name"], {})[r["chain"]] = r["bytecode_sha256"]

print("\nCross-chain bytecode equivalence (by contract name):")
identical = split = 0
for name, hashes in sorted(by_name_hashes.items()):
    uniq = set(hashes.values())
    tag = "IDENTICAL" if len(uniq) == 1 else f"SPLIT into {len(uniq)}"
    if len(uniq) == 1: identical += 1
    else: split += 1
    chains = ",".join(sorted(hashes.keys()))
    print(f"  {tag:14s} {name:38s} chains={chains}")

print(f"\n  identical across all deployed chains: {identical}")
print(f"  bytecode splits (chain forks):        {split}")

# Update deployments.json
by_key = {(r["chain"], r["address"]): r for r in results}
for c in DEP["contracts"]:
    r = by_key.get((c["chain"], c["address"]))
    if not r: continue
    c["onchain"] = {
        "bytecodeBytes": r["bytecode_bytes"],
        "bytecodeSha256": r["bytecode_sha256"],
        "hasCode": r["has_code"],
        "nativeBalanceWei": r["native_balance_wei"],
        "isProxy": r["is_proxy"],
        "eip1967Impl": r["eip1967_impl"],
        "eip1967Admin": r["eip1967_admin"],
        "eip1967Beacon": r["eip1967_beacon"],
        "owner": r["owner"],
        "paused": r["paused"],
        "errors": r["errors"],
    }
    c["reconciled"]["bytecodeHashed"] = r["has_code"]
    c["reconciled"]["ownerRead"] = r["owner"] is not None
    c["reconciled"]["balanceRead"] = r["native_balance_wei"] is not None
    c["reconciled"]["pausedRead"] = r["paused"] is not None

(BASELINE / "deployments.json").write_text(json.dumps(DEP, indent=2))
(BASELINE / "onchain_fetch_report.json").write_text(json.dumps({
    "totals": {
        "attempted": len(results),
        "hasCode": sum(1 for r in results if r["has_code"]),
        "proxies": sum(1 for r in results if r["is_proxy"]),
        "ownerReadable": sum(1 for r in results if r["owner"]),
        "pausedReadable": sum(1 for r in results if r["paused"] is not None),
        "identicalAcrossChains": identical,
        "chainSplits": split,
    },
    "results": results,
    "byNameHashes": by_name_hashes,
}, indent=2))
print("\nUpdated deployments.json + onchain_fetch_report.json")
