import { SdpPaymentsError } from "@sdp/payments";
import { RAMP_PROVIDER_CLIENTS } from "@sdp/payments/ramps";
import {
  buildBvnkOnrampPaymentRuleKey,
  buildBvnkPartyDetails,
  bvnkOnboardingRequirements,
  isBvnkWalletActive,
  latestBvnkOfframpBeneficiary,
  normalizeBvnkCurrencyAndNetwork,
  readBvnkOfframpWallet,
  readBvnkOnrampPaymentRuleState,
} from "@sdp/payments/ramps/providers/bvnk/provider-data";
import {
  isLightsparkExternalAccountActive,
  latestLightsparkPayoutAccount,
  readLightsparkCustomerId,
} from "@sdp/payments/ramps/providers/lightspark/provider-data";
import { readMuralOrganization } from "@sdp/payments/ramps/providers/mural/provider-data";
import { readyCounterparty } from "@sdp/payments/ramps/requirements";
import { isSolanaCryptoAsset, SOLANA_ASSET_TO_RAIL } from "@sdp/payments/ramps/shared";
import type { RampRuntimeContext } from "@sdp/payments/ramps/types";
import { parseDecimalAmount } from "@sdp/solana/amount";
import type { PaymentRampEstimate, PaymentRampQuote, RampProviderEstimateResult } from "@sdp/types";
import {
  OFFRAMP_SUPPORT,
  ONRAMP_SUPPORT,
  RAMP_PROVIDER_SUPPORT_DETAILS,
  RAMP_SUPPORT_HASH,
  type RampFiatCurrency,
} from "@sdp/types/generated/ramp-support";
import type {
  OfframpPairSupport,
  OnrampPairSupport,
  RampProviderDirectionSupport,
} from "@sdp/types/payment-rails";
import type { RampProviderId } from "@sdp/types/provider-access";
import type { CounterpartyRequirements } from "@sdp/types/ramp-requirements";
import { z } from "zod";
import { getDb } from "@/db";
import type { CounterpartyRow } from "@/db/repositories/counterparty.repository";
import type {
  PaymentTransferRow,
  PaymentTransferStatus,
} from "@/db/repositories/payments.repository";
import { requireProjectId } from "@/lib/auth";
import { getClientIp } from "@/lib/client-ip";
import {
  AppError,
  badRequest,
  badRequestQuery,
  conflict,
  counterpartyNotProvisioned,
  internalError,
  notFound,
  unsupportedRampCorridor,
} from "@/lib/errors";
import { success } from "@/lib/response";
import { getRequestTenantScope } from "@/lib/tenant-scope";
import { getCounterpartiesRepository } from "@/routes/counterparties/context";
import {
  approvedWalletOperationAttemptId,
  approvedWalletOperationId,
  beginApprovedWalletOperationEffect,
  walletOperationExecutionRequest,
} from "@/services/policy/approved-operation-replay";
import {
  enforceWalletOperationPolicy,
  walletOperationActorFromAuth,
} from "@/services/policy/enforcement.service";
import { assertProviderAvailable } from "@/services/provider-availability.service";
import {
  type AppContext,
  getPaymentsRepository,
  rampRuntime,
  resolveSdpEnvironment,
} from "../context";
import { mapTransferRow } from "../mappers";
import {
  cancelRampTransferSchema,
  createOfframpQuoteSchema,
  createOnrampQuoteSchema,
  estimateOfframpSchema,
  estimateOnrampSchema,
  listOfframpCurrenciesQuerySchema,
  listOnrampCurrenciesQuerySchema,
  simulateSandboxTransferSchema,
  type submitCounterpartyRequirementsSchema,
} from "../schemas";
import { type ResolvedScope, resolveScope, resolveWalletAddress } from "../wallets";
import {
  bvnkOnrampQuote,
  completePendingBvnkOfframpTransfer,
  createPendingBvnkOfframpTransfer,
  ensureBvnkCustomer,
  ensureBvnkOfframpBeneficiary,
  ensureBvnkOfframpWallet,
  ensureBvnkPaymentRule,
} from "./ramps/bvnk";
import { ensureLightsparkCustomer, ensureLightsparkPayoutAccount } from "./ramps/lightspark";
import {
  muralOnrampQuote,
  resolveMuralOnrampAccount,
  resolveMuralRequirements,
} from "./ramps/mural";
import { stripeOnrampQuote } from "./ramps/stripe";

type OnrampCurrencyPair = {
  source: (typeof ONRAMP_SUPPORT)[number]["source"];
  dest: (typeof ONRAMP_SUPPORT)[number]["dest"];
  providers: RampProviderId[];
};

type OfframpCurrencyPair = {
  source: (typeof OFFRAMP_SUPPORT)[number]["source"];
  dest: (typeof OFFRAMP_SUPPORT)[number]["dest"];
  providers: RampProviderId[];
};

type SubmitCounterpartyRequirementsInput = z.infer<typeof submitCounterpartyRequirementsSchema>;

function filterProviders(
  providers: readonly RampProviderId[],
  provider?: RampProviderId
): RampProviderId[] {
  if (provider) {
    return providers.includes(provider) ? [provider] : [];
  }
  return [...providers];
}

function uniqueSorted<T extends string>(values: readonly T[]): T[] {
  return [...new Set(values)].sort();
}

function buildProviderDetails(
  providerIds: readonly RampProviderId[],
  direction: "onramp" | "offramp"
): Partial<Record<RampProviderId, RampProviderDirectionSupport>> {
  const providerDetails: Partial<Record<RampProviderId, RampProviderDirectionSupport>> = {};
  for (const providerId of providerIds) {
    providerDetails[providerId] = RAMP_PROVIDER_SUPPORT_DETAILS[providerId][direction];
  }
  return providerDetails;
}

function providersFromPairs(
  pairs: readonly { providers: readonly RampProviderId[] }[]
): RampProviderId[] {
  return uniqueSorted(pairs.flatMap((row) => row.providers));
}

/** Throws unless the org has the ramp provider enabled for the request's environment. */
export async function assertRampProviderAvailable(
  c: AppContext,
  providerId: RampProviderId,
  organizationId: string
): Promise<void> {
  await assertProviderAvailable(
    c.env,
    getDb(c.env),
    organizationId,
    "ramps",
    providerId,
    resolveSdpEnvironment(c) === "sandbox"
  );
}

type RampQuoteDirection = "onramp" | "offramp";

/**
 * Throws unless the committed corridor-support matrix (the same tables estimate
 * selects providers from) lists the provider for the requested crypto/fiat pair.
 * When fiatCurrency is omitted (off-ramp quotes may defer fiat selection to the
 * provider), the provider must support the crypto rail for at least one fiat.
 */
function assertRampCorridorSupported(
  direction: RampQuoteDirection,
  input: { provider: RampProviderId; cryptoToken: string; fiatCurrency?: RampFiatCurrency }
): void {
  const symbol = input.cryptoToken.trim().toUpperCase();
  if (!isSolanaCryptoAsset(symbol)) {
    throw badRequest(
      `cryptoToken must be one of: ${Object.keys(SOLANA_ASSET_TO_RAIL).join(", ")}.`
    );
  }
  const rail = SOLANA_ASSET_TO_RAIL[symbol];
  const pairs: readonly (OnrampPairSupport | OfframpPairSupport)[] =
    direction === "onramp" ? ONRAMP_SUPPORT : OFFRAMP_SUPPORT;
  const fiat = input.fiatCurrency;
  const matched = pairs.filter((pair) => {
    const railSide = direction === "onramp" ? pair.dest : pair.source;
    const fiatSide = direction === "onramp" ? pair.source : pair.dest;
    return railSide === rail && (fiat === undefined || fiatSide === fiat);
  });
  const supportedProviders = providersFromPairs(matched);
  if (!supportedProviders.includes(input.provider)) {
    throw unsupportedRampCorridor(input.provider, direction, {
      assetRail: rail,
      fiatCurrency: fiat,
      supportedProviders,
    });
  }
}
type ScopedRampWallet = ResolvedScope["wallets"][number];

type RampPolicyOperationType = "ramp_onramp_quote" | "ramp_offramp_quote";

interface PersistRampQuoteTransferInput {
  scope: ResolvedScope;
  projectId: string;
  counterparty: CounterpartyRow;
  quote: PaymentRampQuote;
  direction: RampQuoteDirection;
  wallet: ScopedRampWallet;
  walletAddress: string;
  cryptoToken: string;
  cryptoAmount: string | null;
  fiatCurrency: RampFiatCurrency | null;
  fiatAmount: string | null;
  rampsMemo: Record<string, string> | undefined;
  providerData?: Record<string, unknown>;
}

function requireRampTransferWallet(
  scope: ResolvedScope,
  walletIdOrAddress: string,
  walletAddress: string,
  fieldName: string
): ScopedRampWallet {
  const wallet = scope.wallets.find(
    (entry) => entry.walletId === walletIdOrAddress || entry.publicKey === walletAddress
  );
  if (!wallet) {
    throw badRequest(`${fieldName} must reference an SDP wallet.`);
  }
  return wallet;
}

async function enforceRampWalletOperationPolicy(
  c: AppContext,
  input: {
    scope: ResolvedScope;
    wallet: ScopedRampWallet;
    operationType: RampPolicyOperationType;
    provider: RampProviderId;
    counterpartyId: string;
    asset: string;
    amount?: string | null;
    destination?: string | null;
    rawPayload?: Record<string, unknown>;
  }
) {
  return enforceWalletOperationPolicy(
    c.env,
    getRequestTenantScope(c),
    {
      organizationId: input.scope.auth.organizationId,
      projectId: input.scope.auth.projectId,
      custodyWalletId: input.wallet.id,
      walletId: input.wallet.walletId,
      apiKeyId: input.scope.auth.apiKeyId,
      actor: walletOperationActorFromAuth(input.scope.auth),
      operationFamily: "ramp",
      operationType: input.operationType,
      asset: input.asset,
      amount: input.amount ?? null,
      destination: input.destination ?? null,
      providerExtensions: { provider: input.provider },
      rawPayload: {
        provider: input.provider,
        counterpartyId: input.counterpartyId,
        ...(input.rawPayload ?? {}),
      },
    },
    approvedWalletOperationId(c),
    approvedWalletOperationAttemptId(c)
  );
}

function rampQuoteTransferStatus(quote: PaymentRampQuote): PaymentTransferStatus {
  if (quote.deliveryMode === "manual_instructions" && quote.status === "pending") {
    return "awaiting_payment";
  }
  return quote.status;
}

async function persistRampQuoteTransfer(
  c: AppContext,
  input: PersistRampQuoteTransferInput
): Promise<void> {
  const repository = getPaymentsRepository(c);
  const existing = await repository.getTransferByProviderReference({
    provider: input.quote.provider,
    providerReference: input.quote.id,
    organizationId: input.scope.auth.organizationId,
    projectId: input.projectId,
  });
  if (existing) {
    return;
  }

  const apiKey = c.get("apiKey");
  const isOnramp = input.direction === "onramp";
  const created = await repository.createTransfer({
    organizationId: input.scope.auth.organizationId,
    projectId: input.projectId,
    walletId: input.wallet.walletId,
    counterpartyId: input.counterparty.id,
    sourceAddress: isOnramp ? null : input.walletAddress,
    destinationAddress: isOnramp ? input.walletAddress : null,
    token: input.cryptoToken,
    amount: input.cryptoAmount,
    memo: null,
    type: input.direction,
    direction: isOnramp ? "inbound" : "outbound",
    status: rampQuoteTransferStatus(input.quote),
    provider: input.quote.provider,
    providerReference: input.quote.id,
    deliveryMode: input.quote.deliveryMode,
    fiatCurrency: input.fiatCurrency,
    fiatAmount: input.fiatAmount,
    rampsMemo: input.rampsMemo,
    providerData: input.providerData ?? {},
    serializedTx: null,
    signature: null,
    slot: null,
    initiatedByKeyId: apiKey ? apiKey.id : null,
  });

  if (!created) {
    throw new AppError("INTERNAL_ERROR", "Failed to create ramp transfer record");
  }
}

export async function advanceCounterpartyRequirements(
  c: AppContext,
  input: SubmitCounterpartyRequirementsInput & { counterparty: CounterpartyRow; projectId: string }
): Promise<CounterpartyRequirements> {
  switch (input.provider) {
    case "moonpay":
      return readyCounterparty("moonpay", input.direction);
    case "moneygram":
      return readyCounterparty("moneygram", input.direction);
    case "lightspark": {
      const customer = await ensureLightsparkCustomer(c, {
        counterparty: input.counterparty,
        projectId: input.projectId,
        collectedData: input.collectedData,
      });
      if (input.direction === "offramp") {
        await ensureLightsparkPayoutAccount(c, {
          counterparty: input.counterparty,
          projectId: input.projectId,
          customer,
          fiatCurrency: input.fiatCurrency,
          collectedData: input.collectedData,
        });
      }
      return readyCounterparty("lightspark", input.direction);
    }
    case "bvnk": {
      if (input.direction === "offramp") {
        await ensureBvnkOfframpBeneficiary(c, {
          counterparty: input.counterparty,
          projectId: input.projectId,
          fiatCurrency: input.fiatCurrency,
          collectedData: input.collectedData,
        });
        const refreshed = await getCounterpartiesRepository(c).getCounterpartyById({
          counterpartyId: input.counterparty.id,
          organizationId: input.counterparty.organization_id,
          projectId: input.projectId,
        });
        if (!refreshed) throw notFound("Counterparty");
        const wallet = await ensureBvnkOfframpWallet(
          c,
          rampRuntime(c),
          refreshed,
          input.projectId,
          input.fiatCurrency
        );
        if (!isBvnkWalletActive(wallet.status)) {
          return {
            provider: "bvnk",
            direction: input.direction,
            status: "funding_account_provisioning",
          };
        }
        return readyCounterparty("bvnk", input.direction);
      }
      const customer = await ensureBvnkCustomer(c, input.counterparty, input.projectId, {
        fiatCurrency: input.fiatCurrency,
        collectedData: input.collectedData,
      });
      const scope = await resolveScope(c);
      const destinationWalletAddress = resolveWalletAddress(
        scope.wallets,
        input.destinationWallet,
        "destinationWallet",
        scope.auth
      );
      const { currency, network } = normalizeBvnkCurrencyAndNetwork(input.cryptoToken);
      const resolution = await ensureBvnkPaymentRule(
        c,
        rampRuntime(c),
        input.counterparty,
        input.projectId,
        customer,
        { currency, network, destinationWalletAddress, fiatCurrency: input.fiatCurrency }
      );
      return bvnkOnboardingRequirements(resolution, input.direction);
    }
    case "mural":
      return resolveMuralRequirements(c, input.counterparty, input.projectId, input.direction);
    case "coinbase":
      return readyCounterparty("coinbase", input.direction);
    case "stripe":
      return readyCounterparty("stripe", input.direction);
    default: {
      const _exhaustive: never = input;
      throw internalError(`Unhandled ramp provider: ${_exhaustive}`);
    }
  }
}

async function estimateAcrossProviders(
  c: AppContext,
  providers: readonly RampProviderId[],
  runProvider: (provider: RampProviderId, ctx: RampRuntimeContext) => Promise<PaymentRampEstimate>
): Promise<RampProviderEstimateResult[]> {
  const scope = await resolveScope(c);
  const ctx = rampRuntime(c);

  return Promise.all(
    providers.map(async (provider): Promise<RampProviderEstimateResult> => {
      try {
        await assertRampProviderAvailable(c, provider, scope.auth.organizationId);
        const estimate = await runProvider(provider, ctx);
        return { provider, status: "ok", estimate };
      } catch (error) {
        if (error instanceof SdpPaymentsError && error.code === "ESTIMATE_NOT_AVAILABLE") {
          return { provider, status: "unsupported" };
        }
        return {
          provider,
          status: "error",
          error: error instanceof Error ? error.message : String(error),
        };
      }
    })
  );
}

export async function estimateOnramp(c: AppContext) {
  const body = await c.req.json();
  const parsed = estimateOnrampSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const input = parsed.data;
  const row = ONRAMP_SUPPORT.find(
    (pair) => pair.source === input.fiatCurrency && pair.dest === input.assetRail
  );
  const providers = row ? row.providers : [];

  const estimates = await estimateAcrossProviders(c, providers, (provider, ctx) =>
    RAMP_PROVIDER_CLIENTS[provider].estimateOnramp(ctx, {
      assetRail: input.assetRail,
      fiatCurrency: input.fiatCurrency,
      fiatAmount: input.fiatAmount,
    })
  );

  return success(c, { estimates });
}

export async function estimateOfframp(c: AppContext) {
  const body = await c.req.json();
  const parsed = estimateOfframpSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const input = parsed.data;
  const row = OFFRAMP_SUPPORT.find(
    (pair) => pair.source === input.assetRail && pair.dest === input.fiatCurrency
  );
  const providers = row ? row.providers : [];

  const estimates = await estimateAcrossProviders(c, providers, (provider, ctx) =>
    RAMP_PROVIDER_CLIENTS[provider].estimateOfframp(ctx, {
      assetRail: input.assetRail,
      fiatCurrency: input.fiatCurrency,
      cryptoAmount: input.cryptoAmount,
    })
  );

  return success(c, { estimates });
}

export async function createOnrampQuote(c: AppContext): Promise<Response> {
  const body = await c.req.json();
  const parsed = createOnrampQuoteSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const input = parsed.data;
  assertRampCorridorSupported("onramp", input);
  const scope = await resolveScope(c);
  await assertRampProviderAvailable(c, input.provider, scope.auth.organizationId);

  const projectId = requireProjectId(c);
  const repo = getCounterpartiesRepository(c);
  const counterparty = await repo.getCounterpartyById({
    counterpartyId: input.counterpartyId,
    organizationId: scope.auth.organizationId,
    projectId,
  });
  if (!counterparty) {
    throw new AppError("NOT_FOUND", "Counterparty not found");
  }

  const destinationWalletAddress = resolveWalletAddress(
    scope.wallets,
    input.destinationWallet,
    "destinationWallet",
    scope.auth,
    ["payments:write"]
  );
  const destinationWallet = requireRampTransferWallet(
    scope,
    input.destinationWallet,
    destinationWalletAddress,
    "destinationWallet"
  );
  await enforceRampWalletOperationPolicy(c, {
    scope,
    wallet: destinationWallet,
    operationType: "ramp_onramp_quote",
    provider: input.provider,
    counterpartyId: input.counterpartyId,
    asset: input.cryptoToken,
    amount: input.fiatAmount,
    destination: destinationWalletAddress,
    rawPayload: {
      fiatCurrency: input.fiatCurrency,
      fiatAmount: input.fiatAmount,
      cryptoToken: input.cryptoToken,
      executionRequest: walletOperationExecutionRequest(c, input),
    },
  });

  await beginApprovedWalletOperationEffect(c);

  let quote: PaymentRampQuote;
  let transferProviderData: Record<string, unknown> | undefined;
  switch (input.provider) {
    case "moonpay": {
      quote = await RAMP_PROVIDER_CLIENTS.moonpay.createOnrampQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        fiatAmount: input.fiatAmount,
        destinationWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
        redirectUrl: input.redirectUrl,
      });
      break;
    }
    case "lightspark": {
      const customerId = readLightsparkCustomerId(counterparty.provider_data);
      if (!customerId) {
        throw counterpartyNotProvisioned("lightspark", "onramp");
      }
      quote = await RAMP_PROVIDER_CLIENTS.lightspark.createOnrampQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        fiatAmount: input.fiatAmount,
        destinationWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
        customerId,
        redirectUrl: input.redirectUrl,
      });
      break;
    }
    case "bvnk": {
      const { currency, network } = normalizeBvnkCurrencyAndNetwork(input.cryptoToken);
      const bvnkResult = await bvnkOnrampQuote(c, {
        counterparty,
        paymentRule: {
          currency,
          network,
          fiatCurrency: input.fiatCurrency,
          destinationWalletAddress,
        },
      });
      quote = bvnkResult.quote;
      transferProviderData = bvnkResult.transferProviderData;
      break;
    }
    case "mural": {
      const account = await resolveMuralOnrampAccount(
        c,
        readMuralOrganization(counterparty.provider_data)
      );
      if (!account) {
        throw counterpartyNotProvisioned("mural", "onramp");
      }
      quote = muralOnrampQuote({ account, fiatCurrency: input.fiatCurrency });
      transferProviderData = { mural: { accountId: account.id } };
      break;
    }
    case "moneygram": {
      quote = await RAMP_PROVIDER_CLIENTS.moneygram.createOnrampQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        fiatAmount: input.fiatAmount,
        destinationWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
      });
      break;
    }
    case "coinbase": {
      quote = await RAMP_PROVIDER_CLIENTS.coinbase.createOnrampQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        fiatAmount: input.fiatAmount,
        destinationWalletAddress,
        externalCustomerId: counterparty.id,
        email: counterparty.email,
        phone: counterparty.entity_type === "individual" ? counterparty.identity.phone : undefined,
        domain: input.domain,
      });
      break;
    }
    case "stripe": {
      quote = await stripeOnrampQuote(c, {
        counterparty,
        destinationWalletAddress,
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        fiatAmount: input.fiatAmount,
        customerIpAddress: getClientIp(c) ?? undefined,
      });
      break;
    }
    default: {
      const exhaustive: never = input.provider;
      throw new AppError(
        "INTERNAL_ERROR",
        `On-ramp quotes are not implemented for provider: ${String(exhaustive)}`
      );
    }
  }

  await persistRampQuoteTransfer(c, {
    scope,
    projectId,
    counterparty,
    quote,
    direction: "onramp",
    wallet: destinationWallet,
    walletAddress: destinationWalletAddress,
    cryptoToken: input.cryptoToken,
    cryptoAmount: null,
    fiatCurrency: input.fiatCurrency ? input.fiatCurrency : null,
    fiatAmount: input.fiatAmount,
    rampsMemo: input.rampsMemo,
    providerData: transferProviderData,
  });

  return success(c, { quote });
}

export async function createOfframpQuote(c: AppContext): Promise<Response> {
  const body = await c.req.json();
  const parsed = createOfframpQuoteSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const input = parsed.data;
  assertRampCorridorSupported("offramp", input);
  const scope = await resolveScope(c);
  await assertRampProviderAvailable(c, input.provider, scope.auth.organizationId);

  const projectId = requireProjectId(c);
  const repo = getCounterpartiesRepository(c);
  const counterparty = await repo.getCounterpartyById({
    counterpartyId: input.counterpartyId,
    organizationId: scope.auth.organizationId,
    projectId,
  });
  if (!counterparty) {
    throw new AppError("NOT_FOUND", "Counterparty not found");
  }

  const sourceWalletAddress = resolveWalletAddress(
    scope.wallets,
    input.sourceWallet,
    "sourceWallet",
    scope.auth,
    ["payments:write"]
  );
  const sourceWallet = requireRampTransferWallet(
    scope,
    input.sourceWallet,
    sourceWalletAddress,
    "sourceWallet"
  );
  await enforceRampWalletOperationPolicy(c, {
    scope,
    wallet: sourceWallet,
    operationType: "ramp_offramp_quote",
    provider: input.provider,
    counterpartyId: input.counterpartyId,
    asset: input.cryptoToken,
    amount: input.cryptoAmount,
    rawPayload: {
      fiatCurrency: input.fiatCurrency,
      cryptoToken: input.cryptoToken,
      cryptoAmount: input.cryptoAmount,
      executionRequest: walletOperationExecutionRequest(c, input),
    },
  });

  await beginApprovedWalletOperationEffect(c);

  let quote: PaymentRampQuote;
  let pendingTransfer: PaymentTransferRow | undefined;
  switch (input.provider) {
    case "moonpay": {
      quote = await RAMP_PROVIDER_CLIENTS.moonpay.createOfframpQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        cryptoAmount: input.cryptoAmount,
        sourceWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
        redirectUrl: input.redirectUrl,
      });
      break;
    }
    case "lightspark": {
      if (!input.fiatCurrency) {
        throw badRequest("fiatCurrency is required for Lightspark off-ramp.");
      }
      const customerId = readLightsparkCustomerId(counterparty.provider_data);
      const payoutAccount = latestLightsparkPayoutAccount(
        counterparty.provider_data,
        input.fiatCurrency
      );
      if (
        !customerId ||
        !payoutAccount ||
        !isLightsparkExternalAccountActive(payoutAccount.status)
      ) {
        throw counterpartyNotProvisioned("lightspark", "offramp");
      }
      quote = await RAMP_PROVIDER_CLIENTS.lightspark.createOfframpQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        cryptoAmount: input.cryptoAmount,
        sourceWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
        customerId,
        payoutAccountId: payoutAccount.accountId,
      });
      break;
    }
    case "bvnk": {
      if (!input.fiatCurrency) {
        throw badRequest("fiatCurrency is required for BVNK off-ramp.");
      }
      const beneficiary = latestBvnkOfframpBeneficiary(
        counterparty.provider_data,
        input.fiatCurrency
      );
      const wallet = readBvnkOfframpWallet(counterparty.provider_data, input.fiatCurrency);
      if (!beneficiary || !wallet || !isBvnkWalletActive(wallet.status)) {
        throw counterpartyNotProvisioned("bvnk", "offramp");
      }
      pendingTransfer = await createPendingBvnkOfframpTransfer(c, {
        organizationId: scope.auth.organizationId,
        projectId,
        counterpartyId: counterparty.id,
        walletId: sourceWallet.walletId,
        walletAddress: sourceWalletAddress,
        cryptoToken: input.cryptoToken,
        cryptoAmount: input.cryptoAmount,
        fiatCurrency: input.fiatCurrency,
        rampsMemo: input.rampsMemo,
      });
      try {
        quote = await RAMP_PROVIDER_CLIENTS.bvnk.createOfframpQuote(rampRuntime(c), {
          cryptoToken: input.cryptoToken,
          fiatCurrency: input.fiatCurrency,
          cryptoAmount: input.cryptoAmount,
          sourceWalletAddress,
          paymentTransferId: pendingTransfer.id,
          externalCustomerId: counterparty.external_id ?? counterparty.id,
          bvnkCompliance: buildBvnkPartyDetails(counterparty, "ORIGINATOR"),
          bvnkOfframpWalletId: wallet.id,
        });
      } catch (error) {
        await getPaymentsRepository(c).updateTransfer({
          transferId: pendingTransfer.id,
          organizationId: scope.auth.organizationId,
          projectId,
          status: "failed",
          error: error instanceof Error ? error.message : String(error),
          updatedAt: new Date().toISOString(),
        });
        throw error;
      }
      break;
    }
    case "moneygram": {
      quote = await RAMP_PROVIDER_CLIENTS.moneygram.createOfframpQuote(rampRuntime(c), {
        cryptoToken: input.cryptoToken,
        fiatCurrency: input.fiatCurrency,
        cryptoAmount: input.cryptoAmount,
        sourceWalletAddress,
        externalCustomerId: counterparty.external_id ?? counterparty.id,
      });
      break;
    }
    case "mural":
      throw internalError("Mural off-ramp quote is not implemented yet.");
    case "coinbase":
      throw badRequest("Coinbase Onramp does not support off-ramp.");
    case "stripe":
      throw badRequest("Stripe off-ramp is not supported.");
    default: {
      const exhaustive: never = input.provider;
      throw new AppError(
        "INTERNAL_ERROR",
        `Off-ramp quotes are not implemented for provider: ${String(exhaustive)}`
      );
    }
  }

  if (pendingTransfer) {
    await completePendingBvnkOfframpTransfer(c, {
      organizationId: scope.auth.organizationId,
      projectId,
      transferId: pendingTransfer.id,
      quote,
      status: rampQuoteTransferStatus(quote),
    });
  } else {
    await persistRampQuoteTransfer(c, {
      scope,
      projectId,
      counterparty,
      quote,
      direction: "offramp",
      wallet: sourceWallet,
      walletAddress: sourceWalletAddress,
      cryptoToken: input.cryptoToken,
      cryptoAmount: input.cryptoAmount,
      fiatCurrency: input.fiatCurrency ? input.fiatCurrency : null,
      fiatAmount: null,
      rampsMemo: input.rampsMemo,
    });
  }

  return success(c, { quote });
}

export async function cancelRampTransfer(c: AppContext) {
  const body = await c.req.json();
  const parsed = cancelRampTransferSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const input = parsed.data;
  const scope = await resolveScope(c);
  const projectId = requireProjectId(c);
  const repository = getPaymentsRepository(c);

  const transfer = await repository.getTransferByProviderReference({
    provider: input.provider,
    providerReference: input.providerReference,
    organizationId: scope.auth.organizationId,
    projectId,
  });
  if (!transfer) {
    throw notFound("Transfer");
  }
  const cancelableStatuses: readonly PaymentTransferStatus[] = ["pending", "awaiting_payment"];
  if (!cancelableStatuses.includes(transfer.status)) {
    throw badRequest(`Transfer can no longer be canceled (status: ${transfer.status}).`);
  }

  const updated = await repository.updateTransferStatusGuarded({
    transferId: transfer.id,
    organizationId: scope.auth.organizationId,
    projectId,
    fromStatuses: cancelableStatuses,
    toStatus: "canceled",
    updatedAt: new Date().toISOString(),
  });
  if (!updated) {
    throw conflict("Transfer status changed before it could be canceled.");
  }

  return success(c, { transfer: mapTransferRow(updated) });
}

export async function listOnrampCurrencies(c: AppContext) {
  const parsed = listOnrampCurrenciesQuerySchema.safeParse(c.req.query());

  if (!parsed.success) {
    throw badRequestQuery({
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const { source, dest, provider } = parsed.data;
  const pairs: OnrampCurrencyPair[] = ONRAMP_SUPPORT.flatMap((row) => {
    if (source && row.source !== source) return [];
    if (dest && row.dest !== dest) return [];
    const providers = filterProviders(row.providers, provider);
    if (providers.length === 0) return [];
    return [{ source: row.source, dest: row.dest, providers }];
  });

  return success(c, {
    currencies: {
      sources: uniqueSorted(pairs.map((row) => row.source)),
      destinations: uniqueSorted(pairs.map((row) => row.dest)),
    },
    pairs,
    providerDetails: buildProviderDetails(providersFromPairs(pairs), "onramp"),
    supportHash: RAMP_SUPPORT_HASH,
  });
}

export async function listOfframpCurrencies(c: AppContext) {
  const parsed = listOfframpCurrenciesQuerySchema.safeParse(c.req.query());

  if (!parsed.success) {
    throw badRequestQuery({
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const { source, dest, provider } = parsed.data;
  const pairs: OfframpCurrencyPair[] = OFFRAMP_SUPPORT.flatMap((row) => {
    if (source && row.source !== source) return [];
    if (dest && row.dest !== dest) return [];
    const providers = filterProviders(row.providers, provider);
    if (providers.length === 0) return [];
    return [{ source: row.source, dest: row.dest, providers }];
  });

  return success(c, {
    currencies: {
      sources: uniqueSorted(pairs.map((row) => row.source)),
      destinations: uniqueSorted(pairs.map((row) => row.dest)),
    },
    pairs,
    providerDetails: buildProviderDetails(providersFromPairs(pairs), "offramp"),
    supportHash: RAMP_SUPPORT_HASH,
  });
}

export async function simulateSandboxTransfer(c: AppContext) {
  if (resolveSdpEnvironment(c) !== "sandbox") {
    throw new AppError(
      "FORBIDDEN",
      "Sandbox transfer simulation is only available in sandbox mode"
    );
  }

  const body = await c.req.json();
  const parsed = simulateSandboxTransferSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  let transaction: unknown;
  switch (parsed.data.provider) {
    case "lightspark":
      transaction = await RAMP_PROVIDER_CLIENTS.lightspark.sandboxSend(
        rampRuntime(c),
        parsed.data.payload
      );
      break;
    case "bvnk": {
      const payload = parsed.data.payload;
      const scope = await resolveScope(c);
      const projectId = requireProjectId(c);
      const counterparty = await getCounterpartiesRepository(c).getCounterpartyById({
        counterpartyId: payload.counterpartyId,
        organizationId: scope.auth.organizationId,
        projectId,
      });
      if (!counterparty) {
        throw new AppError("NOT_FOUND", "Counterparty not found");
      }
      const destinationWalletAddress = resolveWalletAddress(
        scope.wallets,
        payload.destinationWallet,
        "destinationWallet",
        scope.auth,
        ["payments:write"]
      );
      const { currency, network } = normalizeBvnkCurrencyAndNetwork(payload.cryptoToken);
      const key = buildBvnkOnrampPaymentRuleKey(
        payload.fiatCurrency,
        currency,
        network,
        destinationWalletAddress
      );
      const entry = readBvnkOnrampPaymentRuleState(counterparty.provider_data, key);
      if (!entry.walletId) {
        throw new AppError(
          "BAD_REQUEST",
          "BVNK funding wallet is not provisioned yet for this destination."
        );
      }
      if (!isBvnkWalletActive(entry.walletStatus)) {
        throw new AppError(
          "BAD_REQUEST",
          "BVNK funding wallet is not active for this destination."
        );
      }
      transaction = await RAMP_PROVIDER_CLIENTS.bvnk.simulatePayin(rampRuntime(c), {
        walletId: entry.walletId,
        amount: payload.amount,
        currency: payload.fiatCurrency,
        originatorName: counterparty.display_name,
        remittanceInformation: entry.bankAccount?.paymentReference,
      });
      break;
    }
    case "mural": {
      const payload = parsed.data.payload;
      const scope = await resolveScope(c);
      const projectId = requireProjectId(c);
      const counterparty = await getCounterpartiesRepository(c).getCounterpartyById({
        counterpartyId: payload.counterpartyId,
        organizationId: scope.auth.organizationId,
        projectId,
      });
      if (!counterparty) {
        throw new AppError("NOT_FOUND", "Counterparty not found");
      }
      const org = readMuralOrganization(counterparty.provider_data);
      if (!org.id) {
        throw badRequest("Mural organization is not provisioned yet for this counterparty.");
      }
      const account = await resolveMuralOnrampAccount(c, org);
      if (!account) {
        throw badRequest("Mural account is not active yet for this counterparty.");
      }
      const rail = {
        USD: "wire",
        MXN: "spei",
        BRL: "pix",
        ARS: "cvu",
      } as const satisfies Record<typeof payload.fiatCurrency, "wire" | "spei" | "pix" | "cvu">;
      transaction = await RAMP_PROVIDER_CLIENTS.mural.simulatePayin(rampRuntime(c), {
        organizationId: org.id,
        destinationAccountId: account.id,
        rail: rail[payload.fiatCurrency],
        amountValue: String(parseDecimalAmount(String(payload.amount), 2)),
        currencySymbol: payload.fiatCurrency,
      });
      break;
    }
  }

  return success(c, { transaction });
}
