/**
 * CDP-powered x402 resource server.
 *
 * `createX402Server()` is the primary entry point. It accepts a route
 * configuration (and optional receiver wallet config), provisions EVM and
 * Solana receiver wallets automatically, and returns a fully initialized
 * `X402Server`.
 *
 * `X402Server` **extends** `x402HTTPResourceServer` — it is a drop-in
 * replacement and can be passed anywhere an `x402HTTPResourceServer` is
 * expected (e.g. `paymentMiddlewareFromHTTPServer`, Hono / Next.js adapters).
 *
 * Routes may be supplied in either format:
 *
 * **Simplified CDP format** (`CdpRouteConfig`) — just `price` and optional
 * `description` / `networks` / `paymentFlow`. The server fills in `scheme`,
 * `payTo`, and all x402 internals automatically. `paymentFlow` is exact-scheme
 * only (`"authorization"` default, or `"upfront"`) and is applied to every
 * network the route expands to (Base and Solana when `networks` is omitted).
 *
 * **Full x402 format** (`RouteConfig`) — the same `accepts` / `description`
 * shape accepted by `x402HTTPResourceServer`. Vacant `payTo` fields (`""`)
 * are filled with the provisioned receiver address for that network family.
 *
 * Both formats can be mixed within the same `routes` map.
 *
 * The `payToConfig` field controls how the receiver wallet address is resolved:
 * - `{ type: "eoa" }` (default) — provisions a CDP EVM EOA + Solana account
 * - `{ type: "smart", ownerAccountName }` — provisions a CDP Smart Contract Wallet
 * - `{ type: "address", evm, solana }` — uses provided addresses directly, no provisioning
 *
 * @example Minimal CDP Dev setup:
 * ```typescript
 * import { createX402Server } from "@coinbase/cdp-sdk/x402";
 * import { paymentMiddlewareFromHTTPServer } from "@x402/express";
 *
 * // Set: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET
 * const server = await createX402Server({
 *   routes: {
 *     "GET /report": { price: "$0.01", description: "AI-generated report" },
 *   },
 * });
 * app.use(paymentMiddlewareFromHTTPServer(server));
 * console.log("EVM receiver:", server.payToEvmAddress);
 * console.log("Solana receiver:", server.payToSvmAddress);
 * ```
 *
 * @example Bring your own addresses (skip wallet provisioning):
 * ```typescript
 * const server = await createX402Server({
 *   routes: { "GET /report": { price: "$0.01" } },
 *   payToConfig: { type: "address", evm: "0x...", solana: "..." },
 * });
 * ```
 *
 * @example Full x402 format (interop with x402HTTPResourceServer):
 * ```typescript
 * const server = await createX402Server({
 *   routes: {
 *     "GET /report": {
 *       accepts: [
 *         { scheme: "exact", price: "$0.01", network: "eip155:8453", payTo: "" },
 *         { scheme: "exact", price: "$0.01", network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", payTo: "" },
 *       ],
 *     },
 *   },
 * });
 * ```
 */

import { readFile } from "node:fs/promises";

import { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server";

import { assertBuilderCode, declareServerBuilderCodeExtension } from "./builder-code.js";
import {
  baseMainnetCaip2,
  baseSepoliaCaip2,
  solanaMainnetCaip2,
  solanaDevnetCaip2,
} from "./constants.js";
import { createCdpFacilitatorClient } from "./facilitator.js";
import {
  getCdpDefaultSchemes,
  getCdpExtensionRegistrations,
  CDP_SUPPORTED_EXTENSIONS,
  CDP_EXTENSION_BAZAAR,
  CDP_EXTENSION_BUILDER_CODE,
  buildBazaarDeclaration,
} from "./server-extensions.js";
import { findSmartAccountByOwner, isOwnerAlreadyHasSmartWalletError } from "./smart-account.js";
import { CdpClient } from "../client/cdp.js";

import type { RoutesConfig, RouteConfig } from "@x402/core/server";
import type { Network } from "@x402/core/types";
import type { BuilderCodeRequiredExtension } from "@x402/extensions/builder-code";
import type { Address } from "viem";

/*
 * ---------------------------------------------------------------------------
 * Default networks
 * ---------------------------------------------------------------------------
 */

const DEFAULT_SERVER_ACCOUNT_NAME = "x402-receiver-wallet-1";

/**
 * Default EVM networks (production) — Base mainnet (`eip155:8453`).
 */
export const CDP_SERVER_DEFAULT_EVM_NETWORKS = [baseMainnetCaip2] as const;

/**
 * Default Solana networks (production) — Solana mainnet.
 */
export const CDP_SERVER_DEFAULT_SVM_NETWORKS = [solanaMainnetCaip2] as const;

/**
 * Default networks for production: Base mainnet + Solana mainnet.
 */
export const CDP_SERVER_DEFAULT_NETWORKS: readonly string[] = [
  ...CDP_SERVER_DEFAULT_EVM_NETWORKS,
  ...CDP_SERVER_DEFAULT_SVM_NETWORKS,
];

/**
 * Default EVM networks for development — Base Sepolia (`eip155:84532`).
 */
export const CDP_SERVER_DEVELOPMENT_EVM_NETWORKS = [baseSepoliaCaip2] as const;

/**
 * Default Solana networks for development — Solana Devnet.
 */
export const CDP_SERVER_DEVELOPMENT_SVM_NETWORKS = [solanaDevnetCaip2] as const;

/**
 * Default networks for development: Base Sepolia + Solana Devnet.
 */
export const CDP_SERVER_DEVELOPMENT_NETWORKS: readonly string[] = [
  ...CDP_SERVER_DEVELOPMENT_EVM_NETWORKS,
  ...CDP_SERVER_DEVELOPMENT_SVM_NETWORKS,
];

/*
 * ---------------------------------------------------------------------------
 * Types
 * ---------------------------------------------------------------------------
 */

/**
 * Payment scheme identifiers supported by the simplified CDP route format.
 *
 * - `"exact"` — (default) Transfer a fixed amount, locked at signing time.
 * - `"upto"` — Usage-based billing: the client authorizes a maximum amount
 *   and the server settles the actual amount charged (≤ max), via Permit2 on
 *   EVM and a payment channel on Solana.
 *
 * Both support EVM and Solana networks. Solana `upto` delegates voucher signing
 * to the facilitator.
 */
export type CdpPaymentScheme = "exact" | "upto";

/**
 * Payment flows supported on the simplified CDP route format.
 *
 * The shorthand is exact-scheme only:
 * - `"authorization"` — (default) verify, run the handler, then settle.
 * - `"upfront"` — settle on-chain before the handler runs.
 */
export type CdpPaymentFlow = "authorization" | "upfront";

const CDP_EXACT_PAYMENT_FLOWS: ReadonlySet<string> = new Set(["authorization", "upfront"]);

/**
 * Simplified CDP-owned route configuration.
 *
 * Specifying just `price` (and optionally `description` / `networks` /
 * `paymentFlow`) is enough for most routes. `createX402Server` automatically
 * expands this into the full x402 `RouteConfig` format with `scheme`, `payTo`,
 * and `maxTimeoutSeconds` filled in.
 *
 * For routes that need fine-grained control (custom scheme, `extra`, explicit
 * `payTo`, etc.) pass a full x402 `RouteConfig` instead — both formats are
 * accepted in the same `routes` map.
 */
export interface CdpRouteConfig {
  /**
   * Payment amount required to access this route, e.g. `"$0.01"`.
   * Accepts any amount string supported by the x402 protocol.
   */
  price: string;
  /** Human-readable description of what this route provides. */
  description?: string;
  /**
   * Payment scheme to use for this route.
   *
   * Defaults to `"exact"`. Both `"exact"` and `"upto"` support Base and
   * Solana out of the box — see {@link CdpPaymentScheme}.
   */
  scheme?: CdpPaymentScheme;
  /**
   * Payment flow for this route. Exact-scheme only.
   *
   * Defaults to `"authorization"` (the x402 protocol default) when omitted.
   * `"upfront"` settles on-chain before the route handler runs, and is
   * stamped onto every payment option this route expands to — including the
   * default Base and Solana networks when `networks` is omitted.
   *
   * Throws if set on a non-`"exact"` scheme. For other schemes, pass a
   * full x402 route with `accepts[].extra.paymentFlow`.
   */
  paymentFlow?: CdpPaymentFlow;
  /**
   * CAIP-2 network identifiers for which the route accepts payments.
   * Defaults to `CDP_SERVER_DEFAULT_NETWORKS` (Base mainnet + Solana mainnet),
   * or `CDP_SERVER_DEVELOPMENT_NETWORKS` (Base Sepolia + Solana Devnet) when
   * `environment` is `"development"`.
   */
  networks?: string[];
  /**
   * Maximum seconds a payment token is valid before expiry.
   * Defaults to `300` (5 minutes).
   */
  maxTimeoutSeconds?: number;
  /**
   * Extension overrides for this route.
   *
   * Gas-sponsoring, `bazaar`, and `builder-code` are injected automatically on
   * EVM routes (`builder-code` always carries the SDK's own service code, plus
   * the server config's `builderCode` app code when set). Use this field to
   * override the auto-generated Bazaar or builder-code declaration.
   */
  extensions?: Record<string, unknown>;
}

/**
 * Receiver wallet configuration for `createX402Server`.
 *
 * Controls how receiver wallet addresses are resolved:
 * - `"eoa"` — provision a CDP EVM EOA + Solana account (default)
 * - `"smart"` — provision a CDP Smart Contract Wallet + Solana account
 * - `"address"` — use provided addresses directly, no provisioning needed
 */
export type PayToConfig =
  | {
      /** CDP Server Wallet (EOA). Default when `type` is omitted. */
      type: "eoa";
      /**
       * Named CDP account to use as the receiver wallet.
       * Defaults to `"x402-receiver-wallet-1"`.
       */
      accountName?: string;
    }
  | {
      /** CDP Smart Contract Wallet. */
      type: "smart";
      /**
       * Named CDP smart account. Defaults to `"x402-receiver-wallet-1"`.
       */
      accountName?: string;
      /**
       * Owner EOA account name. Required for `"smart"` type.
       */
      ownerAccountName: string;
    }
  | {
      /**
       * Use provided addresses directly.
       * No wallet provisioning is performed — `CDP_WALLET_SECRET` is not required.
       * API key credentials (`CDP_API_KEY_ID` / `CDP_API_KEY_SECRET`) are still
       * needed for the CDP facilitator.
       * At least one of `evm` or `solana` should be provided, or all routes
       * must supply explicit `payTo` values in the full x402 `RouteConfig` format.
       */
      type: "address";
      /** EVM address to receive payments. */
      evm?: Address;
      /** Solana address to receive payments. */
      solana?: string;
    };

/**
 * Configuration for `createX402Server()`.
 *
 * All credential fields fall back to environment variables, so an empty
 * object `{}` with a `routes` map is sufficient in most environments.
 *
 * Pass `configPath` to load routes (and optionally credentials) from a JSON
 * file instead of specifying them inline.
 */
export interface CdpX402ServerConfig {
  /**
   * CDP API key ID.
   * Falls back to `CDP_API_KEY_ID` env var.
   */
  apiKeyId?: string;
  /**
   * CDP API key secret.
   * Falls back to `CDP_API_KEY_SECRET` env var.
   */
  apiKeySecret?: string;
  /**
   * CDP wallet secret used to provision the receiver wallet.
   * Falls back to `CDP_WALLET_SECRET` env var.
   * Not required when `payToConfig.type` is `"address"`.
   */
  walletSecret?: string;
  /**
   * Deployment environment. Controls which networks are used by default.
   *
   * - `"production"` (default) — Base mainnet + Solana mainnet.
   * - `"development"` — Base Sepolia + Solana Devnet.
   *
   * Falls back to `CDP_X402_SERVER_ENVIRONMENT` env var.
   */
  environment?: "production" | "development";
  /**
   * Receiver wallet / payTo configuration.
   *
   * Defaults to `{ type: "eoa" }` which provisions a CDP Server Wallet (EOA)
   * named `"x402-receiver-wallet-1"`.
   *
   * Use `{ type: "address", evm: "0x...", solana: "..." }` to provide your
   * own addresses without provisioning a CDP wallet.
   */
  payToConfig?: PayToConfig;
  /**
   * Optional [builder code](https://github.com/x402-foundation/x402/blob/main/specs/extensions/builder_code.md)
   * for on-chain attribution (`a` / app code).
   *
   * Every route with an EVM (`eip155:*`) payment option always advertises the
   * `builder-code` extension with this SDK's own service code (`s`); Solana-only
   * routes are skipped because the attribution suffix is ERC-8021 EVM calldata.
   * Setting `builderCode` additionally declares your own app code (`a`) on
   * those routes. Must match `^[a-z0-9_]{1,32}$`. Override per-route via
   * `extensions["builder-code"]`.
   */
  builderCode?: string;
  /**
   * Payment-protected routes served by this server.
   *
   * Map of HTTP method + path pattern → route config.
   * Keys use the `"METHOD /path"` convention, e.g. `"GET /report"`.
   *
   * Each value is either:
   * - A `CdpRouteConfig` — simplified format, just `price` + optional fields.
   * - A `RouteConfig` — full x402 format with an `accepts` array/object.
   *
   * Both formats can be mixed within the same map.
   * May be omitted when `configPath` supplies the routes.
   */
  routes?: Record<string, CdpRouteConfig | RouteConfig>;
  /**
   * Path to a JSON file whose fields are merged with this inline config.
   * Inline config takes precedence over file config when both specify the
   * same field. The file mirrors {@link CdpX402ServerConfig} (minus `configPath`).
   *
   * @example
   * ```json
   * {
   *   "routes": {
   *     "GET /report": { "price": "$0.01", "description": "AI-generated report" }
   *   }
   * }
   * ```
   *
   * A full JSON Schema for the file lives at
   * `examples/typescript/x402/servers/express/x402.config.schema.json`.
   *
   * Security: this file may carry credentials (`apiKeySecret` / `walletSecret`).
   * Prefer environment variables for credentials and use the file for `routes`;
   * if secrets are stored here, keep the file out of version control.
   */
  configPath?: string;
}

/*
 * ---------------------------------------------------------------------------
 * Internal wallet provisioning
 * ---------------------------------------------------------------------------
 */

interface ProvisionedAddresses {
  evmAddress: Address | "";
  svmAddress: string;
  ownerWallet?: string;
}

/** Network families referenced by a route set, controlling which wallets to provision. */
interface NetworkFamilies {
  evm: boolean;
  svm: boolean;
}

/**
 * Resolves server-scoped CDP credentials and environment, falling back from
 * explicit config to `CDP_*` env vars
 *
 * @param config - Optional explicit credential overrides.
 * @returns Resolved credential strings and environment (any credential may be `undefined` if not found).
 */
function resolveServerCredentials(
  config: Pick<CdpX402ServerConfig, "apiKeyId" | "apiKeySecret" | "walletSecret" | "environment">,
): {
  apiKeyId: string | undefined;
  apiKeySecret: string | undefined;
  walletSecret: string | undefined;
  environment: "production" | "development";
} {
  const rawEnv =
    config.environment ??
    (process.env.CDP_X402_SERVER_ENVIRONMENT as "production" | "development" | undefined);
  const environment: "production" | "development" =
    rawEnv === "development" ? "development" : "production";

  return {
    apiKeyId: config.apiKeyId ?? process.env.CDP_API_KEY_ID,
    apiKeySecret: config.apiKeySecret ?? process.env.CDP_API_KEY_SECRET,
    walletSecret: config.walletSecret ?? process.env.CDP_WALLET_SECRET,
    environment,
  };
}

/**
 * Provisions CDP EVM and Solana receiver accounts for use as `payTo` addresses.
 *
 * @param credentials - CDP API credentials for wallet operations.
 * @param credentials.apiKeyId - CDP API key ID.
 * @param credentials.apiKeySecret - CDP API key secret.
 * @param credentials.walletSecret - CDP wallet secret for signing.
 * @param payToConfig - Resolved wallet type (`"eoa"` or `"smart"`) and account names.
 * @param need - Which network families the routes actually reference. Only the
 *   referenced families are provisioned, so an EVM-only server never creates a
 *   Solana account (and vice versa).
 * @returns Provisioned EVM address, Solana address, and optional owner wallet name.
 */
async function provisionServerAccounts(
  credentials: { apiKeyId: string; apiKeySecret: string; walletSecret: string },
  payToConfig: Exclude<PayToConfig, { type: "address" }>,
  need: NetworkFamilies,
): Promise<ProvisionedAddresses> {
  const cdpClient = new CdpClient({
    apiKeyId: credentials.apiKeyId,
    apiKeySecret: credentials.apiKeySecret,
    walletSecret: credentials.walletSecret,
  });

  const accountName = payToConfig.accountName ?? DEFAULT_SERVER_ACCOUNT_NAME;

  const svmAddress = need.svm
    ? (await cdpClient.solana.getOrCreateAccount({ name: accountName })).address
    : "";

  if (!need.evm) {
    return { evmAddress: "", svmAddress };
  }

  if (payToConfig.type === "smart") {
    const ownerAccount = await cdpClient.evm.getOrCreateAccount({
      name: payToConfig.ownerAccountName,
    });

    let smartAccount;
    try {
      smartAccount = await cdpClient.evm.getOrCreateSmartAccount({
        name: accountName,
        owner: ownerAccount,
      });
    } catch (error) {
      if (isOwnerAlreadyHasSmartWalletError(error)) {
        const existingAddress = await findSmartAccountByOwner(cdpClient, ownerAccount.address);
        if (!existingAddress) throw error;
        smartAccount = await cdpClient.evm.getSmartAccount({
          address: existingAddress as Address,
          owner: ownerAccount,
        });
      } else {
        throw error;
      }
    }

    return {
      evmAddress: smartAccount.address as Address,
      svmAddress,
      ownerWallet: payToConfig.ownerAccountName,
    };
  }

  const evmAccount = await cdpClient.evm.getOrCreateAccount({ name: accountName });
  return {
    evmAddress: evmAccount.address as Address,
    svmAddress,
  };
}

/*
 * ---------------------------------------------------------------------------
 * Route resolution helpers
 * ---------------------------------------------------------------------------
 */

/**
 * Classifies a CAIP-2 network identifier into a receiver-wallet family.
 *
 * @param network - CAIP-2 network identifier, e.g. `"eip155:8453"`.
 * @returns `"evm"` for `eip155:*`, `"svm"` for `solana:*`, otherwise `"other"`.
 */
function networkFamily(network: string): "evm" | "svm" | "other" {
  if (network.startsWith("eip155:")) return "evm";
  if (network.startsWith("solana:")) return "svm";
  return "other";
}

/**
 * Returns the default networks for a simplified route that omits `networks`.
 * Both supported schemes (`"exact"` and `"upto"`) cover Base + Solana.
 *
 * @param environment - Deployment environment controlling mainnet vs testnet defaults.
 * @returns The default CAIP-2 networks.
 */
function getDefaultNetworks(environment: "production" | "development"): readonly string[] {
  return environment === "development"
    ? CDP_SERVER_DEVELOPMENT_NETWORKS
    : CDP_SERVER_DEFAULT_NETWORKS;
}

/**
 * Determines which network families a route references.
 *
 * @param route - Simplified or full x402 route config.
 * @param environment - Deployment environment controlling default network selection.
 * @returns The network families referenced by the route.
 */
function routeNetworkFamilies(
  route: CdpRouteConfig | RouteConfig,
  environment: "production" | "development",
): NetworkFamilies {
  let networks: string[];
  if ("accepts" in route) {
    const accepts = Array.isArray(route.accepts) ? route.accepts : [route.accepts];
    networks = accepts.map(option => option.network as string);
  } else {
    networks = [...(route.networks ?? getDefaultNetworks(environment))];
  }
  return {
    evm: networks.some(network => networkFamily(network) === "evm"),
    svm: networks.some(network => networkFamily(network) === "svm"),
  };
}

/**
 * Aggregates the network families referenced across all routes.
 *
 * @param routes - Map of route patterns to simplified or full x402 route configs.
 * @param environment - Deployment environment controlling default network selection.
 * @returns The union of network families referenced by the route set.
 */
function requiredNetworkFamilies(
  routes: Record<string, CdpRouteConfig | RouteConfig>,
  environment: "production" | "development",
): NetworkFamilies {
  const result: NetworkFamilies = { evm: false, svm: false };
  for (const route of Object.values(routes)) {
    const families = routeNetworkFamilies(route, environment);
    result.evm ||= families.evm;
    result.svm ||= families.svm;
  }
  return result;
}

/**
 * Throws when `paymentFlow` is set on a simplified route that cannot use it:
 * unknown flow names, or any scheme other than `"exact"`.
 *
 * @param route - Simplified CDP route config being converted.
 * @param scheme - Resolved scheme for the route (`"exact"` or `"upto"`).
 */
function assertPaymentFlowCompatible(route: CdpRouteConfig, scheme: CdpPaymentScheme): void {
  if (route.paymentFlow === undefined) return;
  if (!CDP_EXACT_PAYMENT_FLOWS.has(route.paymentFlow)) {
    throw new Error(
      `Unsupported paymentFlow "${String(route.paymentFlow)}". ` +
        `The simplified route format only supports "authorization" (default) and "upfront".`,
    );
  }
  if (scheme !== "exact") {
    throw new Error(
      `paymentFlow is only supported on the "exact" scheme. ` +
        `Route uses scheme "${scheme}". Omit paymentFlow, or pass a full x402 route ` +
        `with accepts[].extra.paymentFlow to configure other schemes.`,
    );
  }
}

/**
 * Simplified-route scheme identifiers accepted by {@link validateRouteSemantics}.
 * Kept separate from the `CdpPaymentScheme` type check since config files are
 * untyped JSON cast to `CdpX402ServerConfig` — a typo or unsupported scheme
 * (e.g. `"auth-capture"`) has no compile-time guard and must be rejected here.
 */
const CDP_SIMPLIFIED_ROUTE_SCHEMES: ReadonlySet<string> = new Set<CdpPaymentScheme>([
  "exact",
  "upto",
]);

/**
 * Throws when a simplified route's `scheme` isn't one of the values the
 * simplified route format actually supports.
 *
 * @param scheme - The raw `scheme` value from a simplified route config.
 * @returns The validated scheme, narrowed to {@link CdpPaymentScheme}.
 */
function assertValidSimplifiedScheme(scheme: unknown): CdpPaymentScheme {
  if (typeof scheme !== "string" || !CDP_SIMPLIFIED_ROUTE_SCHEMES.has(scheme)) {
    throw new Error(
      `Unsupported simplified route scheme "${String(scheme)}". ` +
        `Supported values: ${[...CDP_SIMPLIFIED_ROUTE_SCHEMES].map(s => `"${s}"`).join(", ")}.`,
    );
  }
  return scheme as CdpPaymentScheme;
}

/**
 * Validates every route's scheme/network and `paymentFlow` compatibility up
 * front, before any CDP or facilitator I/O. Without this, an invalid route
 * (e.g. `{ scheme: "upto", paymentFlow: "upfront" }`, an unsupported/typo'd
 * scheme such as `"auth-capture"`, an empty `networks` list, or a non-EVM/
 * non-Solana network) would only be caught during `resolveRoutes` — or not
 * at all until facilitator init — by which point `provisionServerAccounts`
 * has already created real receiver wallets for the (wrongly) inferred
 * network families.
 *
 * @param routes - Map of route patterns to simplified or full x402 route configs.
 * @param environment - Deployment environment controlling default network selection.
 */
function validateRouteSemantics(
  routes: Record<string, CdpRouteConfig | RouteConfig>,
  environment: "production" | "development",
): void {
  for (const route of Object.values(routes)) {
    if ("accepts" in route) continue;
    const scheme = assertValidSimplifiedScheme(route.scheme ?? "exact");
    assertPaymentFlowCompatible(route, scheme);
    const networks = route.networks ?? getDefaultNetworks(environment);
    if (networks.length === 0) {
      throw new Error("Simplified route must include at least one network.");
    }
    for (const network of networks) {
      if (networkFamily(network) === "other") {
        throw new Error(
          `Unsupported network family for simplified route: "${network}". ` +
            `Simplified routes only support "eip155:*" (EVM) and "solana:*" networks.`,
        );
      }
    }
  }
}

/**
 * Throws when the resolved `payTo` address for a network is empty. This can
 * happen when `payToConfig: { type: "address" }` is used without providing
 * an address for the given network family.
 *
 * @param payTo - The resolved address (may be empty string).
 * @param network - The CAIP-2 network identifier for the route option.
 */
function assertNonEmptyPayTo(payTo: string, network: string): void {
  if (payTo === "") {
    const family = network.startsWith("eip155:")
      ? "EVM (`evm`)"
      : network.startsWith("solana:")
        ? "Solana (`solana`)"
        : `"${network}"`;
    throw new Error(
      `No receiver address for ${family} network "${network}". ` +
        `Provide a receiver address via payToConfig (e.g. { type: "address", evm: "0x...", solana: "..." }) ` +
        `or use payToConfig: { type: "eoa" } to auto-provision a CDP wallet.`,
    );
  }
}

/**
 * Fills vacant `payTo` fields (`""`) in a full x402 `RouteConfig` with the
 * provisioned receiver addresses for each network family. Returns a new object —
 * the original is not mutated. Non-vacant `payTo` values are left untouched.
 *
 * @param route - The x402 `RouteConfig` whose vacant `payTo` fields will be filled.
 * @param evmAddress - EVM receiver address for `eip155:*` payment options.
 * @param svmAddress - Solana receiver address for `solana:*` payment options.
 * @returns A new `RouteConfig` with all blank `payTo` fields resolved.
 */
function fillX402RoutePayTo(
  route: RouteConfig,
  evmAddress: Address | "",
  svmAddress: string,
): RouteConfig {
  const accepts = Array.isArray(route.accepts) ? route.accepts : [route.accepts];

  const filled = accepts.map(option => {
    const network = option.network as string;

    if (typeof option.payTo !== "string" || option.payTo.trim() !== "") {
      return option;
    }
    if (network.startsWith("eip155:")) {
      assertNonEmptyPayTo(evmAddress, network);
      return { ...option, payTo: evmAddress };
    }
    if (network.startsWith("solana:")) {
      assertNonEmptyPayTo(svmAddress, network);
      return { ...option, payTo: svmAddress };
    }
    throw new Error(
      `Cannot fill vacant payTo for network "${network}": unrecognised network family. ` +
        `Provide an explicit payTo address for this route option.`,
    );
  });

  return {
    ...route,
    accepts: Array.isArray(route.accepts) ? filled : filled[0]!,
  };
}

/**
 * Converts a {@link CdpRouteConfig} into a full x402 `RouteConfig`, wiring
 * the provisioned receiver addresses into each payment option's `payTo`.
 *
 * @param route - Simplified CDP route config with `price` and optional fields.
 * @param evmAddress - EVM receiver address for `eip155:*` networks (`""` when none).
 * @param svmAddress - Solana receiver address for `solana:*` networks (`""` when none).
 * @param environment - Deployment environment controlling default network selection.
 * @param available - Which receiver families have an address. When the route
 *   relies on default networks, families without a receiver address are dropped
 *   so e.g. an EVM-only `payToConfig` works without listing `networks` explicitly.
 * @returns A full x402 `RouteConfig` with `accepts`, `payTo`, and `scheme` resolved.
 */
function convertCdpRoute(
  route: CdpRouteConfig,
  evmAddress: Address | "",
  svmAddress: string,
  environment: "production" | "development",
  available: NetworkFamilies,
): RouteConfig {
  const scheme = route.scheme ?? "exact";
  assertPaymentFlowCompatible(route, scheme);
  const usingDefaultNetworks = route.networks === undefined;
  const defaultNetworks = getDefaultNetworks(environment);
  /*
   * For default networks, drop families that have no receiver address so a
   * partial payToConfig (e.g. EVM-only) doesn't fail on an unused default
   * network. Explicit `networks` are always honored (and validated) as-is.
   */
  const networks = usingDefaultNetworks
    ? defaultNetworks.filter(network => {
        const family = networkFamily(network);
        if (family === "evm") return available.evm;
        if (family === "svm") return available.svm;
        return true;
      })
    : route.networks!;

  if (networks.length === 0) {
    throw new Error(
      `No receiver address available for any default network of scheme "${scheme}". ` +
        `Provide a receiver address via payToConfig (e.g. { type: "address", evm: "0x...", solana: "..." }), ` +
        `set explicit \`networks\` on the route, or use payToConfig: { type: "eoa" } to auto-provision a CDP wallet.`,
    );
  }

  const maxTimeoutSeconds = route.maxTimeoutSeconds ?? 300;

  const accepts = networks.map(network => {
    const payTo = network.startsWith("eip155:")
      ? (evmAddress as string)
      : network.startsWith("solana:")
        ? (svmAddress as string)
        : (() => {
            throw new Error(
              `Cannot resolve payTo for network "${network}": unrecognised network family.`,
            );
          })();
    assertNonEmptyPayTo(payTo, network);
    return {
      scheme,
      price: route.price,
      network: network as `${string}:${string}`,
      payTo,
      maxTimeoutSeconds,
      ...(route.paymentFlow === "upfront" ? { extra: { paymentFlow: "upfront" as const } } : {}),
    };
  });

  return {
    accepts: accepts.length === 1 ? accepts[0]! : accepts,
    ...(route.description !== undefined && { description: route.description }),
    ...(route.extensions !== undefined && { extensions: route.extensions }),
  };
}

/**
 * Parses an x402 route pattern key (`"GET /report"`) into its HTTP method and
 * path components for building a Bazaar discovery declaration. Returns `undefined`
 * when the pattern lacks a method prefix or uses a wildcard verb.
 *
 * @param pattern - Route pattern key, e.g. `"GET /report"` or `"POST /orders/:id"`.
 * @returns Parsed `{ method, path }` pair, or `undefined` when not parseable.
 */
function parseRouteKeyForBazaar(pattern: string): { method: string; path: string } | undefined {
  const spaceIdx = pattern.indexOf(" ");
  if (spaceIdx === -1) return undefined;

  const method = pattern.slice(0, spaceIdx).toUpperCase();
  const path = pattern.slice(spaceIdx + 1);

  if (method === "*" || !path.startsWith("/")) return undefined;
  return { method, path };
}

/**
 * Returns `true` when any payment option on a resolved route targets an EVM
 * (`eip155:*`) network. Used to gate EVM-specific extensions so a Solana-only
 * route doesn't advertise EVM gas-sponsoring keys.
 *
 * @param route - Resolved x402 `RouteConfig` to inspect.
 * @returns `true` if at least one accept option is on an EVM network.
 */
function routeHasEvmAccept(route: RouteConfig): boolean {
  const accepts = Array.isArray(route.accepts) ? route.accepts : [route.accepts];
  return accepts.some(option => (option.network as string).startsWith("eip155:"));
}

/**
 * Merges CDP auto-injected extensions into a resolved route config. Gas-sponsoring
 * extensions and builder-code are added only to routes with an EVM (`eip155:*`)
 * payment option — gas sponsoring is meaningless for Solana-only routes, and
 * builder-code attribution is an ERC-8021 EVM calldata suffix. The Bazaar
 * declaration is built from the route pattern for all routes. User-provided
 * `route.extensions` always win.
 *
 * @param pattern - Route key (e.g. `"GET /report"`) used to derive Bazaar metadata.
 * @param route - Resolved x402 `RouteConfig` to augment with CDP extensions.
 * @param builderCode - Pre-validated builder-code declaration to advertise. `create()`
 * always supplies one (at minimum the SDK's own service code); `undefined` here only
 * occurs in tests that call this function directly.
 * @returns A new `RouteConfig` with CDP extensions merged in.
 */
function withAutoInjectedExtensions(
  pattern: string,
  route: RouteConfig,
  builderCode?: BuilderCodeRequiredExtension,
): RouteConfig {
  const bazaar = parseRouteKeyForBazaar(pattern);
  const hasEvmAccept = routeHasEvmAccept(route);

  return {
    ...route,
    extensions: {
      ...(hasEvmAccept && CDP_SUPPORTED_EXTENSIONS),
      ...(bazaar && { [CDP_EXTENSION_BAZAAR]: buildBazaarDeclaration(bazaar.method, bazaar.path) }),
      ...(builderCode && hasEvmAccept && { [CDP_EXTENSION_BUILDER_CODE]: builderCode }),
      ...route.extensions,
    },
  };
}

/**
 * Resolves a mixed `Record<string, CdpRouteConfig | RouteConfig>` into the
 * x402 `RoutesConfig` format. Simplified routes are expanded; full x402
 * routes have vacant `payTo` fields filled. All CDP extensions are injected
 * into every route.
 *
 * @param routes - Map of route patterns to simplified or full x402 route configs.
 * @param evmAddress - EVM receiver address for `eip155:*` payment options (`""` when none).
 * @param svmAddress - Solana receiver address for `solana:*` payment options (`""` when none).
 * @param environment - Deployment environment controlling default network selection.
 * @param builderCode - Builder-code declaration injected on every EVM route.
 * @returns A fully resolved `RoutesConfig` ready to pass to an HTTP resource server.
 */
function resolveRoutes(
  routes: Record<string, CdpRouteConfig | RouteConfig>,
  evmAddress: Address | "",
  svmAddress: string,
  environment: "production" | "development",
  builderCode?: BuilderCodeRequiredExtension,
): RoutesConfig {
  const result: Record<string, RouteConfig> = {};
  const available: NetworkFamilies = { evm: evmAddress !== "", svm: svmAddress !== "" };

  for (const [pattern, route] of Object.entries(routes)) {
    const resolved =
      "accepts" in route
        ? fillX402RoutePayTo(route, evmAddress, svmAddress)
        : convertCdpRoute(route, evmAddress, svmAddress, environment, available);
    result[pattern] = withAutoInjectedExtensions(pattern, resolved, builderCode);
  }

  return result;
}

/**
 * Loads and parses a JSON config file, stripping `configPath` from the result
 * to prevent circular references when the field is present in the file itself.
 *
 * Security: this file may carry credentials (`apiKeySecret` / `walletSecret`).
 * Prefer supplying credentials via environment variables and reserving the
 * config file for `routes`; if secrets are stored here, keep the file out of
 * version control (e.g. add it to `.gitignore`).
 *
 * @param filePath - Absolute or relative path to the JSON config file.
 * @returns Parsed config with `configPath` removed.
 */
async function loadConfigFile(filePath: string): Promise<Omit<CdpX402ServerConfig, "configPath">> {
  const content = await readFile(filePath, "utf-8");
  const parsed = JSON.parse(content) as CdpX402ServerConfig;
  const result: Omit<CdpX402ServerConfig, "configPath"> = { ...parsed };
  delete (result as CdpX402ServerConfig).configPath;
  return result;
}

/*
 * ---------------------------------------------------------------------------
 * X402Server class
 * ---------------------------------------------------------------------------
 */

/**
 * A CDP-powered x402 resource server that **extends** `x402HTTPResourceServer`.
 *
 * It is a drop-in replacement anywhere an `x402HTTPResourceServer` is
 * expected — pass it directly to `paymentMiddlewareFromHTTPServer`, Hono /
 * Next.js adapters, or any other framework integration.
 *
 * Use `createX402Server()` (or `X402Server.create()`) to obtain an initialized
 * instance. The constructor is intentionally private; call the factory instead.
 *
 * In addition to the full `x402HTTPResourceServer` surface (`initialize()`,
 * `processHTTPRequest()`, `processSettlement()`, `requiresPayment()`, etc.)
 * this class exposes `payToEvmAddress` and `payToSvmAddress` for the
 * provisioned receiver wallets.
 */
export class X402Server extends x402HTTPResourceServer {
  /**
   * EVM address of the receiver wallet. `undefined` when no EVM wallet was
   * provisioned (no `eip155:*` route, and `payToConfig` did not supply an
   * `evm` address).
   */
  readonly payToEvmAddress: Address | undefined;
  /**
   * Solana address of the provisioned receiver wallet. `undefined` when no
   * Solana wallet was provisioned (no `solana:*` route, and `payToConfig` did
   * not supply a `solana` address).
   */
  readonly payToSvmAddress: string | undefined;
  /**
   * Owner account name for `"smart"` receiver wallets, otherwise `undefined`.
   * Only set when `payToConfig.type` is `"smart"`.
   */
  readonly ownerWallet: string | undefined;

  private _initPromise: Promise<void> | null = null;

  /**
   * Private constructor — use `createX402Server()` or `X402Server.create()` instead.
   *
   * @param resourceServer - Initialized `x402ResourceServer` with schemes registered.
   * @param routes - Resolved `RoutesConfig` with `payTo` and extensions filled in.
   * @param payToEvmAddress - EVM address of the provisioned receiver wallet,
   *   or `undefined` if none was provisioned.
   * @param payToSvmAddress - Solana address of the provisioned receiver wallet,
   *   or `undefined` if none was provisioned.
   * @param ownerWallet - Owner account name for smart wallets, otherwise `undefined`.
   * @internal
   */
  private constructor(
    resourceServer: x402ResourceServer,
    routes: RoutesConfig,
    payToEvmAddress: Address | undefined,
    payToSvmAddress: string | undefined,
    ownerWallet?: string,
  ) {
    super(resourceServer, routes);
    this.payToEvmAddress = payToEvmAddress;
    this.payToSvmAddress = payToSvmAddress;
    this.ownerWallet = ownerWallet;
  }

  /**
   * The underlying `x402ResourceServer` with EVM and Solana schemes registered.
   *
   * @returns The `x402ResourceServer` instance used by this server.
   */
  get resourceServer(): x402ResourceServer {
    return this.server;
  }

  /**
   * Provisions CDP receiver wallets (unless `payToConfig.type` is `"address"`),
   * resolves routes, constructs the HTTP resource server, and syncs supported
   * schemes with the CDP facilitator.
   *
   * This is the async entry point for `X402Server`. Prefer the module-level
   * `createX402Server()` wrapper for convenience.
   *
   * @param config - Credential, wallet, and route configuration.
   * @returns A fully initialized `X402Server` instance ready to be passed to
   *   any framework middleware.
   */
  static async create(config: CdpX402ServerConfig): Promise<X402Server> {
    // Routes are deep-merged so file and inline routes both survive; inline wins on conflicts.
    let merged = config;
    if (config.configPath) {
      const fileConfig = await loadConfigFile(config.configPath);
      merged = {
        ...fileConfig,
        ...config,
        routes: { ...fileConfig.routes, ...config.routes },
      };
    }

    // Validate before any I/O so a bad config fails before wallet provisioning.
    const routes = merged.routes;
    if (!routes || Object.keys(routes).length === 0) {
      throw new Error("createX402Server requires at least one payment route.");
    }
    if (merged.builderCode !== undefined) {
      assertBuilderCode(merged.builderCode);
    }
    /*
     * Always declared (not gated on `builderCode`) so every EVM route attaches
     * this SDK's own service code to `s`, in addition to the developer's own
     * app code (`a`) when configured.
     */
    const builderCodeDeclaration: BuilderCodeRequiredExtension = declareServerBuilderCodeExtension(
      merged.builderCode,
    );

    const credentials = resolveServerCredentials(merged);
    const { environment } = credentials;
    validateRouteSemantics(routes, environment);

    const facilitatorClient = createCdpFacilitatorClient({
      apiKeyId: credentials.apiKeyId,
      apiKeySecret: credentials.apiKeySecret,
    });

    const resourceServer = new x402ResourceServer(facilitatorClient);
    for (const ext of getCdpExtensionRegistrations()) {
      resourceServer.registerExtension(ext);
    }

    const payToConfig = merged.payToConfig;
    let evmAddress: Address | "";
    let svmAddress: string;
    let ownerWallet: string | undefined;

    if (payToConfig?.type === "address") {
      evmAddress = payToConfig.evm ?? "";
      svmAddress = payToConfig.solana ?? "";
    } else {
      const missing: string[] = [];
      if (!credentials.apiKeyId) missing.push("CDP_API_KEY_ID");
      if (!credentials.apiKeySecret) missing.push("CDP_API_KEY_SECRET");
      if (!credentials.walletSecret) missing.push("CDP_WALLET_SECRET");
      if (missing.length > 0) {
        throw new Error(
          `Missing required CDP credentials: ${missing.join(", ")}. ` +
            "Provide them via config options or set the corresponding environment variables " +
            "(CDP_API_KEY_ID / CDP_API_KEY_SECRET / CDP_WALLET_SECRET). " +
            "Alternatively, pass payToConfig: { type: 'address', evm: '0x...', solana: '...' } to skip wallet provisioning.",
        );
      }

      /*
       * Only provision the wallet families the routes actually reference, so an
       * EVM-only server never creates a Solana account (and vice versa).
       */
      const need = requiredNetworkFamilies(routes, environment);
      const provisioned = await provisionServerAccounts(
        {
          apiKeyId: credentials.apiKeyId!,
          apiKeySecret: credentials.apiKeySecret!,
          walletSecret: credentials.walletSecret!,
        },
        (payToConfig ?? { type: "eoa" }) as Exclude<PayToConfig, { type: "address" }>,
        need,
      );

      evmAddress = provisioned.evmAddress;
      svmAddress = provisioned.svmAddress;
      ownerWallet = provisioned.ownerWallet;
    }

    for (const scheme of getCdpDefaultSchemes()) {
      resourceServer.register(scheme.network as Network, scheme.server);
    }

    const resolvedRoutes = resolveRoutes(
      routes,
      evmAddress,
      svmAddress,
      environment,
      builderCodeDeclaration,
    );

    const instance = new X402Server(
      resourceServer,
      resolvedRoutes,
      evmAddress === "" ? undefined : evmAddress,
      svmAddress === "" ? undefined : svmAddress,
      ownerWallet,
    );
    await instance.initialize();
    return instance;
  }

  /**
   * Initializes the underlying resource server (facilitator support sync +
   * route validation) exactly once.
   *
   * `createX402Server()` already calls this before returning, and framework
   * adapters such as `paymentMiddlewareFromHTTPServer` call `initialize()`
   * again on startup. The base implementation re-fetches facilitator support
   * on every call, so this override memoizes the first run to avoid a redundant
   * facilitator round-trip. The promise is cleared on failure so a later call
   * can retry.
   *
   * @returns A promise that resolves once initialization has completed.
   */
  override async initialize(): Promise<void> {
    if (!this._initPromise) {
      this._initPromise = super.initialize().catch(error => {
        this._initPromise = null;
        throw error;
      });
    }
    return this._initPromise;
  }
}

/*
 * ---------------------------------------------------------------------------
 * Public factory
 * ---------------------------------------------------------------------------
 */

/**
 * Creates and initializes a CDP-powered x402 resource server.
 *
 * Returns an `X402Server` which **extends** `x402HTTPResourceServer` and can
 * be passed directly to `paymentMiddlewareFromHTTPServer` or any other
 * framework adapter.
 *
 * All credential fields fall back to environment variables; an empty `{}`
 * with a `routes` map is sufficient in most environments.
 *
 * Pass `payToConfig: { type: "address", evm: "0x...", solana: "..." }` to
 * provide your own receiver addresses without provisioning a CDP wallet.
 *
 * @param config - Credential, wallet, and route configuration. All credential fields
 *   fall back to environment variables; `routes` is the only required field (or
 *   `configPath` pointing to a JSON file that supplies routes).
 * @returns A fully initialized `X402Server` ready to be passed to any framework middleware.
 * @example Minimal setup (auto-provisions receiver wallet):
 * ```typescript
 * // Set: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET
 * const server = await createX402Server({
 *   routes: {
 *     "GET /report": { price: "$0.01", description: "AI-generated report" },
 *   },
 * });
 * app.use(paymentMiddlewareFromHTTPServer(server));
 * console.log("EVM receiver:", server.payToEvmAddress);
 * console.log("Solana receiver:", server.payToSvmAddress);
 * ```
 *
 * @example Bring your own addresses:
 * ```typescript
 * const server = await createX402Server({
 *   routes: { "GET /report": { price: "$0.01" } },
 *   payToConfig: { type: "address", evm: "0x1234...", solana: "ABC..." },
 * });
 * ```
 *
 * @example Exact + upfront payment flow (shorthand; applies to Base and Solana):
 * ```typescript
 * const server = await createX402Server({
 *   routes: {
 *     "GET /report": { price: "$0.01", paymentFlow: "upfront" },
 *   },
 * });
 * ```
 *
 * @example Full x402 RouteConfig format with vacant payTo:
 * ```typescript
 * const server = await createX402Server({
 *   routes: {
 *     "GET /report": {
 *       accepts: { scheme: "exact", price: "$0.01", network: "eip155:8453", payTo: "" },
 *     },
 *   },
 * });
 * ```
 *
 * @example Config-file variant:
 * ```typescript
 * // x402.config.json: { "routes": { "GET /report": { "price": "$0.01" } } }
 * const server = await createX402Server({ configPath: "./x402.config.json" });
 * ```
 */
export async function createX402Server(config: CdpX402ServerConfig): Promise<X402Server> {
  return X402Server.create(config);
}

export type { RoutesConfig, RouteConfig } from "@x402/core/server";
