/**
 * @vaultfire/x402-guard / batch-settlement
 *
 * Voucher-aware trust checks for the x402 batch-settlement scheme.
 *
 * Two entry points:
 *
 *   1. checkChannel({ payer, payerAuthorizer, receiver })
 *      Pre-flight gate before a channel is opened. Verifies BOTH sides have a
 *      live Vaultfire bond. Use this on the seller side before accepting a
 *      deposit, and on the buyer side before signing a deposit authorization.
 *
 *   2. verifyVoucherCounterparty({ payerAuthorizer })
 *      Hot-path gate that runs on every incoming voucher. Cached, cheap.
 *      Returns a recommendation so the seller can throttle / reject without
 *      blocking the request loop.
 *
 * SOURCE OF TRUTH for the batch-settlement scheme:
 *   github.com/x402-foundation/x402 @ commit ca60063c9057b9ca0215bfde179f0e209bcc7ad9
 *   Cantina audit: https://cantina.xyz/portfolio/5035772b-2800-4f00-bb62-e7994419f8dd
 *
 * This module knows nothing about the x402 contract internals — it only
 * verifies that the ADDRESSES in a ChannelConfig have live Vaultfire bonds.
 * No private keys, no signatures, no chain writes. Read-only RPC.
 */

import { VaultfireGuard } from './index.js';
import type { GuardDecision, GuardConfig } from './index.js';
import type { VaultfireChain } from '@vaultfire/langchain';

// ============================================================================
// x402 batch-settlement constants (verified verbatim against the foundation
// repo @ commit ca60063c). DO NOT edit these without re-verifying the source.
// ============================================================================

/** EIP-712 domain shared by all batch-settlement typed-data signatures. */
export const BATCH_SETTLEMENT_DOMAIN = Object.freeze({
  name: 'x402 Batch Settlement',
  version: '1',
} as const);

/** Deployed address of the x402BatchSettlement contract. */
export const BATCH_SETTLEMENT_ADDRESS =
  '0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003' as const;

/** Deployed address of the ERC3009DepositCollector. */
export const ERC3009_DEPOSIT_COLLECTOR_ADDRESS =
  '0x4020806089470a89826cB9fB1f4059150b550004' as const;

/** Deployed address of the Permit2DepositCollector. */
export const PERMIT2_DEPOSIT_COLLECTOR_ADDRESS =
  '0x4020425FAf3B746C082C2f942b4E5159887B0005' as const;

/** Minimum withdraw delay in seconds (15 minutes), matching the onchain constant. */
export const MIN_WITHDRAW_DELAY_SECONDS = 900;

/** Maximum withdraw delay in seconds (30 days), matching the onchain constant. */
export const MAX_WITHDRAW_DELAY_SECONDS = 2_592_000;

// ============================================================================
// Types — a deliberately narrow subset of the foundation SDK so this package
// has zero runtime dependency on @x402-foundation/* and the addresses are
// frozen at our verification commit. Consumers can adapt their own
// ChannelConfig type by mapping field names.
// ============================================================================

/** Minimal ChannelConfig fields the guard needs to evaluate trust. */
export interface ChannelConfigLike {
  /** Funds source. */
  payer: string;
  /** Address authorized to sign vouchers on behalf of the payer. */
  payerAuthorizer: string;
  /** Funds destination. */
  receiver: string;
  /** Address authorized to sign claim batches on behalf of the receiver. */
  receiverAuthorizer: string;
  /** ERC-20 token contract (USDC, etc.). */
  token: string;
  /** Withdrawal cooldown in seconds. Must be in [900, 2_592_000]. */
  withdrawDelay: number;
  /** Random salt that makes channelId unique per (payer, receiver) pair. */
  salt: string;
}

export type ChannelTrustRecommendation =
  | 'open'
  | 'open-with-cap'
  | 'decline'
  | 'single-payment-only';

export interface ChannelTrustDecision {
  /** True if the channel should be opened. */
  allow: boolean;
  /** Aggregated recommendation, finer-grained than `allow`. */
  recommendation: ChannelTrustRecommendation;
  /** Reason summarizing the decision. */
  reason: string;
  /** Per-side guard decisions. */
  payerTrust: GuardDecision;
  receiverTrust: GuardDecision;
  /** withdrawDelay sanity check result (must be in [900, 2_592_000]). */
  withdrawDelayOk: boolean;
  /** Combined score for quick logging (average of payer + receiver). */
  combinedScore: number;
}

export interface VoucherCounterpartyDecision {
  /** True if the voucher should be accepted. */
  allow: boolean;
  /** payerAuthorizer that was checked. */
  payerAuthorizer: string;
  /** Underlying guard decision. */
  guardDecision: GuardDecision;
  /** Whether the result came from the cache. */
  cached: boolean;
  /** Cache entry age in ms (0 if fresh fetch). */
  cacheAgeMs: number;
}

// ============================================================================
// withdrawDelay validation
// ============================================================================

export function isValidWithdrawDelay(seconds: number): boolean {
  return (
    Number.isInteger(seconds) &&
    seconds >= MIN_WITHDRAW_DELAY_SECONDS &&
    seconds <= MAX_WITHDRAW_DELAY_SECONDS
  );
}

// ============================================================================
// Channel-level trust check
// ============================================================================

export interface CheckChannelOptions extends GuardConfig {
  /**
   * If both sides pass but the receiver's score is below this threshold,
   * recommend 'open-with-cap' so the seller scales the deposit conservatively.
   * Default: 60.
   */
  receiverConfidenceThreshold?: number;
}

/**
 * Pre-flight trust gate for a batch-settlement channel.
 *
 * Verifies both the payer and the receiver have live Vaultfire identities
 * across the configured chains. Read-only — never touches funds, never signs.
 */
export async function checkChannel(
  channel: ChannelConfigLike,
  options: CheckChannelOptions = {}
): Promise<ChannelTrustDecision> {
  const { receiverConfidenceThreshold = 60, ...guardConfig } = options;
  const guard = new VaultfireGuard(guardConfig);

  // Always check the AUTHORIZERS, not the raw payer/receiver — the authorizer
  // is the address that actually signs vouchers/claims, so that's the address
  // that needs to be bond-backed.
  const [payerTrust, receiverTrust] = await Promise.all([
    guard.check(channel.payerAuthorizer),
    guard.check(channel.receiverAuthorizer),
  ]);

  const withdrawDelayOk = isValidWithdrawDelay(channel.withdrawDelay);

  const combinedScore = Math.round((payerTrust.score + receiverTrust.score) / 2);

  // Decision matrix:
  //   - both trusted + withdraw delay valid     -> open (cap if receiver below threshold)
  //   - only payer trusted                      -> single-payment-only (don't open a channel)
  //   - only receiver trusted                   -> decline (we can't trust the voucher source)
  //   - neither trusted, or bad withdraw delay  -> decline
  let recommendation: ChannelTrustRecommendation;
  let reason: string;
  let allow: boolean;

  if (!withdrawDelayOk) {
    allow = false;
    recommendation = 'decline';
    reason = `withdrawDelay=${channel.withdrawDelay}s is outside [${MIN_WITHDRAW_DELAY_SECONDS}, ${MAX_WITHDRAW_DELAY_SECONDS}]`;
  } else if (payerTrust.allow && receiverTrust.allow) {
    allow = true;
    if (receiverTrust.score < receiverConfidenceThreshold) {
      recommendation = 'open-with-cap';
      reason = `Both sides bonded; receiver score ${receiverTrust.score} below ${receiverConfidenceThreshold} — open with a conservative cap`;
    } else {
      recommendation = 'open';
      reason = `Both sides bonded on Vaultfire (payer ${payerTrust.score}/${receiverTrust.score} on ${payerTrust.chain}/${receiverTrust.chain})`;
    }
  } else if (payerTrust.allow && !receiverTrust.allow) {
    allow = false;
    recommendation = 'single-payment-only';
    reason = `Payer bonded but receiverAuthorizer has no live Vaultfire bond — channel rejected. Fall back to single x402 payment.`;
  } else {
    allow = false;
    recommendation = 'decline';
    reason = `payerAuthorizer trust=${payerTrust.allow}, receiverAuthorizer trust=${receiverTrust.allow}. Reason: ${payerTrust.reason} | ${receiverTrust.reason}`;
  }

  return {
    allow,
    recommendation,
    reason,
    payerTrust,
    receiverTrust,
    withdrawDelayOk,
    combinedScore,
  };
}

// ============================================================================
// Voucher hot-path check — cached
// ============================================================================

const DEFAULT_VOUCHER_CACHE_TTL_MS = 60_000; // 1 minute

type CacheEntry = { decision: GuardDecision; at: number };

const voucherCache = new Map<string, CacheEntry>();

export interface VerifyVoucherCounterpartyOptions extends GuardConfig {
  /** Cache TTL in ms. Default: 60_000. Set to 0 to disable. */
  cacheTtlMs?: number;
}

/**
 * Hot-path voucher gate. Verifies the channel's payerAuthorizer still has a
 * live bond on Vaultfire. Cached for short TTL so a busy agent loop doesn't
 * hammer the RPC.
 */
export async function verifyVoucherCounterparty(
  payerAuthorizer: string,
  options: VerifyVoucherCounterpartyOptions = {}
): Promise<VoucherCounterpartyDecision> {
  const { cacheTtlMs = DEFAULT_VOUCHER_CACHE_TTL_MS, ...guardConfig } = options;
  const cacheKey = payerAuthorizer.toLowerCase();
  const cached = voucherCache.get(cacheKey);
  const now = Date.now();

  if (cached && cacheTtlMs > 0 && now - cached.at < cacheTtlMs) {
    return {
      allow: cached.decision.allow,
      payerAuthorizer,
      guardDecision: cached.decision,
      cached: true,
      cacheAgeMs: now - cached.at,
    };
  }

  const guard = new VaultfireGuard(guardConfig);
  const decision = await guard.check(payerAuthorizer);
  if (cacheTtlMs > 0) {
    voucherCache.set(cacheKey, { decision, at: now });
  }

  return {
    allow: decision.allow,
    payerAuthorizer,
    guardDecision: decision,
    cached: false,
    cacheAgeMs: 0,
  };
}

/** For tests — clears the voucher cache. */
export function _clearVoucherCache(): void {
  voucherCache.clear();
}

// ============================================================================
// Convenience: assert the channel is bond-backed or throw.
// ============================================================================

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

export async function assertChannelTrusted(
  channel: ChannelConfigLike,
  options: CheckChannelOptions = {}
): Promise<ChannelTrustDecision> {
  const decision = await checkChannel(channel, options);
  if (!decision.allow) {
    throw new ChannelTrustError(
      `Vaultfire channel trust check failed: ${decision.reason}`,
      decision
    );
  }
  return decision;
}

// Re-export the chain type for ergonomics.
export type { VaultfireChain };
