"""Illustrative HeadlessDomains -> machine.checkout.best Base payment client.

This file is never run by the test suite and never contains a key. Running it
would authorize real USDC, so do so only under an explicit payment approval.
"""

import os
import hashlib
import json
import re
import secrets
import time
from decimal import Decimal
from pathlib import Path
import tempfile
from urllib.parse import urlparse

import requests
from eth_account import Account
from eth_account.messages import encode_typed_data


API_BASE = "https://headlessdomains.com"
BASE_CHAIN_ID = 8453
BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
PENDING_SETTLEMENT_STATES = {"prepared", "broadcast", "pending"}
RECOVERY_FIELDS = (
    "session_id", "session_token", "settle_url", "receipt_url", "expires_at",
    "payment_challenges",
)


def required(name):
    value = str(os.environ.get(name) or "").strip()
    if not value:
        raise RuntimeError(f"Missing {name}")
    return value


def require_https(value, field):
    parsed = urlparse(str(value))
    if parsed.scheme != "https" or not parsed.netloc:
        raise RuntimeError(f"Refusing non-HTTPS {field}")


def recovery_file_path():
    configured = str(os.environ.get("HEADLESSDOMAINS_RECOVERY_FILE") or "").strip()
    if configured:
        return Path(configured).expanduser()
    return Path.home() / ".local" / "state" / "headlessdomains" / "base-mpp-recovery.json"


def _write_recovery_envelope(envelope):
    """Atomically write recovery state with owner-only permissions."""
    target = recovery_file_path()
    target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    os.chmod(target.parent, 0o700)
    fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
    try:
        os.fchmod(fd, 0o600)
        with os.fdopen(fd, "w", encoding="utf-8") as stream:
            json.dump(envelope, stream, sort_keys=True)
            stream.write("\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, target)
        os.chmod(target, 0o600)
    except Exception:
        try:
            os.unlink(temporary)
        except FileNotFoundError:
            pass
        raise
    return target


def persist_recovery_envelope(checkout, request_body, quote, *, client_reference=None):
    """Atomically persist the one-time capability before challenge validation."""
    envelope = {
        "version": "headlessdomains-base-mpp-recovery.v1",
        **{field: checkout.get(field) for field in RECOVERY_FIELDS},
        "binding": {
            "request": request_body,
            "domain": f"{request_body['domain']}.{request_body['namespace']}",
            "quote": quote,
            "quote_sha256": hashlib.sha256(
                json.dumps(quote, sort_keys=True, separators=(",", ":")).encode()
            ).hexdigest(),
        },
        "state": "payment_required",
        "client_reference": client_reference,
    }
    return _write_recovery_envelope(envelope)


def load_recovery_envelope(request_body):
    """Load an unfinished request without creating a new checkout session."""
    target = recovery_file_path()
    if not target.exists():
        return None
    if target.stat().st_mode & 0o077:
        raise RuntimeError("Recovery file must have mode 0600")
    with target.open(encoding="utf-8") as stream:
        envelope = json.load(stream)
    if envelope.get("version") != "headlessdomains-base-mpp-recovery.v1":
        raise RuntimeError("Unsupported recovery envelope version")
    if envelope.get("binding", {}).get("request") != request_body:
        raise RuntimeError("Unfinished recovery belongs to a different request")
    if envelope.get("state") == "complete":
        return None
    missing = [field for field in RECOVERY_FIELDS if not envelope.get(field)]
    if missing:
        raise RuntimeError(f"Recovery envelope is missing {', '.join(missing)}")
    return envelope


def update_recovery_envelope(envelope, state, **values):
    envelope.update(values)
    envelope["state"] = state
    _write_recovery_envelope(envelope)


def complete_recovery_envelope(envelope, receipt, registration, status):
    """Replace spend-capable recovery data with minimal completion evidence."""
    completed = {
        "version": envelope["version"],
        "state": "complete",
        "completed_at": int(time.time()),
        "binding": {
            "request": envelope["binding"]["request"],
            "domain": envelope["binding"]["domain"],
            "quote_sha256": envelope["binding"]["quote_sha256"],
        },
        "completion": {
            "session_id": envelope["session_id"],
            "order_id": registration.get("order_id") or status.get("order_id"),
            "domain_id": registration.get("domain_id"),
            "domain_status": registration.get("domain_status"),
            "order_status": status.get("status"),
            "network": receipt.get("network"),
            "asset": receipt.get("asset"),
            "amount_minor": receipt.get("amount"),
            "transaction": receipt.get("transaction"),
        },
    }
    _write_recovery_envelope(completed)


def quote_amount_minor(quote):
    """Convert the canonical six-decimal authenticated quote to minor units."""
    raw_amount = (quote.get("price") or {}).get("amount")
    if isinstance(raw_amount, bool) or raw_amount is None or raw_amount == "":
        raise RuntimeError("Quote amount is missing or invalid")
    try:
        value = Decimal(str(raw_amount))
    except Exception as exc:
        raise RuntimeError("Quote amount is not valid USDC") from exc
    scaled = value * Decimal(1_000_000)
    if not value.is_finite() or value <= 0 or scaled != scaled.to_integral_value():
        raise RuntimeError("Quote amount must be positive USDC with at most six decimals")
    return int(scaled)


def runtime_signing_domain(challenge):
    extra = challenge.get("extra") or {}
    name = str(extra.get("name") or "")
    version = str(extra.get("version") or "")
    if not name or not version:
        raise RuntimeError("Challenge omitted the runtime EIP-712 signing domain")
    return name, version


def wait_for_confirmation(receipt_url, session_token, *, timeout_seconds=300):
    """Poll one existing session; never creates or signs another payment."""
    deadline = time.monotonic() + timeout_seconds
    headers = {"X-Checkout-Session-Token": session_token}
    last_status = None
    while time.monotonic() < deadline:
        response = requests.get(receipt_url, headers=headers, timeout=30)
        response.raise_for_status()
        receipt = response.json()
        last_status = receipt.get("status")
        if last_status == "confirmed":
            return receipt
        if last_status not in PENDING_SETTLEMENT_STATES:
            raise RuntimeError(f"Settlement entered terminal state: {last_status}")
        time.sleep(3)
    raise RuntimeError(
        f"Settlement remains {last_status}; preserve this session and reconcile later"
    )


def main():
    api_key = required("HEADLESSDOMAINS_API_KEY")
    # This is the customer's EIP-3009 signer, not the machine-checkout relayer.
    # Prefer loading it from an OS keychain in a real client.
    account = Account.from_key(required("BASE_PAYER_PRIVATE_KEY"))
    expected_receiver = required("EXPECTED_RECIPIENT")
    max_usdc = Decimal(required("MAX_USDC"))
    request_body = {
        "domain": required("DOMAIN_LABEL").lower(), "namespace": "agent",
        "years": 1, "payment_method": "mpp", "mpp_network": "base-mainnet",
        "agreed_to_terms": True,
    }
    merchant_headers = {
        "X-API-Key": api_key, "X-Payment-Method": "mpp",
        "X-MPP-Network": "base-mainnet", "Content-Type": "application/json",
    }
    recovery_envelope = load_recovery_envelope(request_body)
    if recovery_envelope:
        # A retry/restart resumes the stored capability. It does not quote,
        # register, create a second session, or create a second authorization.
        checkout_data = {field: recovery_envelope[field] for field in RECOVERY_FIELDS}
        quote_data = recovery_envelope["binding"]["quote"]
        client_reference = recovery_envelope["client_reference"]
    else:
        client_reference = secrets.token_urlsafe(24)
        quote = requests.post(
            f"{API_BASE}/api/v1/domains/quote", json=request_body,
            headers=merchant_headers, timeout=30,
        )
        quote.raise_for_status()
        quote_data = quote.json()
        if quote_data.get("mutation") != "none" or quote_data.get("available") is not True:
            raise RuntimeError("Quote is not non-mutating and available")
        if quote_data.get("chain_id") != BASE_CHAIN_ID:
            raise RuntimeError("Quote is not for Base mainnet")
        if str(quote_data.get("recipient", "")).lower() != expected_receiver.lower():
            raise RuntimeError("Quote receiver does not match EXPECTED_RECIPIENT")
        require_https(quote_data["next"]["url"], "quote next.url")
        if Decimal(quote_amount_minor(quote_data)) / Decimal(1_000_000) > max_usdc:
            raise RuntimeError("Quote exceeds MAX_USDC")

        checkout = requests.post(
            f"{API_BASE}/api/v1/domains/register", json=request_body,
            headers=merchant_headers, timeout=30,
        )
        if checkout.status_code != 402:
            checkout.raise_for_status()
            raise RuntimeError("Expected machine checkout challenge")
        checkout_data = checkout.json()
        # session_token is returned once. Persist the complete recovery envelope
        # before parsing or validating any optional/derived challenge field.
        persist_recovery_envelope(
            checkout_data, request_body, quote_data,
            client_reference=client_reference,
        )
        recovery_envelope = load_recovery_envelope(request_body)
    require_https(checkout_data["settle_url"], "settle_url")
    require_https(checkout_data["receipt_url"], "receipt_url")
    challenge = checkout_data["payment_challenges"]
    challenge_amount = str(challenge.get("amount") or "")
    if not re.fullmatch(r"[1-9][0-9]*", challenge_amount):
        raise RuntimeError("Challenge amount is not canonical native-USDC minor units")
    amount_minor = int(challenge_amount)
    if amount_minor != quote_amount_minor(quote_data):
        raise RuntimeError("Challenge amount does not exactly match the authenticated quote")
    if challenge["network"] != "eip155:8453" or challenge["asset"].lower() != BASE_USDC.lower():
        raise RuntimeError("Refusing wrong chain or token")
    if challenge["payTo"].lower() != expected_receiver.lower():
        raise RuntimeError("Refusing unexpected receiver")
    if Decimal(amount_minor) / Decimal(1_000_000) > max_usdc:
        raise RuntimeError("Challenge exceeds MAX_USDC")

    settlement_payload = recovery_envelope.get("settlement_request")
    settlement_idempotency = recovery_envelope.get("settlement_idempotency_key")
    if settlement_payload and settlement_idempotency:
        authorization = settlement_payload["payload"]["authorization"]
    else:
        now = int(time.time())
        backdate_seconds = 5
        max_span_seconds = int(challenge["maxTimeoutSeconds"])
        nonce = "0x" + secrets.token_bytes(32).hex()
        authorization = {
            "from": account.address, "to": expected_receiver, "value": str(amount_minor),
            "validAfter": now - backdate_seconds,
            # maxTimeoutSeconds constrains validBefore - validAfter, so the
            # backdating allowance is part of the server's maximum span.
            "validBefore": now - backdate_seconds + min(300, max_span_seconds),
            "nonce": nonce,
        }
        signing_name, signing_version = runtime_signing_domain(challenge)
        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"}, {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"},
                ],
                "TransferWithAuthorization": [
                    {"name": "from", "type": "address"}, {"name": "to", "type": "address"},
                    {"name": "value", "type": "uint256"}, {"name": "validAfter", "type": "uint256"},
                    {"name": "validBefore", "type": "uint256"}, {"name": "nonce", "type": "bytes32"},
                ],
            },
            "primaryType": "TransferWithAuthorization",
            "domain": {
                "name": signing_name, "version": signing_version,
                "chainId": BASE_CHAIN_ID, "verifyingContract": BASE_USDC,
            },
            "message": {**authorization, "value": amount_minor},
        }
        signature = account.sign_message(encode_typed_data(full_message=typed_data)).signature.hex()
        settlement_payload = {
            "x402Version": 2, "accepted": challenge,
            "payload": {
                "authorization": authorization,
                "signature": "0x" + signature.removeprefix("0x"),
            },
        }
        settlement_idempotency = hashlib.sha256(
            f"{checkout_data['session_id']}\0{nonce}".encode()
        ).hexdigest()
        # Persist the exact signed request before the first settlement POST.
        # A process restart therefore reuses this authorization byte-for-byte.
        update_recovery_envelope(
            recovery_envelope, "authorization_ready",
            settlement_request=settlement_payload,
            settlement_idempotency_key=settlement_idempotency,
        )
    try:
        settlement = requests.post(
            checkout_data["settle_url"],
            headers={
                "X-Checkout-Session-Token": checkout_data["session_token"],
                "Idempotency-Key": settlement_idempotency,
                "X-Checkout-Client-Reference": client_reference,
            },
            json=settlement_payload,
            timeout=60,
        )
        settlement.raise_for_status()
        settlement_status = settlement.json().get("status")
        if settlement_status not in PENDING_SETTLEMENT_STATES | {"confirmed"}:
            raise RuntimeError(f"Settlement entered terminal state: {settlement_status}")
        update_recovery_envelope(recovery_envelope, settlement_status)
    except requests.Timeout:
        # A lost response is unknown state. Poll the same receipt; do not sign or
        # POST a second authorization.
        settlement_status = "pending"
        update_recovery_envelope(recovery_envelope, "settlement_unknown")

    if settlement_status == "confirmed":
        receipt = wait_for_confirmation(
            checkout_data["receipt_url"], checkout_data["session_token"],
            timeout_seconds=30,
        )
    else:
        receipt = wait_for_confirmation(
            checkout_data["receipt_url"], checkout_data["session_token"],
        )
    if (
        receipt.get("network") != "eip155:8453"
        or str(receipt.get("asset", "")).lower() != BASE_USDC.lower()
        or str(receipt.get("pay_to", "")).lower() != expected_receiver.lower()
        or str(receipt.get("payer", "")).lower() != account.address.lower()
        or int(receipt.get("amount", -1)) != amount_minor
    ):
        raise RuntimeError("Confirmed receipt does not match the approved payment")
    update_recovery_envelope(recovery_envelope, "confirmed")

    recovery_url = (
        f"{API_BASE}/api/agent/register-status/{checkout_data['session_id']}/reconcile"
    )
    recovery_data = None
    for _ in range(20):
        reconcile_response = requests.post(
            recovery_url, headers={"X-API-Key": api_key}, timeout=30,
        )
        if reconcile_response.status_code == 200:
            recovery_data = reconcile_response.json()
            break
        if reconcile_response.status_code != 202:
            raise RuntimeError(
                f"HeadlessDomains reconciliation failed: {reconcile_response.text}"
            )
        time.sleep(3)
    if recovery_data is None:
        raise RuntimeError("Provisioning remains in progress; preserve session for recovery")
    if recovery_data.get("domain_status") != "active" or not recovery_data.get("domain_id"):
        raise RuntimeError("Payment confirmed but domain is not active; preserve session for recovery")

    status = requests.get(
        f"{API_BASE}/api/agent/register-status/{checkout_data['session_id']}",
        headers={"X-API-Key": api_key}, timeout=30,
    )
    status.raise_for_status()
    status_data = status.json()
    if status_data.get("status") != "paid":
        raise RuntimeError("Registration order is not paid; preserve session for recovery")
    lookup = requests.get(
        f"{API_BASE}/api/v1/lookup/{request_body['domain']}.{request_body['namespace']}",
        timeout=30,
    )
    lookup.raise_for_status()
    complete_recovery_envelope(
        recovery_envelope, receipt, recovery_data, status_data,
    )
    print(recovery_data)


if __name__ == "__main__":
    main()
