#!/usr/bin/env python3
"""Independently verify one owner-supplied Base x402 payment evidence file.

This tool is read-only: it never signs, sends, or broadcasts transactions.
The submitted hash is untrusted input.  It verifies chain ID, receipt status,
inclusion, confirmation depth, and the exact ERC-20 Transfer log to the stated
payee/amount/asset.  Route, nonce, authorization, and timing attestations are
off-chain facts and are reported as not proven by Base RPC.
"""
from __future__ import annotations

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

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


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"RPC {name} was 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"}, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=15) 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 validate(evidence: Any) -> dict[str, Any]:
    if not isinstance(evidence, dict):
        fail("evidence must be a JSON object")
    required = {"schema", "ownerAttestation", "route", "payment", "transactionHash", "finalityPolicy"}
    if not required.issubset(evidence):
        fail("evidence is missing required fields")
    if evidence["schema"] != "vaultfire.x402.v2-live-evidence.v1":
        fail("unsupported evidence schema")
    if not isinstance(evidence.get("evidenceId"), str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,160}", evidence["evidenceId"]):
        fail("invalid evidenceId")
    if not isinstance(evidence["ownerAttestation"], dict) or not all(evidence["ownerAttestation"].get(k) for k in ("suppliedBy", "approvedAt", "authorization")):
        fail("owner attestation is absent or incomplete; refusing to treat evidence as live")
    owner = evidence["ownerAttestation"]
    if any("OWNER_SUPPLIED" in str(owner[key]) or str(owner[key]).startswith("YYYY-") for key in ("suppliedBy", "approvedAt", "authorization")):
        fail("owner attestation still contains template placeholders; refusing to treat evidence as live")
    route = evidence["route"]
    payment = evidence["payment"]
    if not isinstance(route, dict) or route.get("protocolVersion") != "v2" or not re.fullmatch(r"[A-Z]+", str(route.get("method", ""))) or not str(route.get("path", "")).startswith("/"):
        fail("invalid V2 route evidence")
    if not isinstance(payment, dict) or payment.get("x402Version") != 2 or payment.get("network") != "eip155:8453" or payment.get("chainId") != BASE_CHAIN_ID:
        fail("evidence is not a Base x402 v2 payment")
    for field in ("asset", "payee"):
        if not isinstance(payment.get(field), str) or not ADDRESS.fullmatch(payment[field]):
            fail(f"invalid payment {field}")
    if not isinstance(payment.get("amount"), str) or not re.fullmatch(r"[1-9][0-9]*", payment["amount"]):
        fail("invalid atomic payment amount")
    if not HASH.fullmatch(str(evidence["transactionHash"])):
        fail("transactionHash must be exactly 32 bytes of hex; it is untrusted until RPC verifies it")
    if str(evidence["transactionHash"]).lower() == "0x" + "0" * 64:
        fail("zero transactionHash is a template placeholder, not live evidence")
    finality = evidence["finalityPolicy"]
    if not isinstance(finality, dict) or not isinstance(finality.get("requiredConfirmations"), int) or finality["requiredConfirmations"] < 1:
        fail("finalityPolicy.requiredConfirmations must be a positive integer")
    return evidence


def transfer_matches(log: Any, asset: str, payee: str, amount: int) -> bool:
    if not isinstance(log, dict) or str(log.get("address", "")).lower() != asset.lower():
        return False
    topics = log.get("topics")
    if not isinstance(topics, list) or len(topics) < 3 or str(topics[0]).lower() != TRANSFER_TOPIC:
        return False
    expected_to = "0x" + "0" * 24 + payee[2:].lower()
    if str(topics[2]).lower() != expected_to:
        return False
    try:
        return quantity(log.get("data"), "Transfer data") == amount
    except ValueError:
        return False


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--evidence", required=True, type=Path)
    parser.add_argument("--rpc-url", default=os.getenv("BASE_RPC_URL"))
    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
    try:
        evidence = validate(json.loads(args.evidence.read_text(encoding="utf-8")))
        chain_id = quantity(rpc(args.rpc_url, "eth_chainId", []), "eth_chainId")
        if chain_id != BASE_CHAIN_ID:
            fail(f"RPC chain ID {chain_id} is not Base ({BASE_CHAIN_ID})")
        tx_hash = evidence["transactionHash"]
        receipt = rpc(args.rpc_url, "eth_getTransactionReceipt", [tx_hash])
        transaction = rpc(args.rpc_url, "eth_getTransactionByHash", [tx_hash])
        if not isinstance(receipt, dict) or not isinstance(transaction, dict):
            fail("transaction is absent from Base RPC")
        if receipt.get("transactionHash", "").lower() != tx_hash.lower() or transaction.get("hash", "").lower() != tx_hash.lower():
            fail("RPC returned a transaction different from the untrusted submitted hash")
        if quantity(receipt.get("status"), "receipt status") != 1:
            fail("transaction reverted")
        block_number = quantity(receipt.get("blockNumber"), "receipt blockNumber")
        head = quantity(rpc(args.rpc_url, "eth_blockNumber", []), "eth_blockNumber")
        confirmations = head - block_number + 1
        if confirmations < evidence["finalityPolicy"]["requiredConfirmations"]:
            fail(f"only {confirmations} confirmations; policy requires {evidence['finalityPolicy']['requiredConfirmations']}")
        payment = evidence["payment"]
        logs = receipt.get("logs")
        if not isinstance(logs, list) or not any(transfer_matches(log, payment["asset"], payment["payee"], int(payment["amount"])) for log in logs):
            fail("no exact ERC-20 Transfer(asset, payee, amount) log was found")
        if payment.get("payer") and transaction.get("from", "").lower() != payment["payer"].lower():
            fail("transaction sender does not match optional evidence payer")
        print(json.dumps({
            "ok": True,
            "network": "eip155:8453",
            "transactionHash": tx_hash,
            "blockNumber": block_number,
            "confirmations": confirmations,
            "provenByBaseRpc": ["chainId", "receiptStatus", "inclusion", "confirmationDepth", "erc20TransferAssetPayeeAmount"],
            "notProvenByBaseRpc": ["route.method", "route.path", "authorizationId", "nonce", "validAfter", "expiresAt", "owner authorization"],
        }, sort_keys=True))
        return 0
    except (OSError, ValueError, RuntimeError) as exc:
        print(f"verification failed closed: {exc}", file=sys.stderr)
        return 1


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