#!/usr/bin/env python3
"""Read-only Base RPC verification for the bounded V2 USDC payee scan.

Every transaction hash and log coordinate in the input is treated as
untrusted. The verifier checks Base chain ID, receipt success, canonical block
identity, confirmation depth, and the exact USDC Transfer log, including
payer, payee, and atomic amount. It never signs, sends, or broadcasts.
"""
from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

BASE_CHAIN_ID = 8453
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
HASH = re.compile(r"^0x[0-9a-fA-F]{64}$")
ADDRESS = re.compile(r"^0x[0-9a-fA-F]{40}$")


def fail(message: str) -> None:
    raise ValueError(message)


def quantity(value: Any, name: str) -> int:
    if not isinstance(value, str) or not re.fullmatch(r"0x[0-9a-fA-F]+", value):
        fail(f"{name} is not a hex quantity")
    return int(value, 16)


def rpc(url: str, method: str, params: list[Any]) -> Any:
    payload = json.dumps(
        {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
    ).encode()
    request = urllib.request.Request(
        url,
        data=payload,
        headers={
            "Content-Type": "application/json",
            "User-Agent": "Vaultfire-V2-Evidence-Verifier/1.0",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            body = json.loads(response.read().decode())
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
        raise RuntimeError(f"Base RPC request failed for {method}: {exc}") from exc
    if not isinstance(body, dict) or body.get("error") or "result" not in body:
        raise RuntimeError(f"Base RPC returned an error for {method}: {body!r}")
    return body["result"]


def topic_address(value: Any, name: str) -> str:
    if not isinstance(value, str) or not re.fullmatch(r"0x[0-9a-fA-F]{64}", value):
        fail(f"{name} is not a 32-byte topic")
    if value[2:26] != "0" * 24:
        fail(f"{name} is not a canonical address topic")
    return "0x" + value[-40:].lower()


def load_expected(path: Path) -> tuple[str, str, list[dict[str, Any]]]:
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("schema") != "vaultfire.x402.v2-onchain-payee-scan.v1":
        fail("unsupported input schema")
    source = data.get("source")
    rail = data.get("paymentRail")
    if not isinstance(source, dict) or not isinstance(rail, dict):
        fail("input source or paymentRail is absent")
    asset = str(rail.get("asset", ""))
    payee = str(rail.get("payee", ""))
    if not ADDRESS.fullmatch(asset) or not ADDRESS.fullmatch(payee):
        fail("input asset or payee is invalid")
    records: list[dict[str, Any]] = []
    for group in data.get("externalPayments", []):
        payer = str(group.get("payer", "")).lower()
        if not ADDRESS.fullmatch(payer):
            fail("external payer is invalid")
        for transaction in group.get("transactions", []):
            tx_hash = str(transaction.get("transactionHash", ""))
            if not HASH.fullmatch(tx_hash) or tx_hash.lower() == "0x" + "0" * 64:
                fail("input contains an invalid transaction hash")
            records.append(
                {
                    "transactionHash": tx_hash.lower(),
                    "expectedPayer": payer,
                    "expectedAmountAtomic": int(transaction["amountAtomic"]),
                    "expectedBlockNumber": quantity(
                        transaction["blockNumber"], "input blockNumber"
                    ),
                    "expectedBlockHash": str(transaction["blockHash"]).lower(),
                    "expectedLogIndex": quantity(
                        transaction["logIndex"], "input logIndex"
                    ),
                }
            )
    if len(records) != data.get("externalPaymentCount"):
        fail("input transaction count does not match externalPaymentCount")
    if len({record["transactionHash"] for record in records}) != len(records):
        fail("input contains duplicate transaction hashes")
    return asset.lower(), payee.lower(), records


def verify_record(
    url: str,
    head: int,
    required_confirmations: int,
    asset: str,
    payee: str,
    expected: dict[str, Any],
) -> dict[str, Any]:
    tx_hash = expected["transactionHash"]
    receipt = rpc(url, "eth_getTransactionReceipt", [tx_hash])
    transaction = rpc(url, "eth_getTransactionByHash", [tx_hash])
    if not isinstance(receipt, dict) or not isinstance(transaction, dict):
        fail(f"{tx_hash}: transaction or receipt is absent")
    if (
        str(receipt.get("transactionHash", "")).lower() != tx_hash
        or str(transaction.get("hash", "")).lower() != tx_hash
    ):
        fail(f"{tx_hash}: RPC response identity mismatch")
    if quantity(receipt.get("status"), "receipt status") != 1:
        fail(f"{tx_hash}: transaction reverted")
    block_number = quantity(receipt.get("blockNumber"), "receipt blockNumber")
    block_hash = str(receipt.get("blockHash", "")).lower()
    if (
        block_number != expected["expectedBlockNumber"]
        or block_hash != expected["expectedBlockHash"]
    ):
        fail(f"{tx_hash}: canonical block differs from the supplied log coordinate")
    confirmations = head - block_number + 1
    if confirmations < required_confirmations:
        fail(
            f"{tx_hash}: {confirmations} confirmations, "
            f"{required_confirmations} required"
        )
    logs = receipt.get("logs")
    if not isinstance(logs, list):
        fail(f"{tx_hash}: receipt logs are absent")
    matches: list[dict[str, Any]] = []
    for log in logs:
        if (
            not isinstance(log, dict)
            or str(log.get("address", "")).lower() != asset
            or quantity(log.get("logIndex"), "logIndex")
            != expected["expectedLogIndex"]
        ):
            continue
        topics = log.get("topics")
        if (
            not isinstance(topics, list)
            or len(topics) < 3
            or str(topics[0]).lower() != TRANSFER_TOPIC
        ):
            continue
        matches.append(
            {
                "payer": topic_address(topics[1], "Transfer payer"),
                "payee": topic_address(topics[2], "Transfer payee"),
                "amountAtomic": quantity(log.get("data"), "Transfer amount"),
            }
        )
    if len(matches) != 1:
        fail(f"{tx_hash}: expected exactly one matching Transfer log coordinate")
    transfer = matches[0]
    if (
        transfer["payer"] != expected["expectedPayer"]
        or transfer["payee"] != payee
        or transfer["amountAtomic"] != expected["expectedAmountAtomic"]
    ):
        fail(f"{tx_hash}: Transfer payer, payee, or amount mismatch")
    block = rpc(url, "eth_getBlockByHash", [block_hash, False])
    if not isinstance(block, dict) or str(block.get("hash", "")).lower() != block_hash:
        fail(f"{tx_hash}: canonical block lookup failed")
    timestamp = quantity(block.get("timestamp"), "block timestamp")
    return {
        "transactionHash": tx_hash,
        "blockNumber": block_number,
        "blockHash": block_hash,
        "blockTimestamp": timestamp,
        "blockTimestampUtc": dt.datetime.fromtimestamp(
            timestamp, tz=dt.timezone.utc
        ).isoformat().replace("+00:00", "Z"),
        "receiptStatus": "success",
        "confirmationsAtVerification": confirmations,
        "asset": asset,
        "payer": transfer["payer"],
        "payee": transfer["payee"],
        "amountAtomic": str(transfer["amountAtomic"]),
        "logIndex": expected["expectedLogIndex"],
        "finalityPolicyResult": "pass",
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--rpc-url", default=os.getenv("BASE_RPC_URL"))
    parser.add_argument("--required-confirmations", type=int, default=64)
    args = parser.parse_args()
    if not args.rpc_url:
        print("BASE_RPC_URL or --rpc-url is required; no network call was made.", file=sys.stderr)
        return 2
    if args.required_confirmations < 1:
        print("required confirmations must be positive", file=sys.stderr)
        return 2
    try:
        asset, payee, expected = load_expected(args.input)
        chain_id = quantity(rpc(args.rpc_url, "eth_chainId", []), "chainId")
        if chain_id != BASE_CHAIN_ID:
            fail(f"RPC chain ID {chain_id} is not Base ({BASE_CHAIN_ID})")
        head = quantity(rpc(args.rpc_url, "eth_blockNumber", []), "head")
        records = [
            verify_record(
                args.rpc_url,
                head,
                args.required_confirmations,
                asset,
                payee,
                record,
            )
            for record in expected
        ]
        total = sum(int(record["amountAtomic"]) for record in records)
        output = {
            "$schema": "../schemas/x402-v2-onchain-independent-verification.schema.json",
            "schema": "vaultfire.x402.v2-onchain-independent-verification.v1",
            "generatedAtUtc": dt.datetime.now(tz=dt.timezone.utc)
            .isoformat()
            .replace("+00:00", "Z"),
            "network": "eip155:8453",
            "chainId": BASE_CHAIN_ID,
            "verificationHead": head,
            "requiredConfirmations": args.required_confirmations,
            "asset": asset,
            "payee": payee,
            "transactionCount": len(records),
            "totalAmountAtomic": str(total),
            "fullyApplicationCorrelatedCount": 0,
            "activation": "fail-closed-no-route-or-request-receipt-correlation",
            "provenByBaseRpc": [
                "chainId",
                "transactionAndReceiptIdentity",
                "receiptSuccess",
                "canonicalBlockIdentity",
                "confirmationDepth",
                "erc20TransferAssetPayerPayeeAmount",
            ],
            "notProven": [
                "HTTP route and method",
                "route price",
                "request, payment, authorization, nonce, and idempotency identifiers",
                "facilitator verification result",
                "payment-response receipt",
                "Coinbase Bazaar discovery",
                "external-wallet invocation",
            ],
            "records": records,
        }
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(json.dumps(output, indent=2) + "\n", encoding="utf-8")
        print(
            json.dumps(
                {
                    "ok": True,
                    "chainId": BASE_CHAIN_ID,
                    "transactionCount": len(records),
                    "totalAmountAtomic": str(total),
                    "fullyApplicationCorrelatedCount": 0,
                    "output": str(args.output),
                },
                sort_keys=True,
            )
        )
        return 0
    except (OSError, ValueError, RuntimeError, KeyError, TypeError) as exc:
        print(f"verification failed closed: {exc}", file=sys.stderr)
        return 1


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