/**
 * Vaultfire x402 Payment Client — Coinbase HTTP 402 Payment Protocol
 *
 * Standalone implementation of the x402 open standard for internet-native
 * payments using USDC on Base mainnet (and Avalanche) via EIP-3009
 * (transferWithAuthorization).
 *
 * Protocol flow:
 *   1. Client requests a resource from a server
 *   2. Server responds HTTP 402 + X-PAYMENT-REQUIRED header
 *   3. Client signs a USDC transferWithAuthorization (EIP-712)
 *   4. Client resends request with X-PAYMENT header
 *   5. Facilitator verifies + settles, server returns resource
 *
 * This is a STANDALONE version — it does not import from any Vaultfire-
 * specific wallet or VNS modules. Instead, you inject a WalletProvider and
 * optionally a VNSResolver via configureX402().
 *
 * References:
 *   - https://github.com/coinbase/x402
 *   - https://docs.x402.org
 *   - EIP-3009: https://eips.ethereum.org/EIPS/eip-3009
 *   - EIP-712: https://eips.ethereum.org/EIPS/eip-712
 *
 * @module @vaultfire/x402
 */

// ---------------------------------------------------------------------------
// External provider interfaces (inject via configureX402)
// ---------------------------------------------------------------------------

/**
 * Minimal interface for providing wallet access to the x402 client.
 *
 * Implement this interface to connect any wallet (browser extension, hardware
 * wallet, custodial key, agent key, etc.) without tight coupling.
 *
 * @example
 * ```ts
 * const myWallet: WalletProvider = {
 *   getAddress: () => '0xYourAddress',
 *   getPrivateKey: () => process.env.AGENT_PRIVATE_KEY ?? null,
 *   isUnlocked: () => true,
 * };
 * configureX402(myWallet);
 * ```
 */
export interface WalletProvider {
  /** Returns the current wallet address, or null if not available. */
  getAddress(): string | null;
  /**
   * Returns the private key for EIP-712 signing, or null if unavailable.
   * SECURITY: Never log, store, or transmit the returned value.
   */
  getPrivateKey(): string | null;
  /** Returns true when the wallet is ready for signing. */
  isUnlocked(): boolean;
}

/**
 * Optional VNS (Vaultfire Name Service) resolver interface.
 *
 * Implement and inject via configureX402() to enable human-readable payment
 * addresses like "vaultfire-sentinel.vns" alongside raw Ethereum addresses.
 *
 * @example
 * ```ts
 * const vnsResolver: VNSResolver = {
 *   resolvePaymentAddress: async (name) => { ... },
 *   reverseResolveVNS: async (addr) => { ... },
 * };
 * configureX402(myWallet, vnsResolver);
 * ```
 */
export interface VNSResolver {
  /**
   * Resolve a VNS name or raw address to a payment address.
   * Returns null if the name is not registered.
   */
  resolvePaymentAddress(
    nameOrAddress: string,
  ): Promise<{ address: string; vnsName?: string } | null>;
  /**
   * Reverse resolve an Ethereum address to its VNS name.
   * Returns null if no VNS name is registered for the address.
   */
  reverseResolveVNS(address: string): Promise<string | null>;
}

// ---------------------------------------------------------------------------
// Module-level provider state
// ---------------------------------------------------------------------------

let _walletProvider: WalletProvider | null = null;
let _vnsResolver: VNSResolver | null = null;

/**
 * Configure the x402 client with a wallet provider and optional VNS resolver.
 *
 * Must be called before using any signing functions. Safe to call multiple
 * times — later calls override earlier ones.
 *
 * @param wallet - WalletProvider implementation
 * @param vns    - Optional VNSResolver for human-readable addresses
 */
export function configureX402(wallet: WalletProvider, vns?: VNSResolver): void {
  _walletProvider = wallet;
  if (vns !== undefined) {
    _vnsResolver = vns;
  }
}

/** Get the configured wallet provider (throws if not configured). */
function getWallet(): WalletProvider {
  if (!_walletProvider) {
    throw new Error(
      'x402 wallet not configured. Call configureX402(walletProvider) before using x402 functions.',
    );
  }
  return _walletProvider;
}

/** Get the wallet address (throws if not configured or unavailable). */
function getWalletAddress(): string {
  const addr = getWallet().getAddress();
  if (!addr) {
    throw new Error('No wallet address available. Create or import a wallet first.');
  }
  return addr;
}

/** Get the wallet address without throwing (returns null if unavailable). */
function getWalletAddressSafe(): string | null {
  try {
    return _walletProvider?.getAddress() ?? null;
  } catch {
    return null;
  }
}

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Base mainnet USDC contract (EIP-3009 compatible) */
export const BASE_USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

/** Avalanche mainnet USDC contract */
export const AVALANCHE_USDC_ADDRESS = '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E';

/** Arbitrum mainnet USDC contract (native USDC, EIP-3009 compatible) */
export const ARBITRUM_USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';

/** Polygon mainnet USDC contract (native USDC, EIP-3009 compatible) */
export const POLYGON_USDC_ADDRESS = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';

/** Base mainnet chain ID */
export const BASE_CHAIN_ID = 8453;

/** Avalanche mainnet chain ID */
export const AVALANCHE_CHAIN_ID = 43114;

/** Arbitrum mainnet chain ID */
export const ARBITRUM_CHAIN_ID = 42161;

/** Polygon mainnet chain ID */
export const POLYGON_CHAIN_ID = 137;

/** x402 network identifier for Base mainnet */
export const BASE_NETWORK = 'eip155:8453';

/** x402 network identifier for Avalanche mainnet */
export const AVALANCHE_NETWORK = 'eip155:43114';

/** x402 network identifier for Arbitrum mainnet */
export const ARBITRUM_NETWORK = 'eip155:42161';

/** x402 network identifier for Polygon mainnet */
export const POLYGON_NETWORK = 'eip155:137';

/** Default facilitator URL (Coinbase x402 facilitator) */
export const DEFAULT_FACILITATOR_URL = 'https://x402.org/facilitator';

/** USDC decimals */
export const USDC_DECIMALS = 6;

/** Default payment timeout in seconds */
export const DEFAULT_TIMEOUT_SECONDS = 60;

/** x402 protocol version */
export const X402_VERSION = 2;

/** Vaultfire deployer / relayer address */
export const VAULTFIRE_DEPLOYER = '0xfA15Ee28939B222B0448261A22156070f0A7813C';

/** Vaultfire hub URL */
export const VAULTFIRE_HUB_URL = 'https://theloopbreaker.com';

/** Header names per x402 v2 spec */
export const HEADERS = {
  PAYMENT_REQUIRED: 'X-PAYMENT-REQUIRED',
  PAYMENT_SIGNATURE: 'X-PAYMENT',
  PAYMENT_RESPONSE: 'X-PAYMENT-RESPONSE',
} as const;

/** Well-known USDC contract addresses per chain ID */
export const USDC_ADDRESSES: Record<number, string> = {
  [BASE_CHAIN_ID]: BASE_USDC_ADDRESS,
  [AVALANCHE_CHAIN_ID]: AVALANCHE_USDC_ADDRESS,
  [ARBITRUM_CHAIN_ID]: ARBITRUM_USDC_ADDRESS,
  [POLYGON_CHAIN_ID]: POLYGON_USDC_ADDRESS,
};

/** RPC endpoints per chain ID */
export const RPC_ENDPOINTS: Record<number, string> = {
  [BASE_CHAIN_ID]: 'https://mainnet.base.org',
  [AVALANCHE_CHAIN_ID]: 'https://api.avax.network/ext/bc/C/rpc',
  [ARBITRUM_CHAIN_ID]: 'https://arb1.arbitrum.io/rpc',
  [POLYGON_CHAIN_ID]: 'https://polygon-rpc.com',
};

/** Block explorer URLs per chain ID */
export const EXPLORER_URLS: Record<number, string> = {
  [BASE_CHAIN_ID]: 'https://basescan.org',
  [AVALANCHE_CHAIN_ID]: 'https://snowtrace.io',
  [ARBITRUM_CHAIN_ID]: 'https://arbiscan.io',
  [POLYGON_CHAIN_ID]: 'https://polygonscan.com',
};

/** Network identifiers per chain ID */
export const NETWORK_IDS: Record<number, string> = {
  [BASE_CHAIN_ID]: BASE_NETWORK,
  [AVALANCHE_CHAIN_ID]: AVALANCHE_NETWORK,
  [ARBITRUM_CHAIN_ID]: ARBITRUM_NETWORK,
  [POLYGON_CHAIN_ID]: POLYGON_NETWORK,
};

/** Chain name lookup from chain ID */
export const CHAIN_NAMES: Record<number, string> = {
  [BASE_CHAIN_ID]: 'Base',
  [AVALANCHE_CHAIN_ID]: 'Avalanche',
  [ARBITRUM_CHAIN_ID]: 'Arbitrum',
  [POLYGON_CHAIN_ID]: 'Polygon',
};

/** All supported chain IDs */
export const SUPPORTED_CHAIN_IDS = [BASE_CHAIN_ID, AVALANCHE_CHAIN_ID, ARBITRUM_CHAIN_ID, POLYGON_CHAIN_ID] as const;

// ---------------------------------------------------------------------------
// EIP-712 Domain & Types for USDC transferWithAuthorization (EIP-3009)
// ---------------------------------------------------------------------------

/**
 * EIP-712 domain for Base USDC (default / backwards-compatible).
 * USDC uses name="USD Coin", version="2".
 */
export const USDC_EIP712_DOMAIN = {
  name: 'USD Coin',
  version: '2',
  chainId: BASE_CHAIN_ID,
  verifyingContract: BASE_USDC_ADDRESS,
} as const;

/**
 * Get the EIP-712 domain for USDC on a specific chain.
 * Each chain's USDC contract is a separate verifying contract.
 *
 * @param chainId - The chain ID (defaults to Base)
 * @returns The EIP-712 domain configuration for that chain's USDC
 */
export function getUsdcEip712Domain(chainId: number = BASE_CHAIN_ID): {
  name: string;
  version: string;
  chainId: number;
  verifyingContract: string;
} {
  const usdcAddress = USDC_ADDRESSES[chainId];
  if (!usdcAddress) {
    throw new Error(
      `Unsupported chain ID: ${chainId}. Supported chains: ${SUPPORTED_CHAIN_IDS.join(', ')}`,
    );
  }
  return {
    name: 'USD Coin',
    version: '2',
    chainId,
    verifyingContract: usdcAddress,
  };
}

/**
 * Resolve a chain ID from a chain name string or numeric ID.
 *
 * @param chain - Chain name ('base', 'avalanche', 'arbitrum', 'polygon') or numeric chain ID
 * @returns The numeric chain ID
 */
export function resolveChainId(chain: number | string = BASE_CHAIN_ID): number {
  if (typeof chain === 'number') return chain;
  const map: Record<string, number> = {
    base: BASE_CHAIN_ID,
    avalanche: AVALANCHE_CHAIN_ID,
    arbitrum: ARBITRUM_CHAIN_ID,
    polygon: POLYGON_CHAIN_ID,
  };
  return map[chain.toLowerCase()] ?? BASE_CHAIN_ID;
}

/**
 * EIP-712 type definition for TransferWithAuthorization (EIP-3009).
 * This is the standard type used by USDC for gasless transfers.
 */
export const TRANSFER_WITH_AUTHORIZATION_TYPES: Record<
  string,
  Array<{ name: string; type: string }>
> = {
  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' },
  ],
};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** Resource description in x402 payment requirements */
export interface X402Resource {
  url: string;
  description?: string;
  mimeType?: string;
}

/** Extra fields for EVM exact scheme */
export interface X402ExtraEVM {
  assetTransferMethod: 'eip3009' | 'permit2';
  name: string;
  version: string;
}

/** A single payment requirement from the server */
export interface X402PaymentRequirement {
  scheme: 'exact';
  network: string;
  amount: string;
  asset: string;
  payTo: string;
  maxTimeoutSeconds: number;
  extra?: X402ExtraEVM;
}

/** The full 402 response payload (decoded from X-PAYMENT-REQUIRED header) */
export interface X402PaymentRequired {
  x402Version: number;
  accepts: X402PaymentRequirement[];
  resource?: X402Resource;
  error?: string;
}

/** EIP-3009 authorization parameters */
export interface EIP3009Authorization {
  from: string;
  to: string;
  value: string;
  validAfter: string;
  /** Internal: chain ID for EIP-712 domain selection (not part of the wire format) */
  _chainId?: number;
  validBefore: string;
  nonce: string;
}

/** The payload sent in the X-PAYMENT header */
export interface X402PaymentPayload {
  x402Version: number;
  resource?: X402Resource;
  accepted: X402PaymentRequirement;
  payload: {
    signature: string;
    authorization: EIP3009Authorization;
  };
}

/** Settlement response from the facilitator/server */
export interface X402SettlementResponse {
  success: boolean;
  txHash?: string;
  network?: string;
  error?: string;
}

/** Result of an x402 payment-gated fetch */
export interface X402FetchResult {
  response: Response;
  paid: boolean;
  paymentPayload?: X402PaymentPayload;
  settlement?: X402SettlementResponse;
  error?: string;
}

/** x402 payment history entry (persisted in localStorage) */
export interface X402PaymentRecord {
  id: string;
  timestamp: number;
  url: string;
  amount: string;
  amountFormatted: string;
  asset: string;
  network: string;
  payTo: string;
  from: string;
  txHash?: string;
  status: 'signed' | 'settled' | 'failed';
  description?: string;
  /** VNS name of the recipient (if resolved) */
  recipientVNS?: string;
  /** VNS name of the sender (if resolved) */
  senderVNS?: string;
}

/** Configuration for the x402 client */
export interface X402ClientConfig {
  /** Facilitator URL (default: Coinbase facilitator) */
  facilitatorUrl?: string;
  /** Auto-pay on 402 without prompting (default: false) */
  autoPay?: boolean;
  /** Maximum amount in USDC (human-readable) to auto-pay (default: 1.00) */
  maxAutoPayAmount?: string;
  /** Custom signer function (overrides wallet provider) */
  customSigner?: (domain: object, types: object, value: object) => Promise<string>;
}

// ---------------------------------------------------------------------------
// Payment History (localStorage persistence)
// ---------------------------------------------------------------------------

const PAYMENT_HISTORY_KEY = 'vaultfire_x402_payments';
const MAX_HISTORY_ENTRIES = 200;

/** Get all x402 payment records from local storage */
export function getPaymentHistory(): X402PaymentRecord[] {
  if (typeof window === 'undefined') return [];
  try {
    const raw = localStorage.getItem(PAYMENT_HISTORY_KEY);
    if (!raw) return [];
    return JSON.parse(raw) as X402PaymentRecord[];
  } catch {
    return [];
  }
}

/** Save a payment record to local storage */
export function savePaymentRecord(record: X402PaymentRecord): void {
  if (typeof window === 'undefined') return;
  try {
    const history = getPaymentHistory();
    history.unshift(record);
    // Keep only the most recent entries
    if (history.length > MAX_HISTORY_ENTRIES) {
      history.length = MAX_HISTORY_ENTRIES;
    }
    localStorage.setItem(PAYMENT_HISTORY_KEY, JSON.stringify(history));
  } catch {
    // Storage full — silently fail
  }
}

/** Update the status/txHash of an existing payment record */
export function updatePaymentRecord(
  id: string,
  updates: Partial<Pick<X402PaymentRecord, 'status' | 'txHash'>>,
): void {
  if (typeof window === 'undefined') return;
  try {
    const history = getPaymentHistory();
    const idx = history.findIndex((r) => r.id === id);
    if (idx >= 0) {
      history[idx] = { ...history[idx], ...updates } as X402PaymentRecord;
      localStorage.setItem(PAYMENT_HISTORY_KEY, JSON.stringify(history));
    }
  } catch {
    // ignore
  }
}

/** Clear all payment history */
export function clearPaymentHistory(): void {
  if (typeof window === 'undefined') return;
  try {
    localStorage.removeItem(PAYMENT_HISTORY_KEY);
  } catch {
    // ignore
  }
}

/** Get payment history summary stats */
export function getPaymentStats(): {
  totalPayments: number;
  totalAmountUsdc: string;
  settledCount: number;
  failedCount: number;
} {
  const history = getPaymentHistory();
  let totalMicro = 0n;
  let settled = 0;
  let failed = 0;

  for (const record of history) {
    try {
      totalMicro += BigInt(record.amount);
    } catch {
      // skip invalid amounts
    }
    if (record.status === 'settled') settled++;
    if (record.status === 'failed') failed++;
  }

  return {
    totalPayments: history.length,
    totalAmountUsdc: formatUsdc(totalMicro.toString()),
    settledCount: settled,
    failedCount: failed,
  };
}

// ---------------------------------------------------------------------------
// Utility functions
// ---------------------------------------------------------------------------

/** Format USDC micro-units (6 decimals) to human-readable string */
export function formatUsdc(microAmount: string): string {
  try {
    const n = BigInt(microAmount);
    const whole = n / 1_000_000n;
    const frac = n % 1_000_000n;
    const fracStr = frac.toString().padStart(6, '0').replace(/0+$/, '');
    if (!fracStr) return `${whole}.00`;
    return `${whole}.${fracStr}`;
  } catch {
    return '0.00';
  }
}

/** Parse human-readable USDC amount to micro-units string */
export function parseUsdc(humanAmount: string): string {
  try {
    const [whole = '0', frac = ''] = humanAmount.split('.');
    const fracPadded = frac.slice(0, 6).padEnd(6, '0');
    const micro = BigInt(whole) * 1_000_000n + BigInt(fracPadded);
    return micro.toString();
  } catch {
    return '0';
  }
}

/** Generate a random bytes32 nonce for EIP-3009 */
export function generateNonce(): string {
  const bytes = new Uint8Array(32);
  if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
    crypto.getRandomValues(bytes);
  } else {
    // Node.js fallback
    const nodeCrypto = require('crypto') as typeof import('crypto');
    const buf = nodeCrypto.randomBytes(32);
    bytes.set(buf);
  }
  return '0x' + Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
}

/** Generate a unique payment ID */
export function generatePaymentId(): string {
  return `x402_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}

/** Base64 encode a JSON object (for x402 headers) */
export function encodeHeaderPayload(obj: object): string {
  const json = JSON.stringify(obj);
  if (typeof btoa === 'function') {
    return btoa(json);
  }
  return Buffer.from(json).toString('base64');
}

/** Base64 decode a header string to JSON */
export function decodeHeaderPayload<T>(b64: string): T {
  try {
    if (typeof atob === 'function') {
      return JSON.parse(atob(b64)) as T;
    }
    return JSON.parse(Buffer.from(b64, 'base64').toString('utf-8')) as T;
  } catch {
    throw new Error('Failed to decode x402 header payload');
  }
}

// ---------------------------------------------------------------------------
// EIP-712 Signing (uses ethers.js via configured wallet private key)
// ---------------------------------------------------------------------------

/**
 * Sign an EIP-3009 TransferWithAuthorization using EIP-712 typed data.
 *
 * Uses the WalletProvider's private key (configured via configureX402) to
 * produce a signature that authorizes the facilitator to call
 * transferWithAuthorization on the USDC contract.
 *
 * @param authorization - The transfer parameters to sign
 * @param customSigner  - Optional custom signer function (overrides wallet)
 * @returns The 65-byte hex signature
 */
export async function signTransferAuthorization(
  authorization: EIP3009Authorization,
  customSigner?: X402ClientConfig['customSigner'],
): Promise<string> {
  // Build the EIP-712 domain and value — chain-aware via the requirement's network
  const chainIdFromNetwork = authorization._chainId
    ? authorization._chainId
    : BASE_CHAIN_ID;
  const domain: Record<string, unknown> = { ...getUsdcEip712Domain(chainIdFromNetwork) };
  const types: Record<string, Array<{ name: string; type: string }>> = {
    ...TRANSFER_WITH_AUTHORIZATION_TYPES,
  };
  const value = {
    from: authorization.from,
    to: authorization.to,
    value: authorization.value,
    validAfter: authorization.validAfter,
    validBefore: authorization.validBefore,
    nonce: authorization.nonce,
  };

  // Use custom signer if provided
  if (customSigner) {
    return customSigner(domain, types, value);
  }

  // Use the configured WalletProvider
  const wallet = getWallet();
  if (!wallet.isUnlocked()) {
    throw new Error('Wallet is locked. Unlock your wallet before making x402 payments.');
  }

  const privateKey = wallet.getPrivateKey();
  if (!privateKey) {
    throw new Error('No private key available. Unlock your wallet first.');
  }

  // Dynamic import ethers to keep the module tree-shakeable
  const { ethers } = await import('ethers');
  const signingWallet = new ethers.Wallet(privateKey);

  // Sign the EIP-712 typed data
  const signature = await signingWallet.signTypedData(domain, types, value);
  return signature;
}

// ---------------------------------------------------------------------------
// x402 Protocol Operations
// ---------------------------------------------------------------------------

/**
 * Check if a Response is an x402 Payment Required response.
 */
export function isPaymentRequired(response: Response): boolean {
  return response.status === 402;
}

/**
 * Parse the payment requirements from a 402 response.
 *
 * Checks both the X-PAYMENT-REQUIRED header and the response body.
 */
export async function parsePaymentRequired(
  response: Response,
): Promise<X402PaymentRequired | null> {
  // Try the header first (x402 v2 standard)
  const headerValue =
    response.headers.get(HEADERS.PAYMENT_REQUIRED) ||
    response.headers.get('payment-required');

  if (headerValue) {
    try {
      return decodeHeaderPayload<X402PaymentRequired>(headerValue);
    } catch {
      // Fall through to body parsing
    }
  }

  // Try parsing the response body as JSON
  try {
    const cloned = response.clone();
    const body = await cloned.json();
    if (body && (body.accepts || body.x402Version)) {
      return body as X402PaymentRequired;
    }
  } catch {
    // Not JSON or no payment info in body
  }

  return null;
}

/**
 * Select the best payment requirement from the server's accepted options.
 *
 * Prioritizes:
 *   1. Preferred chain USDC with EIP-3009 (if preferredChainId provided)
 *   2. Any supported chain USDC with EIP-3009 (Base > Avalanche > Arbitrum > Polygon)
 *   3. Any supported chain USDC (any method)
 *   4. Any EVM option with EIP-3009
 *   5. First option (fallback)
 *
 * @param requirements     - The 402 response with accepted payment options
 * @param preferredChainId - Optional preferred chain ID to prioritize
 */
export function selectPaymentRequirement(
  requirements: X402PaymentRequired,
  preferredChainId?: number,
): X402PaymentRequirement | null {
  if (!requirements.accepts || requirements.accepts.length === 0) return null;

  // Helper: check if a requirement matches a given chain's USDC with EIP-3009
  const matchesChainUsdc3009 = (r: X402PaymentRequirement, chainId: number) => {
    const networkId = NETWORK_IDS[chainId];
    const usdcAddr = USDC_ADDRESSES[chainId];
    return (
      networkId &&
      usdcAddr &&
      r.network === networkId &&
      r.asset.toLowerCase() === usdcAddr.toLowerCase() &&
      r.extra?.assetTransferMethod === 'eip3009'
    );
  };

  // Helper: check if a requirement matches a given chain's USDC (any method)
  const matchesChainUsdc = (r: X402PaymentRequirement, chainId: number) => {
    const networkId = NETWORK_IDS[chainId];
    const usdcAddr = USDC_ADDRESSES[chainId];
    return (
      networkId &&
      usdcAddr &&
      r.network === networkId &&
      r.asset.toLowerCase() === usdcAddr.toLowerCase()
    );
  };

  // Priority 1: Preferred chain USDC with EIP-3009
  if (preferredChainId) {
    const preferred3009 = requirements.accepts.find((r) =>
      matchesChainUsdc3009(r, preferredChainId),
    );
    if (preferred3009) return preferred3009;

    // Preferred chain USDC (any method)
    const preferredUsdc = requirements.accepts.find((r) =>
      matchesChainUsdc(r, preferredChainId),
    );
    if (preferredUsdc) return preferredUsdc;
  }

  // Priority 2: Any supported chain USDC with EIP-3009 (ordered by preference)
  for (const chainId of SUPPORTED_CHAIN_IDS) {
    const match = requirements.accepts.find((r) => matchesChainUsdc3009(r, chainId));
    if (match) return match;
  }

  // Priority 3: Any supported chain USDC (any method)
  for (const chainId of SUPPORTED_CHAIN_IDS) {
    const match = requirements.accepts.find((r) => matchesChainUsdc(r, chainId));
    if (match) return match;
  }

  // Priority 4: Any EVM option with EIP-3009
  const evm3009 = requirements.accepts.find(
    (r) => r.network.startsWith('eip155:') && r.extra?.assetTransferMethod === 'eip3009',
  );
  if (evm3009) return evm3009;

  // Fallback: first option
  return requirements.accepts[0] ?? null;
}

/**
 * Create and sign an x402 payment payload for a given requirement.
 *
 * This builds the EIP-3009 authorization, signs it with EIP-712,
 * and returns the complete PaymentPayload ready for the header.
 *
 * @param requirement - The payment requirement to fulfill
 * @param resource    - Optional resource description
 * @param config      - Client configuration
 * @returns The signed payment payload
 */
export async function createPaymentPayload(
  requirement: X402PaymentRequirement,
  resource?: X402Resource,
  config: X402ClientConfig = {},
): Promise<X402PaymentPayload> {
  const walletAddress = getWalletAddress();

  const now = Math.floor(Date.now() / 1000);
  const timeout = requirement.maxTimeoutSeconds || DEFAULT_TIMEOUT_SECONDS;

  // Extract chain ID from the requirement's network identifier (e.g., 'eip155:8453' -> 8453)
  const requirementChainId = requirement.network.startsWith('eip155:')
    ? parseInt(requirement.network.split(':')[1], 10)
    : BASE_CHAIN_ID;

  // Build the EIP-3009 authorization
  const authorization: EIP3009Authorization = {
    from: walletAddress,
    to: requirement.payTo,
    value: requirement.amount,
    validAfter: (now - 5).toString(), // 5 seconds grace period
    validBefore: (now + timeout).toString(),
    nonce: generateNonce(),
    _chainId: requirementChainId,
  };

  // Sign with EIP-712 (chain-aware domain selection via _chainId)
  const signature = await signTransferAuthorization(authorization, config.customSigner);

  const payloadObj: X402PaymentPayload = {
    x402Version: X402_VERSION,
    resource,
    accepted: requirement,
    payload: {
      signature,
      authorization,
    },
  };

  return payloadObj;
}

/**
 * Execute an x402 payment-gated fetch.
 *
 * This is the main entry point for making x402-aware HTTP requests.
 * It handles the full protocol flow:
 *   1. Makes the initial request
 *   2. If 402, parses requirements and signs a payment
 *   3. Resends with the payment header
 *   4. Returns the final response
 *
 * @param url    - The URL to fetch
 * @param init   - Standard fetch init options
 * @param config - x402 client configuration
 * @returns The fetch result with payment metadata
 */
export async function x402Fetch(
  url: string,
  init: RequestInit = {},
  config: X402ClientConfig = {},
): Promise<X402FetchResult> {
  // Step 1: Make the initial request
  const initialResponse = await fetch(url, init);

  // If not 402, return as-is
  if (!isPaymentRequired(initialResponse)) {
    return { response: initialResponse, paid: false };
  }

  // Step 2: Parse payment requirements
  const requirements = await parsePaymentRequired(initialResponse);
  if (!requirements) {
    return {
      response: initialResponse,
      paid: false,
      error: 'Server returned 402 but no valid payment requirements found',
    };
  }

  // Step 3: Select the best payment option
  const selected = selectPaymentRequirement(requirements);
  if (!selected) {
    return {
      response: initialResponse,
      paid: false,
      error: 'No compatible payment option found in server requirements',
    };
  }

  // Step 4: Check auto-pay limits
  if (!config.autoPay) {
    return {
      response: initialResponse,
      paid: false,
      error: 'Payment required but auto-pay is disabled. Call payAndRetry() to proceed.',
    };
  }

  const maxAutoPayMicro = parseUsdc(config.maxAutoPayAmount || '1.00');
  if (BigInt(selected.amount) > BigInt(maxAutoPayMicro)) {
    return {
      response: initialResponse,
      paid: false,
      error: `Payment amount ${formatUsdc(selected.amount)} USDC exceeds auto-pay limit of ${config.maxAutoPayAmount || '1.00'} USDC`,
    };
  }

  // Step 5: Create and sign the payment
  return payAndRetry(url, init, selected, requirements.resource, config);
}

/**
 * Sign a payment and retry the request with the payment header.
 *
 * Use this when you want manual control over the payment flow
 * (e.g., after prompting the user for confirmation).
 *
 * @param url         - The URL to fetch
 * @param init        - Standard fetch init options
 * @param requirement - The selected payment requirement
 * @param resource    - Optional resource description
 * @param config      - Client configuration
 * @returns The fetch result with payment metadata
 */
export async function payAndRetry(
  url: string,
  init: RequestInit = {},
  requirement: X402PaymentRequirement,
  resource?: X402Resource,
  config: X402ClientConfig = {},
): Promise<X402FetchResult> {
  const paymentId = generatePaymentId();
  const walletAddress = getWalletAddressSafe() || 'unknown';

  try {
    // Create the signed payment payload
    const paymentPayload = await createPaymentPayload(requirement, resource, config);

    // Record the payment attempt
    const record: X402PaymentRecord = {
      id: paymentId,
      timestamp: Date.now(),
      url,
      amount: requirement.amount,
      amountFormatted: formatUsdc(requirement.amount),
      asset: requirement.asset,
      network: requirement.network,
      payTo: requirement.payTo,
      from: walletAddress,
      status: 'signed',
      description: resource?.description,
    };
    savePaymentRecord(record);

    // Encode the payload for the header
    const encodedPayload = encodeHeaderPayload(paymentPayload);

    // Build the new request with the payment header
    const headers = new Headers(init.headers);
    headers.set(HEADERS.PAYMENT_SIGNATURE, encodedPayload);

    const paidResponse = await fetch(url, {
      ...init,
      headers,
    });

    // Check for settlement response in headers
    let settlement: X402SettlementResponse | undefined;
    const settlementHeader =
      paidResponse.headers.get(HEADERS.PAYMENT_RESPONSE) ||
      paidResponse.headers.get('payment-response');

    if (settlementHeader) {
      try {
        settlement = decodeHeaderPayload<X402SettlementResponse>(settlementHeader);
        // Update the record with settlement info
        updatePaymentRecord(paymentId, {
          status: settlement.success ? 'settled' : 'failed',
          txHash: settlement.txHash,
        });
      } catch {
        // Settlement header parse failed
      }
    }

    // If the response is successful, mark as settled
    if (paidResponse.ok && !settlement) {
      updatePaymentRecord(paymentId, { status: 'settled' });
    }

    return {
      response: paidResponse,
      paid: true,
      paymentPayload,
      settlement,
    };
  } catch (err) {
    // Record the failure
    updatePaymentRecord(paymentId, { status: 'failed' });

    const errorMessage = err instanceof Error ? err.message : 'Unknown payment error';
    return {
      response: new Response(JSON.stringify({ error: errorMessage }), {
        status: 402,
        headers: { 'Content-Type': 'application/json' },
      }),
      paid: false,
      error: errorMessage,
    };
  }
}

// ---------------------------------------------------------------------------
// Convenience: x402-aware wrapper for common HTTP methods
// ---------------------------------------------------------------------------

/**
 * x402-aware GET request.
 */
export async function x402Get(
  url: string,
  config: X402ClientConfig = {},
): Promise<X402FetchResult> {
  return x402Fetch(url, { method: 'GET' }, config);
}

/**
 * x402-aware POST request.
 */
export async function x402Post(
  url: string,
  body: object,
  config: X402ClientConfig = {},
): Promise<X402FetchResult> {
  return x402Fetch(
    url,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    },
    config,
  );
}

// ---------------------------------------------------------------------------
// Verification helpers (for server-side / agent use)
// ---------------------------------------------------------------------------

/**
 * Verify an x402 payment signature locally.
 *
 * Recovers the signer address from the EIP-712 signature and checks
 * that it matches the authorization.from field.
 *
 * @param payload - The payment payload to verify
 * @returns Whether the signature is valid and the recovered address
 */
export async function verifyPaymentSignature(
  payload: X402PaymentPayload,
): Promise<{ valid: boolean; recoveredAddress: string; error?: string }> {
  try {
    const { ethers } = await import('ethers');

    const domain: Record<string, unknown> = { ...USDC_EIP712_DOMAIN };

    // Adjust domain for the payload's network if different from Base
    if (payload.accepted.network !== BASE_NETWORK) {
      const chainIdMatch = payload.accepted.network.match(/eip155:(\d+)/);
      if (chainIdMatch && chainIdMatch[1]) {
        domain.chainId = parseInt(chainIdMatch[1], 10);
      }
      if (payload.accepted.asset) {
        domain.verifyingContract = payload.accepted.asset;
      }
    }

    const types: Record<string, Array<{ name: string; type: string }>> = {
      ...TRANSFER_WITH_AUTHORIZATION_TYPES,
    };
    const value = {
      from: payload.payload.authorization.from,
      to: payload.payload.authorization.to,
      value: payload.payload.authorization.value,
      validAfter: payload.payload.authorization.validAfter,
      validBefore: payload.payload.authorization.validBefore,
      nonce: payload.payload.authorization.nonce,
    };

    const recoveredAddress = ethers.verifyTypedData(domain, types, value, payload.payload.signature);

    const valid =
      recoveredAddress.toLowerCase() === payload.payload.authorization.from.toLowerCase();

    return { valid, recoveredAddress };
  } catch (err) {
    return {
      valid: false,
      recoveredAddress: '',
      error: err instanceof Error ? err.message : 'Verification failed',
    };
  }
}

/**
 * Verify an x402 payment via a facilitator's /verify endpoint.
 *
 * @param payload        - The payment payload to verify
 * @param requirement    - The original payment requirement
 * @param facilitatorUrl - Facilitator URL (default: Coinbase)
 */
export async function verifyPaymentViaFacilitator(
  payload: X402PaymentPayload,
  requirement: X402PaymentRequirement,
  facilitatorUrl: string = DEFAULT_FACILITATOR_URL,
): Promise<{ valid: boolean; error?: string }> {
  try {
    const res = await fetch(`${facilitatorUrl}/verify`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        payload,
        requirement,
      }),
      signal: AbortSignal.timeout(15_000),
    });

    const result = await res.json();
    return {
      valid: result.valid === true,
      error: result.error,
    };
  } catch (err) {
    return {
      valid: false,
      error: err instanceof Error ? err.message : 'Facilitator verification failed',
    };
  }
}

// ---------------------------------------------------------------------------
// USDC Balance Check (for pre-flight validation)
// ---------------------------------------------------------------------------

/** balanceOf(address) function selector */
const BALANCE_OF_SELECTOR = '0x70a08231';

/**
 * Check a wallet's USDC balance on Base mainnet.
 *
 * Returns the balance in micro-units (6 decimals). Uses a direct JSON-RPC
 * eth_call — no wallet or signer required.
 *
 * @param address - Address to check (defaults to configured wallet address)
 */
export async function getUsdcBalance(
  address?: string,
  chainId: number | string = BASE_CHAIN_ID,
): Promise<string> {
  const addr = address || getWalletAddressSafe();
  if (!addr) return '0';

  const resolvedChainId = resolveChainId(chainId);
  const rpcUrl = RPC_ENDPOINTS[resolvedChainId] || RPC_ENDPOINTS[BASE_CHAIN_ID];
  const usdcAddress = USDC_ADDRESSES[resolvedChainId] || BASE_USDC_ADDRESS;

  const paddedAddress = addr.replace(/^0x/, '').toLowerCase().padStart(64, '0');
  const calldata = BALANCE_OF_SELECTOR + paddedAddress;

  try {
    const res = await fetch(rpcUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'eth_call',
        params: [{ to: usdcAddress, data: calldata }, 'latest'],
      }),
      signal: AbortSignal.timeout(10_000),
    });

    const json = await res.json();
    if (json.error) return '0';
    if (!json.result || json.result === '0x') return '0';
    return BigInt(json.result).toString();
  } catch {
    return '0';
  }
}

/**
 * Get USDC balances across all supported chains.
 *
 * @param address - Address to check (defaults to configured wallet address)
 * @returns Object mapping chain names to balance strings
 */
export async function getUsdcBalanceAllChains(
  address?: string,
): Promise<Record<string, string>> {
  const results = await Promise.allSettled(
    SUPPORTED_CHAIN_IDS.map(async (chainId) => {
      const balance = await getUsdcBalance(address, chainId);
      return { chain: CHAIN_NAMES[chainId], balance };
    }),
  );

  const balances: Record<string, string> = {};
  for (const result of results) {
    if (result.status === 'fulfilled') {
      balances[result.value.chain] = result.value.balance;
    }
  }
  return balances;
}

/**
 * Check if a wallet has enough USDC to cover a payment.
 *
 * @param amountMicro - Amount in USDC micro-units
 * @param address     - Wallet address (defaults to configured wallet)
 */
export async function hasEnoughUsdc(
  amountMicro: string,
  address?: string,
  chainId: number | string = BASE_CHAIN_ID,
): Promise<boolean> {
  const balance = await getUsdcBalance(address, chainId);
  return BigInt(balance) >= BigInt(amountMicro);
}

// ---------------------------------------------------------------------------
// High-level payment initiation (for XMTP / agent use)
// ---------------------------------------------------------------------------

/**
 * Initiate an x402 payment to a specific address OR VNS name.
 *
 * This creates a signed authorization that can be submitted to a facilitator
 * or sent as an XMTP transaction reference.
 *
 * Accepts:
 *   - Raw Ethereum address: "0x1234...abcd"
 *   - VNS name: "vaultfire-sentinel" or "vaultfire-sentinel.vns"
 *     (requires VNS resolver to be configured via configureX402)
 *
 * @param recipientAddressOrVNS - The address or .vns name to pay
 * @param amountUsdc            - Amount in human-readable USDC (e.g., "1.50")
 * @param description           - Optional payment description
 * @param config                - Client configuration
 * @param chain                 - Chain to pay on: 'base' | 'avalanche' | 'arbitrum' | 'polygon' | chain ID (default: Base)
 * @returns The signed payload, record, and resolved VNS info
 */
export async function initiatePayment(
  recipientAddressOrVNS: string,
  amountUsdc: string,
  description?: string,
  config: X402ClientConfig = {},
  chain: number | string = BASE_CHAIN_ID,
): Promise<{ payload: X402PaymentPayload; record: X402PaymentRecord; resolvedVNS?: string }> {
  const chainId = resolveChainId(chain);
  const networkId = NETWORK_IDS[chainId] || BASE_NETWORK;
  const usdcAddress = USDC_ADDRESSES[chainId] || BASE_USDC_ADDRESS;
  const chainName = CHAIN_NAMES[chainId] || 'Base';
  const amountMicro = parseUsdc(amountUsdc);
  const walletAddress = getWalletAddress();

  // Resolve recipient: could be a .vns name or raw address
  let recipientAddress: string;
  let recipientVNS: string | undefined;

  if (_vnsResolver && !recipientAddressOrVNS.startsWith('0x')) {
    // Looks like a VNS name — resolve it
    const resolved = await _vnsResolver.resolvePaymentAddress(recipientAddressOrVNS);
    if (!resolved) {
      throw new Error(
        `Could not resolve recipient: ${recipientAddressOrVNS}. Ensure the VNS name is registered.`,
      );
    }
    recipientAddress = resolved.address;
    recipientVNS = resolved.vnsName;
  } else if (_vnsResolver && recipientAddressOrVNS.startsWith('0x')) {
    // Raw address — try VNS resolution anyway for display purposes
    const resolved = await _vnsResolver.resolvePaymentAddress(recipientAddressOrVNS);
    recipientAddress = resolved?.address || recipientAddressOrVNS;
    recipientVNS = resolved?.vnsName;
  } else {
    // No VNS resolver — accept address as-is
    if (!recipientAddressOrVNS.startsWith('0x')) {
      throw new Error(
        `Cannot resolve "${recipientAddressOrVNS}": no VNS resolver configured. ` +
          'Pass a VNSResolver via configureX402() or use a raw Ethereum address.',
      );
    }
    recipientAddress = recipientAddressOrVNS;
  }

  // Try to resolve sender VNS name for the record
  let senderVNS: string | undefined;
  if (_vnsResolver) {
    try {
      senderVNS = (await _vnsResolver.reverseResolveVNS(walletAddress)) || undefined;
    } catch {
      // Non-critical — skip
    }
  }

  const displayRecipient = recipientVNS || `${recipientAddress.slice(0, 10)}...`;

  // Build a synthetic requirement for the target chain
  const requirement: X402PaymentRequirement = {
    scheme: 'exact',
    network: networkId,
    amount: amountMicro,
    asset: usdcAddress,
    payTo: recipientAddress,
    maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
    extra: {
      assetTransferMethod: 'eip3009',
      name: 'USDC',
      version: '2',
    },
  };

  const resource: X402Resource = {
    url: `x402://payment/${recipientAddress}`,
    description: description || `Payment of ${amountUsdc} USDC to ${displayRecipient} on ${chainName}`,
  };

  const payload = await createPaymentPayload(requirement, resource, config);

  const record: X402PaymentRecord = {
    id: generatePaymentId(),
    timestamp: Date.now(),
    url: resource.url,
    amount: amountMicro,
    amountFormatted: amountUsdc,
    asset: usdcAddress,
    network: networkId,
    payTo: recipientAddress,
    from: walletAddress,
    status: 'signed',
    description: resource.description,
    recipientVNS,
    senderVNS,
  };

  savePaymentRecord(record);

  return { payload, record, resolvedVNS: recipientVNS };
}

/**
 * Enrich payment history with VNS names by reverse-resolving addresses.
 *
 * Returns a copy of the payment history with recipientVNS and senderVNS
 * fields populated. Requires VNS resolver to be configured.
 *
 * @returns Payment history with VNS names populated (best-effort)
 */
export async function getEnrichedPaymentHistory(): Promise<X402PaymentRecord[]> {
  const history = getPaymentHistory();
  if (history.length === 0) return history;

  // If no VNS resolver is configured, return history as-is
  if (!_vnsResolver) {
    return history;
  }

  // Collect unique addresses to resolve
  const addressSet = new Set<string>();
  for (const record of history) {
    if (record.payTo && !record.recipientVNS) addressSet.add(record.payTo.toLowerCase());
    if (record.from && !record.senderVNS) addressSet.add(record.from.toLowerCase());
  }

  // Resolve all addresses in parallel
  const addressList = Array.from(addressSet);
  const resolvedMap = new Map<string, string>();
  const resolver = _vnsResolver;

  await Promise.allSettled(
    addressList.map(async (addr) => {
      const vns = await resolver.reverseResolveVNS(addr);
      if (vns) resolvedMap.set(addr, vns);
    }),
  );

  // Enrich records
  return history.map((record) => ({
    ...record,
    recipientVNS: record.recipientVNS || resolvedMap.get(record.payTo.toLowerCase()),
    senderVNS: record.senderVNS || resolvedMap.get(record.from.toLowerCase()),
  }));
}

// ---------------------------------------------------------------------------
// Agent-mode client (environment key based, for server-side / XMTP agents)
// ---------------------------------------------------------------------------

/**
 * EIP-712 payload types for the agent-mode x402Payment signing scheme.
 * Compatible with the Claude Code / agent-side signing flow.
 */
export const AGENT_X402_TYPES = {
  x402Payment: [
    { name: 'to', type: 'address' },
    { name: 'amount', type: 'uint256' },
    { name: 'currency', type: 'string' },
    { name: 'nonce', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
};

/** Structured payload that accompanies an agent x402 signature */
export interface X402AgentPayload {
  to: string;
  amount: string;
  currency: string;
  nonce: string;
  deadline: string;
  chainId: number;
  verifyingContract: string;
}

/** Complete result of an agent x402 payment signing operation */
export interface X402AgentSignResult {
  signature: string;
  payload: X402AgentPayload;
  signingAddress: string;
}

/**
 * Agent-mode x402 client — signs payments using an environment variable key.
 *
 * This class is designed for server-side agents, AI bots, and automated
 * workflows. The private key is read once from the environment and is never
 * exposed, logged, or stored in any accessible property.
 *
 * @example
 * ```ts
 * const client = new X402AgentClient(); // reads VAULTFIRE_AGENT_KEY
 * const result = await client.signPayment('0xRecipient', '1000000'); // 1 USDC
 * ```
 */
export class X402AgentClient {
  /** The derived EVM address (public — safe to display). */
  public readonly address: string;

  /** Whether the client has a valid signing key. */
  public readonly enabled: boolean;

  /** Internal wallet instance — never exposed. */
  readonly #walletPromise: Promise<import('ethers').Wallet> | null;

  constructor(privateKeyEnvVar = 'VAULTFIRE_AGENT_KEY') {
    const raw = process.env[privateKeyEnvVar];

    if (!raw) {
      this.address = '';
      this.enabled = false;
      this.#walletPromise = null;
      return;
    }

    try {
      const key = raw.startsWith('0x') ? raw : `0x${raw}`;
      // Lazily resolve ethers
      this.#walletPromise = import('ethers').then(({ ethers }) => new ethers.Wallet(key));
      // Synchronously derive address using ethers utility
      const { ethers } = require('ethers') as typeof import('ethers');
      const w = new ethers.Wallet(key);
      this.address = w.address;
      this.enabled = true;
    } catch {
      console.error(
        `[vaultfire/x402] ${privateKeyEnvVar} is set but is not a valid private key. ` +
          'x402 signing is disabled.',
      );
      this.address = '';
      this.enabled = false;
      this.#walletPromise = null;
    }
  }

  /** Returns the agent's signing address. */
  getAddress(): string {
    return this.address;
  }

  /**
   * Sign an x402 payment authorization using EIP-712.
   *
   * Does NOT submit a transaction — produces only the signed payload that a
   * relayer or counterparty can verify and execute.
   *
   * @param to       - Recipient EVM address
   * @param amount   - Amount in USDC micro-units (6 decimals)
   * @param currency - Currency (default: 'USDC')
   * @param chainId  - Chain ID or name (default: 8453 = Base)
   */
  async signPayment(
    to: string,
    amount: string,
    currency: 'USDC' = 'USDC',
    chainId: number | string = BASE_CHAIN_ID,
  ): Promise<X402AgentSignResult> {
    if (!this.#walletPromise || !this.enabled) {
      throw new Error(
        '[vaultfire/x402] Cannot sign: VAULTFIRE_AGENT_KEY is not configured.',
      );
    }

    const chainIdNum =
      typeof chainId === 'string'
        ? ({ base: 8453, avalanche: 43114, arbitrum: 42161, polygon: 137 }[chainId] ?? BASE_CHAIN_ID)
        : chainId;

    const usdcAddress = USDC_ADDRESSES[chainIdNum] ?? BASE_USDC_ADDRESS;

    const { ethers } = await import('ethers');
    const domain: import('ethers').TypedDataDomain = {
      name: 'x402Payment',
      version: '1',
      chainId: chainIdNum,
      verifyingContract: usdcAddress,
    };

    const nonce = Date.now().toString();
    const deadline = (Math.floor(Date.now() / 1000) + 3600).toString();

    const value = { to, amount, currency, nonce, deadline };

    const signingWallet = await this.#walletPromise;
    const signature = await signingWallet.signTypedData(domain, AGENT_X402_TYPES, value);

    console.log(
      `[vaultfire/x402] Payment signed: ${amount} ${currency} to ${to.slice(0, 10)}… on chain ${chainIdNum}`,
    );

    return {
      signature,
      payload: {
        to,
        amount,
        currency,
        nonce,
        deadline,
        chainId: chainIdNum,
        verifyingContract: usdcAddress,
      },
      signingAddress: this.address,
    };
  }
}

// ---------------------------------------------------------------------------
// Singleton agent client
// ---------------------------------------------------------------------------

let _agentClientInstance: X402AgentClient | null = null;

/**
 * Get or create the singleton X402AgentClient instance.
 *
 * Reads VAULTFIRE_AGENT_KEY from the environment on first access.
 * Subsequent calls return the same instance.
 */
export function getX402AgentClient(): X402AgentClient {
  if (!_agentClientInstance) {
    _agentClientInstance = new X402AgentClient();
  }
  return _agentClientInstance;
}
