/**
 * @vaultfire/x402-guard
 *
 * Pre-payment trust check for x402 wallets.
 *
 * Drop-in middleware that verifies a counterparty's onchain identity, reputation,
 * and accountability bonds on Vaultfire BEFORE your wallet signs a USDC payment.
 *
 * Works with AWAL (Coinbase Agentic Wallet), AgentCash, agentkit, or any custom
 * x402 client. Multi-chain: Base, Avalanche, Arbitrum, Polygon.
 *
 * Usage:
 *
 *   import { VaultfireGuard } from '@vaultfire/x402-guard';
 *
 *   const guard = new VaultfireGuard({ minScore: 40 });
 *
 *   // Before signing an x402 payment to `recipient`:
 *   const decision = await guard.check(recipient);
 *   if (!decision.allow) {
 *     throw new Error(`Vaultfire blocked payment: ${decision.reason}`);
 *   }
 *   // ...proceed to sign + send USDC
 */

import { VaultfireClient } from '@vaultfire/langchain';
import type { VaultfireChain, TrustVerification } from '@vaultfire/langchain';

// ============================================================================
// Types
// ============================================================================

export type GuardDecision = {
  /** True if payment should proceed */
  allow: boolean;
  /** Human-readable reason for the decision */
  reason: string;
  /** The counterparty address that was checked */
  address: string;
  /** Chain on which the check passed (or the primary chain if it failed) */
  chain: VaultfireChain;
  /** Street Cred score (0–95) */
  score: number;
  /** Tier: unranked / bronze / silver / gold / platinum */
  tier: string;
  /** Full trust verification details from Vaultfire */
  verification: TrustVerification;
};

export interface GuardConfig {
  /** Minimum Street Cred score required to allow payment (default: 20). */
  minScore?: number;
  /** Chains to check, in order. First chain to pass is used. Default: all four. */
  chains?: VaultfireChain[];
  /** If true, allow payment when the agent is unregistered on every chain (default: false — block). */
  allowUnregistered?: boolean;
  /** Custom RPC URLs per chain (optional). */
  rpcUrls?: Partial<Record<VaultfireChain, string>>;
  /** Treat the check as a soft warning instead of a hard block (default: false). */
  warnOnly?: boolean;
}

// ============================================================================
// VaultfireGuard
// ============================================================================

const DEFAULT_CHAINS: VaultfireChain[] = ['base', 'arbitrum', 'polygon', 'avalanche'];

/**
 * Default public RPCs picked for being free, no-key, and tolerant of small
 * bursts (the official chain-team RPCs like mainnet.base.org rate-limit too
 * aggressively for back-to-back agent calls). Users can override via the
 * `rpcUrls` config or these env vars:
 *
 *   VAULTFIRE_BASE_RPC, VAULTFIRE_ARBITRUM_RPC,
 *   VAULTFIRE_POLYGON_RPC, VAULTFIRE_AVALANCHE_RPC
 */
const DEFAULT_RPC_URLS: Record<VaultfireChain, string> = {
  base: 'https://base.publicnode.com',
  arbitrum: 'https://arbitrum.publicnode.com',
  polygon: 'https://polygon-bor.publicnode.com',
  avalanche: 'https://avalanche-c-chain.publicnode.com',
};

const ENV_RPC_KEYS: Record<VaultfireChain, string> = {
  base: 'VAULTFIRE_BASE_RPC',
  arbitrum: 'VAULTFIRE_ARBITRUM_RPC',
  polygon: 'VAULTFIRE_POLYGON_RPC',
  avalanche: 'VAULTFIRE_AVALANCHE_RPC',
};

function resolveRpcUrl(
  chain: VaultfireChain,
  override: string | undefined
): string {
  if (override) return override;
  // Browser bundlers may strip `process` — guard the env access.
  if (typeof process !== 'undefined' && process.env) {
    const fromEnv = process.env[ENV_RPC_KEYS[chain]];
    if (fromEnv) return fromEnv;
  }
  return DEFAULT_RPC_URLS[chain];
}

export class VaultfireGuard {
  private readonly minScore: number;
  private readonly chains: VaultfireChain[];
  private readonly allowUnregistered: boolean;
  private readonly rpcUrls: Partial<Record<VaultfireChain, string>>;
  private readonly warnOnly: boolean;

  constructor(config: GuardConfig = {}) {
    this.minScore = config.minScore ?? 20;
    this.chains = config.chains ?? DEFAULT_CHAINS;
    this.allowUnregistered = config.allowUnregistered ?? false;
    this.rpcUrls = config.rpcUrls ?? {};
    this.warnOnly = config.warnOnly ?? false;
  }

  /**
   * Check whether a payment to `address` should be allowed.
   *
   * Verifies the counterparty across all configured chains and uses the BEST
   * result (highest score on any chain). This is correct because Vaultfire
   * agents may register on whichever chain is cheapest for them.
   */
  async check(address: string): Promise<GuardDecision> {
    if (!isValidAddress(address)) {
      return this.deny(address, 'base', null, 0, 'Invalid address format');
    }

    // Check each chain in parallel using the three-state checkChain helper:
    // 'verified' (registered + trust data), 'unregistered' (chain reachable, no
    // identity), or 'rpc-error' (chain unreachable). This avoids conflating
    // "unknown counterparty" with "network down".
    const results = await Promise.all(
      this.chains.map((chain) => this.checkChain(chain, address))
    );

    // Prefer the highest-score verified result.
    let best: { chain: VaultfireChain; verification: TrustVerification } | null = null;
    for (const r of results) {
      if (r.kind !== 'verified') continue;
      if (
        !best ||
        r.verification.streetCred.score > best.verification.streetCred.score
      ) {
        best = { chain: r.chain, verification: r.verification };
      }
    }

    if (best) {
      const { chain, verification } = best;
      const score = verification.streetCred.score;
      const tier = verification.streetCred.tier;
      if (verification.trusted) {
        return this.allow(
          address,
          chain,
          verification,
          score,
          tier,
          `Vaultfire trust check passed: Street Cred ${score}/95 (${tier}) on ${chain}, ${verification.activeBondCount} active bond(s)`
        );
      }
      return this.deny(
        address,
        chain,
        verification,
        score,
        `Street Cred ${score}/95 (${tier}) on ${chain} is below minimum ${this.minScore}`
      );
    }

    // No verified result. If at least one chain confirmed "unregistered",
    // honor allowUnregistered. If every chain errored, fail closed.
    const sawUnregistered = results.some((r) => r.kind === 'unregistered');
    const fallbackChain =
      results.find((r) => r.kind === 'unregistered')?.chain ??
      this.chains[0] ??
      'base';

    if (sawUnregistered) {
      if (this.allowUnregistered) {
        return this.allow(
          address,
          fallbackChain,
          null,
          0,
          'unranked',
          `Counterparty is not registered on Vaultfire, but allowUnregistered=true`
        );
      }
      return this.deny(
        address,
        fallbackChain,
        null,
        0,
        `Counterparty has no Vaultfire identity on any supported chain`
      );
    }

    // Every chain RPC failed — fail closed.
    return this.deny(
      address,
      fallbackChain,
      null,
      0,
      'All chain RPCs unreachable; cannot verify trust'
    );
  }

  /**
   * Wrap an x402 fetch function. Returns a fetch that performs a Vaultfire
   * trust check on the response's `payTo` address before the underlying
   * wallet signs/sends.
   *
   * Example:
   *   const guardedFetch = guard.wrapFetch(originalX402Fetch);
   *   await guardedFetch(url, opts);
   */
  wrapFetch<F extends (...args: any[]) => Promise<any>>(
    x402Fetch: F,
    extractPayTo: (response: Response) => Promise<string | null> = defaultExtractPayTo
  ): F {
    const guard = this;

    const wrapped = async (...args: Parameters<F>) => {
      // The x402 protocol pattern is: client makes request → server returns
      // 402 with payment metadata → client retries with X-PAYMENT header.
      // The first request is *intentionally* rejected at the gateway with no
      // state change, so probing for the 402 challenge here is safe and
      // doesn't cause duplicate side-effects.
      const fetchArgs = args as unknown as Parameters<typeof fetch>;
      const probe = await fetch(...fetchArgs);

      if (probe.status !== 402) {
        // Not an x402 challenge — the server already returned a final response.
        // Forward the probe result so we don't refetch and waste a roundtrip.
        return probe as unknown as Awaited<ReturnType<F>>;
      }

      const payTo = await extractPayTo(probe.clone());
      if (payTo) {
        const decision = await guard.check(payTo);
        if (!decision.allow && !guard.warnOnly) {
          throw new VaultfireGuardError(
            `Vaultfire guard blocked x402 payment to ${payTo}: ${decision.reason}`,
            decision
          );
        }
      }

      // Hand off to the underlying x402 client to do the actual payment +
      // retry. Most x402 clients (x402-fetch, AWAL payAndFetch, AgentCash)
      // expect to issue their own initial request; this is consistent with
      // how those libraries are designed.
      return x402Fetch(...args);
    };

    return wrapped as F;
  }

  // ── Internal helpers ────────────────────────────────────────────────────

  private async checkChain(
    chain: VaultfireChain,
    address: string
  ): Promise<
    | { kind: 'verified'; chain: VaultfireChain; verification: TrustVerification }
    | { kind: 'unregistered'; chain: VaultfireChain }
    | { kind: 'rpc-error'; chain: VaultfireChain; error: string }
  > {
    const client = new VaultfireClient({
      chain,
      rpcUrl: resolveRpcUrl(chain, this.rpcUrls[chain]),
    });

    // First do a cheap registration probe. If the agent isn't active, skip the
    // expensive verifyTrust call (which can revert on contracts that don't
    // gracefully handle unknown addresses).
    //
    // Distinguishing "unregistered" from "network failure" is tricky:
    //   - Some Base RPC providers return a clean `false` for unknown addresses.
    //   - Others return a CALL_EXCEPTION with `data=null` (contract reverted
    //     because the agent struct is unset). That's still a definitive
    //     "unregistered" answer, not an RPC failure.
    //   - True RPC failures surface as TIMEOUT, NETWORK_ERROR, or as JSON-RPC
    //     error envelopes.
    let isActive: boolean;
    try {
      isActive = await retryOnRpcError(() => client.isAgentActive(address));
    } catch (err) {
      if (isContractRevert(err)) {
        return { kind: 'unregistered', chain };
      }
      return {
        kind: 'rpc-error',
        chain,
        error: err instanceof Error ? err.message : String(err),
      };
    }

    if (!isActive) {
      return { kind: 'unregistered', chain };
    }

    try {
      const verification = await retryOnRpcError(() =>
        client.verifyTrust(address, this.minScore)
      );
      return { kind: 'verified', chain, verification };
    } catch (err) {
      // Active but verifyTrust failed (e.g., a sub-call reverted). Treat as
      // RPC error so we don't silently downgrade a registered agent.
      return {
        kind: 'rpc-error',
        chain,
        error: err instanceof Error ? err.message : String(err),
      };
    }
  }

  private allow(
    address: string,
    chain: VaultfireChain,
    verification: TrustVerification | null,
    score: number,
    tier: string,
    reason: string
  ): GuardDecision {
    return {
      allow: true,
      reason,
      address,
      chain,
      score,
      tier,
      verification: verification as TrustVerification,
    };
  }

  private deny(
    address: string,
    chain: VaultfireChain,
    verification: TrustVerification | null,
    score: number,
    reason: string
  ): GuardDecision {
    if (this.warnOnly) {
      return {
        allow: true,
        reason: `[warn-only] ${reason}`,
        address,
        chain,
        score,
        tier: verification?.streetCred.tier ?? 'unranked',
        verification: verification as TrustVerification,
      };
    }
    return {
      allow: false,
      reason,
      address,
      chain,
      score,
      tier: verification?.streetCred.tier ?? 'unranked',
      verification: verification as TrustVerification,
    };
  }
}

// ============================================================================
// Errors
// ============================================================================

export class VaultfireGuardError extends Error {
  readonly decision: GuardDecision;
  constructor(message: string, decision: GuardDecision) {
    super(message);
    this.name = 'VaultfireGuardError';
    this.decision = decision;
  }
}

// ============================================================================
// Helpers
// ============================================================================

function isValidAddress(address: string): boolean {
  return /^0x[a-fA-F0-9]{40}$/.test(address);
}

/**
 * Retry an onchain read up to 3 times on transient RPC errors (rate limits,
 * timeouts, server errors). Contract reverts are NOT retried — they're a
 * definitive answer from the chain.
 *
 * Backoff: 250ms, 750ms, 2000ms. Total worst case ~3s before giving up.
 */
async function retryOnRpcError<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
  const delays = [250, 750, 2000];
  let lastErr: unknown;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      // If it's a real contract revert, don't retry — the chain has spoken.
      if (isContractRevert(err)) throw err;
      // Otherwise it's an RPC-side issue — wait and try again.
      if (attempt < maxAttempts - 1) {
        await new Promise((r) => setTimeout(r, delays[attempt]));
      }
    }
  }
  throw lastErr;
}

/**
 * Detect ethers.js contract-revert errors that mean "the call reverted on the
 * contract side" (so the address is unregistered / unknown), as opposed to
 * RPC-side failures (rate limits, timeouts, server errors).
 *
 * ethers v6 surfaces both contract reverts AND certain RPC errors as
 * code=CALL_EXCEPTION with data=null. We must look INSIDE err.info.error to
 * distinguish them:
 *
 *   - Real contract revert: no JSON-RPC error envelope, or err.info.error has
 *     a contract-level error code (typically 3, or no code at all).
 *   - Node/RPC failure: err.info.error.code is in the JSON-RPC reserved range
 *     (-32000 to -32099 covers "server error" including rate limits like
 *     -32016 "over rate limit" on Base mainnet, -32005 "limit exceeded" on
 *     Alchemy, etc.).
 *
 * When in doubt, we treat as RPC-error (fail-closed) rather than unregistered
 * (which could pass under allowUnregistered).
 */
function isContractRevert(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as {
    code?: unknown;
    info?: { error?: { code?: unknown; message?: unknown } };
    shortMessage?: unknown;
  };

  if (e.code !== 'CALL_EXCEPTION') return false;

  // If the underlying JSON-RPC envelope reports a server-side error, this is
  // an RPC failure — NOT a contract revert. Server errors are -32000..-32099.
  const innerCode = e.info?.error?.code;
  if (typeof innerCode === 'number' && innerCode <= -32000 && innerCode >= -32099) {
    return false;
  }

  // Heuristic on message text — covers providers that don't follow the
  // -320xx convention (e.g. "rate limit", "too many requests", "timeout").
  const msg = (
    typeof e.info?.error?.message === 'string'
      ? e.info.error.message
      : typeof e.shortMessage === 'string'
        ? e.shortMessage
        : ''
  ).toLowerCase();
  if (
    msg.includes('rate limit') ||
    msg.includes('too many requests') ||
    msg.includes('timeout') ||
    msg.includes('limit exceeded') ||
    msg.includes('throttl')
  ) {
    return false;
  }

  return true;
}

/**
 * Default extractor for x402 `payTo`. Reads either the JSON body
 * (`{ payTo: "0x..." }` or `{ accepts: [{ payTo: "0x..." }] }`) or
 * the `X-Payment-Address` header.
 */
async function defaultExtractPayTo(response: Response): Promise<string | null> {
  const headerAddr = response.headers.get('x-payment-address');
  if (headerAddr && isValidAddress(headerAddr)) return headerAddr;

  try {
    const body = (await response.json()) as Record<string, unknown>;
    const payTo = body?.payTo;
    if (typeof payTo === 'string' && isValidAddress(payTo)) {
      return payTo;
    }
    const accepts = body?.accepts;
    if (Array.isArray(accepts) && accepts.length > 0) {
      const first = accepts[0] as { payTo?: unknown };
      if (typeof first?.payTo === 'string' && isValidAddress(first.payTo)) {
        return first.payTo;
      }
    }
  } catch {
    // Not JSON; nothing to do.
  }
  return null;
}

// ============================================================================
// Convenience function
// ============================================================================

/**
 * One-shot trust check. Equivalent to `new VaultfireGuard(config).check(address)`.
 */
export async function checkTrust(
  address: string,
  config: GuardConfig = {}
): Promise<GuardDecision> {
  return new VaultfireGuard(config).check(address);
}

export type { VaultfireChain, TrustVerification } from '@vaultfire/langchain';

// ============================================================================
// x402 batch-settlement helpers (v1.1.0+, pure-additive)
// ============================================================================

export {
  BATCH_SETTLEMENT_DOMAIN,
  BATCH_SETTLEMENT_ADDRESS,
  ERC3009_DEPOSIT_COLLECTOR_ADDRESS,
  PERMIT2_DEPOSIT_COLLECTOR_ADDRESS,
  MIN_WITHDRAW_DELAY_SECONDS,
  MAX_WITHDRAW_DELAY_SECONDS,
  isValidWithdrawDelay,
  checkChannel,
  verifyVoucherCounterparty,
  assertChannelTrusted,
  ChannelTrustError,
} from './batch-settlement.js';

export type {
  ChannelConfigLike,
  ChannelTrustRecommendation,
  ChannelTrustDecision,
  VoucherCounterpartyDecision,
  CheckChannelOptions,
  VerifyVoucherCounterpartyOptions,
} from './batch-settlement.js';
