import type {
  BvnkBankFundingDetails,
  BvnkOnboardingStatus,
  BvnkPaymentRampInstruction,
  CounterpartyEntityType,
  SdpEnvironment,
} from "@sdp/types";
import type { RampFiatCurrency } from "@sdp/types/generated/ramp-support";
import { RAMP_FIAT_CURRENCIES } from "@sdp/types/generated/ramp-support";
import type { CryptoAssetSymbol } from "@sdp/types/payment-rails";
import type { CounterpartyRequirements, RampDirection } from "@sdp/types/ramp-requirements";
import { z } from "zod";
import type { CounterpartyRow } from "../../../counterparty";
import { badRequest, internalError } from "../../../errors";
import { hashString } from "../../../hash";
import { readRecord } from "../../../json";
import { readyCounterparty } from "../../requirements";

export interface BvnkRuleEntityAddress {
  addressLine1: string;
  addressLine2?: string;
  postalCode?: string;
  city: string;
  countryCode: string;
  /** ISO 3166-1 alpha-2 country; BVNK rule validation rejects a blank `country`. */
  country: string;
  /** ISO 3166-2 region/state code; BVNK requires it for US beneficiaries. */
  stateCode?: string;
}

export type BvnkEntityType = "INDIVIDUAL" | "COMPANY";

const BVNK_ENTITY_TYPE = {
  individual: "INDIVIDUAL",
  business: "COMPANY",
} as const satisfies Record<CounterpartyEntityType, BvnkEntityType>;

/**
 * Beneficiary entity for a BVNK on-ramp payment rule. The handler builds this
 * from the counterparty identity; the provider only serializes it.
 */
export interface BvnkRuleEntity {
  type: BvnkEntityType;
  customerIdentifier: string;
  relationshipType: "SELF_OWNED" | "THIRD_PARTY";
  firstName?: string;
  lastName?: string;
  dateOfBirth?: string;
  legalName?: string;
  registrationNumber?: string;
  address?: BvnkRuleEntityAddress;
}

export interface BvnkComplianceInput {
  partyDetails?: Record<string, unknown>[];
}

export const BVNK_NETWORKS = ["SOLANA"] as const;

export type BvnkNetwork = (typeof BVNK_NETWORKS)[number];

export const BVNK_CRYPTO_CURRENCIES = [
  "SOL",
  "USDC",
  "USDT",
] as const satisfies readonly CryptoAssetSymbol[];

export type BvnkCryptoCurrency = (typeof BVNK_CRYPTO_CURRENCIES)[number];

const BVNK_SOLANA_NETWORK_ALIASES = new Set(["sol", "solana"]);
const BVNK_CRYPTO_CURRENCY_SET = new Set<string>(BVNK_CRYPTO_CURRENCIES);

interface BvnkCurrencyNetwork {
  currency: BvnkCryptoCurrency;
  network: BvnkNetwork;
}

export function normalizeBvnkCurrencyAndNetwork(value: string): BvnkCurrencyNetwork {
  const normalized = value.trim().toUpperCase();
  if (!/^[A-Z0-9_]+$/.test(normalized)) {
    throw badRequest("cryptoToken must be a valid BVNK currency code");
  }

  const tokenParts = normalized.split("_").filter((part) => part.length > 0);
  const currency = tokenParts[0];
  if (!currency) {
    throw badRequest("cryptoToken must include a BVNK currency code");
  }

  const networkHint = tokenParts.length > 1 ? tokenParts[tokenParts.length - 1]?.toLowerCase() : "";
  if (networkHint && BVNK_SOLANA_NETWORK_ALIASES.has(networkHint)) {
    if (BVNK_CRYPTO_CURRENCY_SET.has(currency)) {
      return { currency: currency as BvnkCryptoCurrency, network: "SOLANA" };
    }
  }
  if (BVNK_CRYPTO_CURRENCY_SET.has(currency)) {
    return { currency: currency as BvnkCryptoCurrency, network: "SOLANA" };
  }

  throw badRequest(
    `Unsupported BVNK cryptoToken '${value}'. SDP BVNK ramps only support Solana assets (for example: SOL, USDC_SOLANA).`
  );
}

export type BvnkVerificationStatus = "init" | "pending" | "completed" | "failed";

/**
 * Builds BVNK's `Idempotency-Key` header for fiat wallet creation.
 *
 * SDP's BVNK wallet `name` is the canonical wallet identity. BVNK caps
 * idempotency keys at 36 characters, so SDP hashes the full wallet name and
 * trims the digest to the provider limit instead of sending the long readable
 * name as the key.
 *
 * @param walletName BVNK wallet `name` generated by SDP.
 * @returns A stable 36-character idempotency key for the wallet name.
 */
export async function buildBvnkWalletIdempotencyKey(walletName: string): Promise<string> {
  return (await hashString(walletName)).slice(0, 36);
}

const BVNK_VERIFIED_STATUSES = new Set(["VERIFIED", "COMPLETED", "APPROVED"]);
const BVNK_VERIFYING_STATUSES = new Set(["PENDING"]);
const BVNK_VERIFICATION_REQUIRED_STATUSES = new Set(["ACTIONS_REQUIRED", "INFO_REQUIRED"]);
const BVNK_VERIFICATION_FAILED_STATUSES = new Set(["REJECTED"]);

/**
 * Whether a cached BVNK customer status counts as fully verified. The customer
 * KYC enum's success state is VERIFIED, but webhook events also report terminal
 * success as COMPLETED/APPROVED — treat all as verified.
 */
export function isBvnkCustomerVerified(status: string | undefined): boolean {
  return status !== undefined && BVNK_VERIFIED_STATUSES.has(status.toUpperCase());
}

/**
 * Onboarding phase for a not-yet-verified BVNK customer, decided from the KYC
 * status the customers:status-change webhook delivers — never from the presence
 * of a cached verificationUrl, which is written once and never cleared. PENDING
 * means the applicant has submitted and is under review; INFO_REQUIRED (and the
 * ACTIONS_REQUIRED synonym) mean the applicant must still act, so we surface the
 * Sumsub URL; REJECTED is terminal-negative. Any other unverified status is
 * unmapped and throws so it surfaces loudly instead of silently stranding the
 * buyer mid-onboarding.
 */
export function bvnkUnverifiedOnboardingStatus(
  status: string | undefined
): Extract<BvnkOnboardingStatus, "verifying" | "verification_required" | "verification_failed"> {
  const normalized = status?.toUpperCase();
  if (normalized && BVNK_VERIFYING_STATUSES.has(normalized)) {
    return "verifying";
  }
  if (normalized && BVNK_VERIFICATION_REQUIRED_STATUSES.has(normalized)) {
    return "verification_required";
  }
  if (normalized && BVNK_VERIFICATION_FAILED_STATUSES.has(normalized)) {
    return "verification_failed";
  }
  throw internalError(`Unmapped BVNK customer KYC status: ${status ?? "(missing)"}`);
}

/**
 * Normalizes a stored subdivision code to the bare 2-letter form BVNK's
 * `stateCode` field requires. Strips a leading ISO 3166-2 country prefix
 * (e.g. `US-TX` -> `TX`) only when the prefix matches the address's own
 * country code; throws a typed bad-request error when the result is not
 * exactly 2 characters.
 *
 * TODO(zach, 2026-07-02): temporary translation layer. The counterparty
 * location model will be refactored to store both ISO code variants so
 * provider mappers can read the form they need directly — remove this
 * normalization when that lands.
 */
export function normalizeBvnkStateCode(countryCode: string, subdivisionCode: string): string {
  const prefixMatch = /^([A-Z]{2})-([A-Z0-9]{2,3})$/.exec(subdivisionCode.toUpperCase());
  const candidate =
    prefixMatch && prefixMatch[1] === countryCode.toUpperCase()
      ? prefixMatch[2]
      : subdivisionCode.toUpperCase();
  if (candidate.length !== 2) {
    throw badRequest("BVNK requires a 2-letter state/subdivision code.", {
      field: "identity.address.subdivisionCode",
      value: subdivisionCode,
    });
  }
  return candidate;
}

/**
 * Builds the caller-defined BVNK off-ramp channel reference.
 *
 * The route pre-generates the SDP payment transfer id before creating the BVNK
 * channel, then persists the transfer with that same id after BVNK returns the
 * channel uuid. This keeps BVNK's human/provider-side reference tied to SDP's
 * transaction id.
 *
 * BVNK rejects `reference` values containing colons or other special
 * characters; allowed characters are alphanumeric characters, dashes,
 * underscores, and periods. This deliberately uses underscores instead of the
 * wallet-name convention (`sdp:offramp:...`) so the reference is accepted by
 * BVNK while still carrying SDP's transfer id.
 *
 * @param paymentTransferId SDP payment transfer id, for example `xfr_<uuid>`.
 * @returns BVNK off-ramp reference in `sdp_offramp_<transfer_id>` format.
 */
export function buildBvnkOfframpReference(paymentTransferId: string): string {
  if (!paymentTransferId.trim()) {
    throw internalError("BVNK off-ramp reference requires a payment transfer id.");
  }
  return `sdp_offramp_${paymentTransferId}`;
}

/**
 * Parses BVNK's channel transaction `data.reference` back into the SDP transfer id.
 *
 * @param reference BVNK off-ramp reference in `sdp_offramp_<transfer_id>` format.
 * @returns SDP payment transfer id (for example `xfr_<uuid>`), or undefined for a
 * channel payment SDP did not create — BVNK also delivers webhooks for foreign
 * transactions (sandbox tests, manual payments), which must be acked and ignored
 * rather than rejected.
 */
export function readBvnkOfframpReference(reference: string): string | undefined {
  const prefix = "sdp_offramp_";
  if (!reference.startsWith(prefix)) {
    return undefined;
  }
  const transferId = reference.slice(prefix.length);
  if (!/^xfr_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(transferId)) {
    return undefined;
  }
  return transferId;
}

/** Shared, one-per-counterparty BVNK customer (KYC) state. */
export interface BvnkCustomerResolution {
  /**
   * BVNK customer `externalReference` value. For SDP-created customers this is
   * a reversible `cp_<uuid_without_hyphens>` alias for the SDP counterparty id,
   * sized to fit BVNK's 36-character limit.
   */
  externalReference?: string;
  customerReference?: string;
  status?: string;
  verificationStatus?: BvnkVerificationStatus;
  verificationUrl?: string;
}

const SDP_COUNTERPARTY_ID_PATTERN =
  /^counterparty_([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i;

/**
 * Builds the value stored in BVNK's customer `externalReference` field.
 *
 * BVNK limits `externalReference` to 36 characters, while SDP counterparty ids
 * are `counterparty_<uuid>` and therefore too long. This function creates a
 * reversible BVNK-facing id in `cp_<uuid_without_hyphens>` format. BVNK returns
 * this caller-provided value in customer/payment webhooks, letting handlers
 * reconstruct the SDP counterparty id and load by primary key.
 *
 * @param counterpartyId SDP counterparty primary key in `counterparty_<uuid>` format.
 * @returns BVNK customer `externalReference` in `cp_<32_hex_uuid>` format.
 * @throws SdpPaymentsError with `INTERNAL_ERROR` when the counterparty id cannot be
 * represented in BVNK's compact externalReference format.
 */
export function buildBvnkCustomerExternalReference(counterpartyId: string): string {
  const match = SDP_COUNTERPARTY_ID_PATTERN.exec(counterpartyId);
  if (!match) {
    throw internalError(
      `Malformed SDP counterparty id for BVNK externalReference: ${counterpartyId}`
    );
  }
  return `cp_${match.slice(1).join("").toLowerCase()}`;
}

/** Per funding-spec (fiat+token+destination) virtual wallet + rule. */
export interface BvnkOnrampRequestSpec {
  currency: BvnkCryptoCurrency;
  network: BvnkNetwork;
  destinationWalletAddress: string;
  fiatCurrency: RampFiatCurrency;
}

export interface BvnkOnrampPaymentRuleState {
  walletId?: string;
  walletName?: string;
  walletStatus?: string;
  ruleId?: string;
  ruleStatus?: string;
  bankAccount?: BvnkBankFundingDetails;
  request?: BvnkOnrampRequestSpec;
  provisioningError?: string;
}

const BVNK_WALLET_ACTIVE_STATUSES = new Set(["ACTIVE", "COMPLETED"]);

export function isBvnkWalletActive(status: string | undefined): boolean {
  return status !== undefined && BVNK_WALLET_ACTIVE_STATUSES.has(status.toUpperCase());
}

export interface BvnkPaymentRuleResolution {
  customer: BvnkCustomerResolution;
  entry: BvnkOnrampPaymentRuleState;
  onboardingStatus: BvnkOnboardingStatus;
}

export function readBvnkData(
  providerData: CounterpartyRow["provider_data"]
): Record<string, unknown> {
  const bvnk = providerData.bvnk;
  return bvnk && typeof bvnk === "object" ? (bvnk as Record<string, unknown>) : {};
}

export function readBvnkCustomer(
  providerData: CounterpartyRow["provider_data"]
): BvnkCustomerResolution {
  const customer = readBvnkData(providerData).customer;
  return customer && typeof customer === "object" ? (customer as BvnkCustomerResolution) : {};
}

export function readBvnkWallets(
  providerData: CounterpartyRow["provider_data"]
): Record<string, BvnkOnrampPaymentRuleState> {
  const wallets = readBvnkData(providerData).wallets;
  return wallets && typeof wallets === "object"
    ? (wallets as Record<string, BvnkOnrampPaymentRuleState>)
    : {};
}

export function withBvnkOnrampPaymentRuleState(
  providerData: CounterpartyRow["provider_data"],
  onrampPaymentRuleKey: string,
  paymentRule: Partial<BvnkOnrampPaymentRuleState>
): CounterpartyRow["provider_data"] {
  const bvnk = readBvnkData(providerData);
  const wallets = readBvnkWallets(providerData);
  return {
    ...providerData,
    bvnk: {
      ...bvnk,
      wallets: {
        ...wallets,
        [onrampPaymentRuleKey]: {
          ...wallets[onrampPaymentRuleKey],
          ...paymentRule,
        },
      },
    },
  };
}

export function pendingBvnkOnrampPaymentRuleKeys(
  providerData: CounterpartyRow["provider_data"]
): string[] {
  return Object.entries(readBvnkWallets(providerData))
    .filter(([, entry]) => entry.request && !entry.ruleId)
    .map(([key]) => key);
}

/** Merchant-owned BVNK off-ramp wallet, one per fiat currency. */
export interface BvnkOfframpWallet {
  id: string;
  status?: string;
}

export function buildBvnkOfframpWalletName(
  fiatCurrency: RampFiatCurrency,
  counterpartyId: string
): string {
  return `sdp:offramp:${fiatCurrency}:${counterpartyId}`;
}

/**
 * Builds the BVNK wallet name for customer-owned on-ramp funding wallets.
 *
 * The name carries the SDP counterparty id and the deterministic on-ramp
 * payment rule fields so wallet lifecycle webhooks can update
 * `provider_data.bvnk.wallets[onrampPaymentRuleKey]` directly.
 *
 * @param counterpartyId SDP counterparty primary key.
 * @param onrampPaymentRuleKey Deterministic key from `buildBvnkOnrampPaymentRuleKey`.
 * @returns BVNK wallet name in
 * `sdp:onramp:<counterparty_id>:<fiat>:<crypto>_<network>:<destination>` format.
 */
export function buildBvnkOnrampWalletName(
  counterpartyId: string,
  onrampPaymentRuleKey: string
): string {
  const key = parseBvnkOnrampPaymentRuleKey(onrampPaymentRuleKey);
  return `sdp:onramp:${counterpartyId}:${key.fiatCurrency}:${key.cryptoCurrency}_${key.cryptoNetwork}:${key.destinationWalletAddress}`;
}

const BVNKOfframpWalletName = z.object({
  namespace: z.literal("sdp"),
  direction: z.literal("offramp"),
  fiatCurrency: z.enum(RAMP_FIAT_CURRENCIES),
  counterpartyId: z.string().min(1),
});

const BVNKOnrampWalletName = z.object({
  namespace: z.literal("sdp"),
  direction: z.literal("onramp"),
  counterpartyId: z.string().min(1),
  onrampKey: z.string().min(1),
});

export const BVNKWallet = z.discriminatedUnion("direction", [
  BVNKOfframpWalletName,
  BVNKOnrampWalletName,
]);

export type BVNKWallet = z.infer<typeof BVNKWallet>;

export function parseBvnkOfframpWalletName(
  walletName: string
): Extract<BVNKWallet, { direction: "offramp" }> {
  const parts = walletName.split(":");
  if (parts.length !== 4) {
    throw internalError(`Malformed BVNK off-ramp wallet name: ${walletName}`);
  }
  const [namespace, direction, fiatCurrency, counterpartyId] = parts;
  const parsed = BVNKOfframpWalletName.safeParse({
    namespace,
    direction,
    fiatCurrency,
    counterpartyId,
  });
  if (!parsed.success) {
    throw internalError(`Malformed BVNK off-ramp wallet name: ${walletName}`);
  }
  return parsed.data;
}

/**
 * Parses a customer-owned BVNK funding wallet name back into its SDP owner and
 * on-ramp entry key.
 *
 * @param walletName BVNK wallet `name` value.
 * @returns Parsed SDP counterparty id and `buildBvnkOnrampPaymentRuleKey` value.
 * @throws SdpPaymentsError with `INTERNAL_ERROR` when the name does not match the SDP
 * on-ramp wallet naming contract.
 */
export function parseBvnkOnrampWalletName(
  walletName: string
): Extract<BVNKWallet, { direction: "onramp" }> {
  const parts = walletName.split(":");
  if (parts.length !== 6) {
    throw internalError(`Malformed BVNK on-ramp wallet name: ${walletName}`);
  }
  const [namespace, direction, counterpartyId, fiatCurrency, cryptoRail, destinationWalletAddress] =
    parts;
  let onrampKey = `${fiatCurrency}:${cryptoRail}:${destinationWalletAddress}`;
  try {
    const key = parseBvnkOnrampPaymentRuleKey(onrampKey);
    onrampKey = buildBvnkOnrampPaymentRuleKey(
      key.fiatCurrency,
      key.cryptoCurrency,
      key.cryptoNetwork,
      key.destinationWalletAddress
    );
  } catch {
    throw internalError(`Malformed BVNK on-ramp wallet name: ${walletName}`);
  }
  const parsed = BVNKOnrampWalletName.safeParse({
    namespace,
    direction,
    counterpartyId,
    onrampKey,
  });
  if (!parsed.success) {
    throw internalError(`Malformed BVNK on-ramp wallet name: ${walletName}`);
  }
  return parsed.data;
}

export function readBvnkOfframpWallets(
  providerData: CounterpartyRow["provider_data"]
): Record<string, BvnkOfframpWallet> {
  const offramp = readRecord(readBvnkData(providerData).offramp)?.wallets;
  return offramp && typeof offramp === "object"
    ? (offramp as Record<string, BvnkOfframpWallet>)
    : {};
}

export function withBvnkOfframpWalletStatus(
  providerData: CounterpartyRow["provider_data"],
  fiatCurrency: RampFiatCurrency,
  status: string
): CounterpartyRow["provider_data"] {
  const bvnk = readBvnkData(providerData);
  const offramp =
    bvnk.offramp && typeof bvnk.offramp === "object"
      ? (bvnk.offramp as Record<string, unknown>)
      : {};
  const wallets = readBvnkOfframpWallets(providerData);
  return {
    ...providerData,
    bvnk: {
      ...bvnk,
      offramp: {
        ...offramp,
        wallets: {
          ...wallets,
          [fiatCurrency]: { ...wallets[fiatCurrency], status },
        },
      },
    },
  };
}

export function readBvnkOfframpWallet(
  providerData: CounterpartyRow["provider_data"],
  fiatCurrency: string
): BvnkOfframpWallet | undefined {
  return readBvnkOfframpWallets(providerData)[fiatCurrency];
}

/** A registered off-ramp payout beneficiary. PII-light: raw account details are not stored. */
export interface BvnkOfframpBeneficiary {
  /** `${fiatCurrency}:${hash(collectedData)}` — content-addressed so distinct bank details never collide. */
  key: string;
  fiatCurrency: string;
  accountType: string;
  createdAt: string;
}

export function readBvnkOfframpBeneficiaries(
  providerData: CounterpartyRow["provider_data"]
): Record<string, unknown> {
  const beneficiaries = readRecord(readBvnkData(providerData).offramp)?.beneficiaries;
  return beneficiaries && typeof beneficiaries === "object"
    ? (beneficiaries as Record<string, unknown>)
    : {};
}

function parseBvnkOfframpBeneficiary(key: string, value: unknown): BvnkOfframpBeneficiary {
  const { fiatCurrency, accountType, createdAt } = value as {
    fiatCurrency?: unknown;
    accountType?: unknown;
    createdAt?: unknown;
  };
  if (
    typeof fiatCurrency !== "string" ||
    typeof accountType !== "string" ||
    typeof createdAt !== "string"
  ) {
    throw internalError(`Malformed BVNK off-ramp beneficiary "${key}" in provider_data`);
  }
  return { key, fiatCurrency, accountType, createdAt };
}

export function readBvnkOfframpBeneficiaryByKey(
  providerData: CounterpartyRow["provider_data"],
  key: string
): BvnkOfframpBeneficiary | null {
  const value = readBvnkOfframpBeneficiaries(providerData)[key];
  return value === undefined ? null : parseBvnkOfframpBeneficiary(key, value);
}

export function latestBvnkOfframpBeneficiary(
  providerData: CounterpartyRow["provider_data"],
  fiatCurrency: string
): BvnkOfframpBeneficiary | null {
  const entries = Object.entries(readBvnkOfframpBeneficiaries(providerData))
    .filter(([key]) => key.startsWith(`${fiatCurrency}:`))
    .map(([key, value]) => parseBvnkOfframpBeneficiary(key, value))
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  return entries[0] ?? null;
}

/**
 * Schema for the logical components encoded into
 * `provider_data.bvnk.wallets[onrampPaymentRuleKey]`.
 */
export const BVNKOnrampPaymentRuleKey = z.object({
  fiatCurrency: z.enum(RAMP_FIAT_CURRENCIES),
  cryptoCurrency: z.enum(BVNK_CRYPTO_CURRENCIES),
  cryptoNetwork: z.enum(BVNK_NETWORKS),
  destinationWalletAddress: z
    .string()
    .min(1)
    .refine((value) => !value.includes(":")),
});

export type BVNKOnrampPaymentRuleKey = z.infer<typeof BVNKOnrampPaymentRuleKey>;

/**
 * Builds the deterministic key for a BVNK on-ramp payment rule.
 *
 * This key identifies the exact funding rule SDP needs for one fiat funding
 * currency, one crypto asset/network destination, and one destination wallet.
 * It is stored under `provider_data.bvnk.wallets` and embedded into the BVNK
 * wallet name so webhook handlers can reverse it without scanning by wallet id.
 *
 * @param fiatCurrency Fiat currency the counterparty sends into the BVNK funding account.
 * @param cryptoCurrency Crypto asset BVNK delivers for the on-ramp, such as `USDC`.
 * @param cryptoNetwork Blockchain network BVNK delivers on, such as `SOLANA`.
 * @param destinationWalletAddress Destination wallet address that receives the on-ramped crypto.
 * @returns Serialized on-ramp payment rule key in `<fiat>:<crypto>_<network>:<destination>` format.
 * @throws SdpPaymentsError with `INTERNAL_ERROR` when inputs do not satisfy the SDP key contract.
 */
export function buildBvnkOnrampPaymentRuleKey(
  fiatCurrency: RampFiatCurrency,
  cryptoCurrency: BvnkCryptoCurrency,
  cryptoNetwork: BvnkNetwork,
  destinationWalletAddress: string
): string {
  const parsed = BVNKOnrampPaymentRuleKey.safeParse({
    fiatCurrency,
    cryptoCurrency,
    cryptoNetwork,
    destinationWalletAddress,
  });
  if (!parsed.success) {
    throw internalError("Malformed BVNK on-ramp payment rule key input");
  }
  return `${parsed.data.fiatCurrency}:${parsed.data.cryptoCurrency}_${parsed.data.cryptoNetwork}:${parsed.data.destinationWalletAddress}`;
}

/**
 * Parses a stored BVNK on-ramp payment rule key back to its logical components.
 *
 * @param key Serialized key from `buildBvnkOnrampPaymentRuleKey`.
 * @returns Parsed fiat funding currency, on-ramped crypto asset/network, and destination wallet.
 * @throws SdpPaymentsError with `INTERNAL_ERROR` when the key no longer matches SDP's BVNK key contract.
 */
export function parseBvnkOnrampPaymentRuleKey(key: string): BVNKOnrampPaymentRuleKey {
  const parts = key.split(":");
  if (parts.length !== 3) {
    throw internalError(`Malformed BVNK on-ramp payment rule key: ${key}`);
  }
  const [fiatCurrency, cryptoRail, destinationWalletAddress] = parts;
  if (!cryptoRail.endsWith("_SOLANA")) {
    throw internalError(`Malformed BVNK on-ramp payment rule key: ${key}`);
  }
  const cryptoNetwork: BvnkNetwork = "SOLANA";
  const cryptoCurrency = cryptoRail.slice(0, -"_SOLANA".length);
  const parsed = BVNKOnrampPaymentRuleKey.safeParse({
    fiatCurrency,
    cryptoCurrency,
    cryptoNetwork,
    destinationWalletAddress,
  });
  if (!parsed.success) {
    throw internalError(`Malformed BVNK on-ramp payment rule key: ${key}`);
  }
  return parsed.data;
}

export function readBvnkOnrampPaymentRuleState(
  providerData: CounterpartyRow["provider_data"],
  key: string
): BvnkOnrampPaymentRuleState {
  const entry = readBvnkWallets(providerData)[key];
  return entry && typeof entry === "object" ? entry : {};
}

export async function bvnkRuleReference(
  counterpartyId: string,
  onrampKey: string
): Promise<string> {
  return (await hashString(`bvnk-rule:${counterpartyId}:${onrampKey}`)).slice(0, 36);
}

export function buildBvnkRuleEntity(counterparty: CounterpartyRow): BvnkRuleEntity {
  const address = counterparty.identity.address;

  return {
    type: BVNK_ENTITY_TYPE[counterparty.entity_type],
    customerIdentifier: counterparty.external_id ?? counterparty.id,
    relationshipType: "SELF_OWNED",
    ...(counterparty.entity_type === "individual"
      ? {
          firstName: counterparty.identity.firstName,
          lastName: counterparty.identity.lastName,
          dateOfBirth: counterparty.identity.dateOfBirth,
        }
      : { legalName: counterparty.display_name }),
    address: {
      addressLine1: address.line1,
      ...(address.line2 ? { addressLine2: address.line2 } : {}),
      ...(address.postalCode ? { postalCode: address.postalCode } : {}),
      city: address.city,
      countryCode: address.countryCode,
      country: address.countryCode,
      ...(address.subdivisionCode
        ? { stateCode: normalizeBvnkStateCode(address.countryCode, address.subdivisionCode) }
        : {}),
    },
  };
}

export function buildBvnkPartyDetails(
  counterparty: CounterpartyRow,
  role: "ORIGINATOR" | "BENEFICIARY"
): BvnkComplianceInput {
  return {
    partyDetails: [
      {
        type: role,
        entityType: BVNK_ENTITY_TYPE[counterparty.entity_type],
        relationshipType: "SELF_OWNED",
        ...(counterparty.entity_type === "individual"
          ? {
              firstName: counterparty.identity.firstName,
              lastName: counterparty.identity.lastName,
              dateOfBirth: counterparty.identity.dateOfBirth,
            }
          : {}),
        countryCode: counterparty.identity.address.countryCode,
      },
    ],
  };
}

export function buildBvnkOnrampInstruction(
  resolution: BvnkPaymentRuleResolution,
  params: {
    network: string;
    destinationWalletAddress: string;
    fiatCurrency: string;
    mode: SdpEnvironment;
  }
): BvnkPaymentRampInstruction {
  const { customer, entry, onboardingStatus } = resolution;
  const verificationNote =
    params.mode === "sandbox"
      ? "Complete identity verification to activate your funding account. BVNK requires you to verify the counterparty through Sumsub. No information entered via the sandbox will be verified."
      : "Complete identity verification to activate your funding account. BVNK requires you to verify the counterparty through Sumsub.";
  const notesByStatus = {
    ready: `Fund your ${params.fiatCurrency} BVNK virtual account to receive crypto on ${params.network}.`,
    verification_required: verificationNote,
    verification_failed:
      "Identity verification was not approved, so this funding account can't be activated. Contact support if you believe this is a mistake.",
    provisioning: "Setting up your funding account; bank details will appear in a moment.",
    verifying: "Identity verification is in review; funding details will appear once approved.",
  } as const satisfies Record<BvnkOnboardingStatus, string>;
  const notes = notesByStatus[onboardingStatus];
  return {
    provider: "bvnk",
    kind: "fiat_funding",
    onboardingStatus,
    verificationUrl: customer.verificationUrl,
    ruleId: entry.ruleId,
    ruleStatus: entry.ruleStatus,
    fundingWalletId: entry.walletId,
    fiatCurrency: params.fiatCurrency,
    beneficiaryAddress: params.destinationWalletAddress,
    network: params.network,
    bankAccount: entry.bankAccount,
    instructionsNotes: notes,
  };
}

export function bvnkOnboardingRequirements(
  resolution: BvnkPaymentRuleResolution,
  direction: RampDirection
): CounterpartyRequirements {
  switch (resolution.onboardingStatus) {
    case "ready":
      return readyCounterparty("bvnk", direction);
    case "verification_required": {
      const { verificationUrl } = resolution.customer;
      if (!verificationUrl) {
        throw internalError('BVNK reported "verification_required" without a verificationUrl.');
      }
      return {
        provider: "bvnk",
        direction,
        status: "customer_verification_required",
        verificationUrl,
      };
    }
    case "verifying":
      return { provider: "bvnk", direction, status: "customer_verifying" };
    case "verification_failed":
      return { provider: "bvnk", direction, status: "customer_verification_failed" };
    case "provisioning":
      return { provider: "bvnk", direction, status: "funding_account_provisioning" };
    default: {
      const exhaustive: never = resolution.onboardingStatus;
      throw internalError(`Unhandled BVNK onboarding status: ${String(exhaustive)}`);
    }
  }
}

export function bvnkOnrampStatusFromProviderData(
  providerData: CounterpartyRow["provider_data"],
  params: { cryptoToken: string; fiatCurrency: RampFiatCurrency; destinationWalletAddress: string }
): CounterpartyRequirements {
  const direction: RampDirection = "onramp";
  const customer = readBvnkCustomer(providerData);
  if (!customer.customerReference) {
    return { provider: "bvnk", direction, status: "onboarding_not_started" };
  }
  if (!isBvnkCustomerVerified(customer.status)) {
    const phase = bvnkUnverifiedOnboardingStatus(customer.status);
    switch (phase) {
      case "verifying":
        return { provider: "bvnk", direction, status: "customer_verifying" };
      case "verification_failed":
        return { provider: "bvnk", direction, status: "customer_verification_failed" };
      case "verification_required": {
        if (!customer.verificationUrl) {
          throw internalError('BVNK reported "verification_required" without a verificationUrl.');
        }
        return {
          provider: "bvnk",
          direction,
          status: "customer_verification_required",
          verificationUrl: customer.verificationUrl,
        };
      }
      default: {
        const exhaustive: never = phase;
        throw internalError(`Unhandled BVNK verification phase: ${String(exhaustive)}`);
      }
    }
  }
  const { currency, network } = normalizeBvnkCurrencyAndNetwork(params.cryptoToken);
  const key = buildBvnkOnrampPaymentRuleKey(
    params.fiatCurrency,
    currency,
    network,
    params.destinationWalletAddress
  );
  const entry = readBvnkOnrampPaymentRuleState(providerData, key);
  if (entry.ruleId && entry.bankAccount?.accountNumber) {
    return { provider: "bvnk", direction, status: "ready" };
  }
  if (entry.provisioningError && !entry.ruleId) {
    return { provider: "bvnk", direction, status: "provisioning_failed" };
  }
  return { provider: "bvnk", direction, status: "funding_account_provisioning" };
}
