/**
 * Custody Provisioning Helpers
 *
 * Creates custody wallets for new organizations using provider APIs.
 */

import type { VaultAddressesResponse } from "@solana/keychain-fireblocks";
import { ApiKeyStamper } from "@solana/keychain-turnkey";
import { importPKCS8, SignJWT } from "jose";
import { SigningError } from "../signing";
import {
  buildCoinbaseCdpAccountName,
  coinbaseCdpRequest,
  extractCoinbaseCdpAccountAddress,
  isCoinbaseCdpAlreadyExistsError,
} from "./coinbase";
import {
  encodeBasicAuth,
  normalizePem,
  normalizeProviderNameFragment,
  parseJsonResponse,
  randomHex,
  readErrorResponseText,
  sha256Hex,
  trimTrailingHyphens,
} from "./common";
import {
  buildParaUserIdentifier,
  type ParaWalletResponse,
  paraRequest,
  validateParaWallet,
  waitForParaWalletReady,
} from "./para";
import type { CustodyProvisioningRuntime } from "./runtime";

export {
  type AnchorageProvisioningConfig,
  type DeleteAnchorageOptions,
  deleteAnchorageWallet,
  type ProvisionAnchorageOptions,
  type ProvisionAnchorageResult,
  provisionAnchorageWallet,
} from "./anchorage";
export { normalizePem } from "./common";
export type { CustodyProvisioningRuntime } from "./runtime";

const DEFAULT_FIREBLOCKS_API_BASE_URL = "https://api.fireblocks.io";
const DEFAULT_PRIVY_API_BASE_URL = "https://api.privy.io/v1";
const PRIVY_REQUEST_TIMEOUT_MS = 10_000;
const DEFAULT_COINBASE_CDP_API_BASE_URL = "https://api.cdp.coinbase.com/platform";
const DEFAULT_PARA_API_BASE_URL = "https://api.getpara.com";
const DEFAULT_TURNKEY_API_BASE_URL = "https://api.turnkey.com";
const DEFAULT_COINBASE_CDP_NETWORK = "solana-devnet";
const DEFAULT_FIREBLOCKS_ASSET_ID = "SOL";
const DEFAULT_UTILA_NETWORK = "networks/solana-devnet";

interface FireblocksVaultAccountResponse {
  id: string;
  name: string;
}

interface PrivyWalletResponse {
  id: string;
  address: string;
  archived_at?: number | null;
  chain_type?: string;
  external_id?: string;
}

interface CoinbaseCdpSolanaAccountResponse {
  address: string;
  name?: string;
}

interface TurnkeyActivityResponse {
  activity?: {
    status?: string;
    result?: {
      createPrivateKeysResultV2?: {
        privateKeys?: Array<{
          privateKeyId?: string;
          addresses?: Array<{
            format?: string;
            address?: string;
          }>;
        }>;
      };
    };
  };
}

export interface ProvisionFireblocksOptions {
  orgId: string;
  orgSlug: string;
  vaultAccountId?: string;
}

export interface FireblocksProvisioningConfig {
  apiKey?: string;
  apiSecretPem?: string;
  apiBaseUrl?: string;
  assetId?: string;
}

export interface ProvisionFireblocksResult {
  vaultAccountId: string;
  assetId: string;
  apiBaseUrl: string;
}

export interface ProvisionPrivyOptions {
  walletId?: string;
  externalId?: string;
  idempotencyKey?: string;
}

export interface PrivyProvisioningConfig {
  appId?: string;
  appSecret?: string;
  apiBaseUrl?: string;
}

export interface ProvisionPrivyResult {
  walletId: string;
  address: string;
}

export interface ProvisionCoinbaseCdpOptions {
  orgId: string;
  orgSlug: string;
  accountPolicy?: string;
}

export interface CoinbaseCdpProvisioningConfig {
  apiKeyId?: string;
  apiKeySecret?: string;
  walletSecret?: string;
  apiBaseUrl?: string;
  network?: "solana" | "solana-devnet";
  accountScope?: string;
}

export interface ProvisionCoinbaseCdpResult {
  address: string;
  network: "solana" | "solana-devnet";
}

export interface ProvisionTurnkeyOptions {
  orgId: string;
  orgSlug: string;
}

export interface TurnkeyProvisioningConfig {
  apiPublicKey?: string;
  apiPrivateKey?: string;
  organizationId?: string;
  apiBaseUrl?: string;
}

export interface ProvisionTurnkeyResult {
  privateKeyId: string;
  address: string;
}

export interface ProvisionParaOptions {
  orgId: string;
  orgSlug: string;
  projectId?: string;
}

export interface ParaProvisioningConfig {
  apiKey?: string;
  apiBaseUrl?: string;
}

export interface ProvisionParaResult {
  walletId: string;
  address: string;
  userIdentifier: string;
  userIdentifierType: "CUSTOM_ID";
}

export interface ProvisionUtilaOptions {
  /** Display name for the new sub-wallet inside the vault. */
  displayName?: string;
}

export interface UtilaProvisioningConfig {
  serviceAccountEmail?: string;
  serviceAccountPrivateKeyPem?: string;
  vaultId?: string;
  network?: "networks/solana-mainnet" | "networks/solana-devnet";
  apiBaseUrl?: string;
}

export interface ProvisionUtilaResult {
  walletId: string;
  address: string;
  vaultId: string;
  network: "networks/solana-mainnet" | "networks/solana-devnet";
}

export async function provisionFireblocksVaultAccount(
  runtime: CustodyProvisioningRuntime,
  config: FireblocksProvisioningConfig,
  options: ProvisionFireblocksOptions
): Promise<ProvisionFireblocksResult> {
  const apiKey = config.apiKey;
  const apiSecretPem = config.apiSecretPem ? normalizePem(config.apiSecretPem) : undefined;

  if (!apiKey || !apiSecretPem) {
    throw new SigningError(
      "Fireblocks environment variables not configured: FIREBLOCKS_API_KEY, FIREBLOCKS_API_SECRET",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_FIREBLOCKS_API_BASE_URL;
  const assetId = config.assetId ?? DEFAULT_FIREBLOCKS_ASSET_ID;

  let vaultAccountId = options.vaultAccountId;

  if (!vaultAccountId) {
    const name = `sdp-${options.orgSlug || options.orgId}`.slice(0, 64);
    const response = await fireblocksRequest<FireblocksVaultAccountResponse>(runtime, {
      apiBaseUrl,
      apiKey,
      apiSecretPem,
      method: "POST",
      uri: "/v1/vault/accounts",
      body: {
        name,
        customerRefId: options.orgId,
      },
    });

    if (!response?.id) {
      throw new SigningError("Fireblocks vault account creation failed", "PROVIDER_NOT_CONFIGURED");
    }

    vaultAccountId = response.id;
  }

  // Ensure the asset wallet exists for this vault account.
  await fireblocksRequest<void>(runtime, {
    apiBaseUrl,
    apiKey,
    apiSecretPem,
    method: "POST",
    uri: `/v1/vault/accounts/${vaultAccountId}/${assetId}`,
    body: {},
    allowStatuses: [409],
  });

  // Ensure the address exists (Fireblocks signer expects addresses_paginated to return at least one).
  const addresses = await fetchFireblocksAddressesWithRetry(runtime, {
    apiBaseUrl,
    apiKey,
    apiSecretPem,
    vaultAccountId,
    assetId,
  });

  if (!addresses?.addresses?.length) {
    throw new SigningError(
      "Fireblocks vault wallet created, but no addresses are available",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  return { vaultAccountId, assetId, apiBaseUrl };
}

export async function provisionPrivyWallet(
  runtime: CustodyProvisioningRuntime,
  config: PrivyProvisioningConfig,
  options: ProvisionPrivyOptions
): Promise<ProvisionPrivyResult> {
  const appId = config.appId;
  const appSecret = config.appSecret;

  if (!appId || !appSecret) {
    throw new SigningError(
      "Privy environment variables not configured: PRIVY_APP_ID, PRIVY_APP_SECRET",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_PRIVY_API_BASE_URL;
  const authHeader = `Basic ${encodeBasicAuth(`${appId}:${appSecret}`)}`;
  validatePrivyProvisionOptions(options);

  if (options.walletId) {
    const existing = await privyRequest<PrivyWalletResponse>(runtime, {
      apiBaseUrl,
      authHeader,
      appId,
      method: "GET",
      path: `/wallets/${options.walletId}`,
    });

    if (!existing?.id || !existing?.address) {
      throw new SigningError("Privy wallet lookup failed", "PROVIDER_NOT_CONFIGURED");
    }

    return { walletId: existing.id, address: existing.address };
  }

  const externalWalletLookup = options.externalId
    ? { apiBaseUrl, authHeader, appId, externalId: options.externalId }
    : undefined;
  if (externalWalletLookup) {
    const existing = await findPrivyWalletByExternalId(runtime, externalWalletLookup);
    if (existing) return existing;
  }

  try {
    const created = await privyRequest<PrivyWalletResponse>(runtime, {
      apiBaseUrl,
      authHeader,
      appId,
      storedCredentialRequest: externalWalletLookup !== undefined,
      idempotencyKey: options.idempotencyKey,
      method: "POST",
      path: "/wallets",
      body: {
        chain_type: "solana",
        ...(options.externalId ? { external_id: options.externalId } : {}),
      },
    });

    if (externalWalletLookup) {
      return validatePrivyExternalWallet(created, externalWalletLookup.externalId);
    }

    if (!created?.id || !created.address) {
      throw new SigningError("Privy wallet creation failed", "PROVIDER_NOT_CONFIGURED");
    }
    return { walletId: created.id, address: created.address };
  } catch (error) {
    if (isTerminalPrivyProvisioningError(error)) {
      throw error;
    }
    if (externalWalletLookup) {
      try {
        const reconciled = await findPrivyWalletByExternalId(runtime, externalWalletLookup);
        if (reconciled) return reconciled;
      } catch (reconciliationError) {
        if (isPrivyProvisioningConflict(reconciliationError)) {
          throw reconciliationError;
        }
      }
    }
    throw error;
  }
}

function isPrivyProvisioningConflict(error: unknown): error is SigningError {
  return error instanceof SigningError && error.code === "CONFLICT";
}

function isTerminalPrivyProvisioningError(error: unknown): error is SigningError {
  return (
    error instanceof SigningError &&
    (error.code === "CONFLICT" || error.code === "PROVIDER_CREDENTIAL_INVALID")
  );
}

function validatePrivyProvisionOptions(options: ProvisionPrivyOptions): void {
  const hasExternalId = options.externalId !== undefined;
  const hasIdempotencyKey = options.idempotencyKey !== undefined;
  if (
    hasExternalId !== hasIdempotencyKey ||
    (options.walletId !== undefined && hasExternalId) ||
    (options.externalId !== undefined && !/^[a-zA-Z0-9_-]{1,64}$/.test(options.externalId)) ||
    (options.idempotencyKey !== undefined &&
      (options.idempotencyKey.length === 0 || options.idempotencyKey.length > 256))
  ) {
    throw new SigningError(
      "Privy wallet creation requires valid externalId and idempotencyKey options",
      "INVALID_REQUEST"
    );
  }
}

export async function provisionCoinbaseCdpAccount(
  runtime: CustodyProvisioningRuntime,
  config: CoinbaseCdpProvisioningConfig,
  options: ProvisionCoinbaseCdpOptions
): Promise<ProvisionCoinbaseCdpResult> {
  const apiKeyId = config.apiKeyId;
  const apiKeySecret = config.apiKeySecret;
  const walletSecret = config.walletSecret;

  if (!apiKeyId || !apiKeySecret || !walletSecret) {
    throw new SigningError(
      "Coinbase CDP environment variables not configured: COINBASE_CDP_API_KEY_ID, COINBASE_CDP_API_KEY_SECRET, COINBASE_CDP_WALLET_SECRET",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_COINBASE_CDP_API_BASE_URL;
  const network = config.network ?? DEFAULT_COINBASE_CDP_NETWORK;

  const name = buildCoinbaseCdpAccountName(
    runtime,
    options.orgSlug || options.orgId,
    config.accountScope
  );

  try {
    const created = await coinbaseCdpRequest<CoinbaseCdpSolanaAccountResponse>(runtime, {
      method: "POST",
      path: "/v2/solana/accounts",
      apiBaseUrl,
      apiKeyId,
      apiKeySecret,
      walletSecret,
      idempotencyKey: runtime.randomUUID(),
      body: {
        name,
        ...(options.accountPolicy ? { accountPolicy: options.accountPolicy } : {}),
      },
    });

    const createdAddress = extractCoinbaseCdpAccountAddress(created);
    if (!createdAddress) {
      throw new SigningError("Coinbase CDP wallet creation failed", "PROVIDER_NOT_CONFIGURED");
    }

    return { address: createdAddress, network };
  } catch (error) {
    if (!isCoinbaseCdpAlreadyExistsError(error)) {
      throw error;
    }

    try {
      const existingByName = await coinbaseCdpRequest<CoinbaseCdpSolanaAccountResponse>(runtime, {
        method: "GET",
        path: `/v2/solana/accounts/by-name/${encodeURIComponent(name)}`,
        apiBaseUrl,
        apiKeyId,
        apiKeySecret,
        walletSecret,
      });

      const existingAddressByName = extractCoinbaseCdpAccountAddress(existingByName);
      if (existingAddressByName) {
        return { address: existingAddressByName, network };
      }

      throw new SigningError(
        `Coinbase CDP account '${name}' already exists but lookup by name returned no address. Provide walletAddress to reuse the account.`,
        "PROVIDER_NOT_CONFIGURED"
      );
    } catch (lookupError) {
      if (lookupError instanceof SigningError && !isCoinbaseCdpAlreadyExistsError(lookupError)) {
        throw new SigningError(
          `Coinbase CDP account '${name}' already exists but could not be resolved by name. Provide walletAddress to reuse the account.`,
          "PROVIDER_NOT_CONFIGURED",
          lookupError
        );
      }

      throw lookupError;
    }
  }
}

export async function provisionParaWallet(
  runtime: CustodyProvisioningRuntime,
  config: ParaProvisioningConfig,
  options: ProvisionParaOptions
): Promise<ProvisionParaResult> {
  const apiKey = config.apiKey;
  if (!apiKey) {
    throw new SigningError(
      "Para environment variables not configured: PARA_API_KEY",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_PARA_API_BASE_URL;

  const userIdentifier = buildParaUserIdentifier(runtime, {
    orgId: options.orgId,
    projectId: options.projectId,
  });
  const created = await paraRequest<ParaWalletResponse>(runtime, {
    apiBaseUrl,
    apiKey,
    method: "POST",
    path: "/v1/wallets",
    body: {
      type: "SOLANA",
      scheme: "ED25519",
      userIdentifier,
      userIdentifierType: "CUSTOM_ID",
    },
  });

  if (!created?.id) {
    throw new SigningError("Para wallet creation failed", "PROVIDER_NOT_CONFIGURED");
  }

  const readyWallet = await waitForParaWalletReady(runtime, {
    apiBaseUrl,
    apiKey,
    walletId: created.id,
  });
  const validated = validateParaWallet(readyWallet, created.id);

  return {
    walletId: validated.id,
    address: validated.address,
    userIdentifier,
    userIdentifierType: "CUSTOM_ID",
  };
}

export async function provisionTurnkeyPrivateKey(
  runtime: CustodyProvisioningRuntime,
  config: TurnkeyProvisioningConfig,
  options: ProvisionTurnkeyOptions
): Promise<ProvisionTurnkeyResult> {
  const apiPublicKey = config.apiPublicKey;
  const apiPrivateKey = config.apiPrivateKey;
  const organizationId = config.organizationId;

  if (!apiPublicKey || !apiPrivateKey || !organizationId) {
    throw new SigningError(
      "Turnkey environment variables not configured: TURNKEY_API_PUBLIC_KEY, TURNKEY_API_PRIVATE_KEY, TURNKEY_ORGANIZATION_ID",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const apiBaseUrl = config.apiBaseUrl ?? DEFAULT_TURNKEY_API_BASE_URL;

  const created = await turnkeyRequest<TurnkeyActivityResponse>(runtime, {
    apiBaseUrl,
    apiPublicKey,
    apiPrivateKey,
    method: "POST",
    path: "/public/v1/submit/create_private_keys",
    body: {
      type: "ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2",
      timestampMs: runtime.now().toString(),
      organizationId,
      parameters: {
        privateKeys: [
          {
            privateKeyName: buildTurnkeyPrivateKeyName(runtime, options.orgSlug || options.orgId),
            curve: "CURVE_ED25519",
            privateKeyTags: [],
            addressFormats: ["ADDRESS_FORMAT_SOLANA"],
          },
        ],
      },
    },
  });

  const createdKey = created.activity?.result?.createPrivateKeysResultV2?.privateKeys?.[0];

  const privateKeyId = createdKey?.privateKeyId;
  const address = findSolanaAddress(createdKey?.addresses);
  if (!privateKeyId || !address) {
    throw new SigningError("Turnkey private key creation failed", "PROVIDER_NOT_CONFIGURED");
  }

  return { privateKeyId, address };
}

export async function provisionUtilaWallet(
  runtime: CustodyProvisioningRuntime,
  config: UtilaProvisioningConfig,
  options: ProvisionUtilaOptions
): Promise<ProvisionUtilaResult> {
  const serviceAccountEmail = config.serviceAccountEmail;
  const serviceAccountPrivateKeyPem = config.serviceAccountPrivateKeyPem;
  const configuredVaultId = config.vaultId;
  const network = config.network ?? DEFAULT_UTILA_NETWORK;

  if (!serviceAccountEmail || !serviceAccountPrivateKeyPem || !configuredVaultId) {
    throw new SigningError(
      "Utila environment variables not configured: UTILA_SERVICE_ACCOUNT_EMAIL, UTILA_SERVICE_ACCOUNT_PRIVATE_KEY, UTILA_VAULT_ID",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  const vaultId = normalizeUtilaVaultId(configuredVaultId);
  if (!vaultId) {
    throw new SigningError(
      "Utila vault ID is empty after normalization",
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  // Create a new Solana sub-wallet inside the configured vault. Passing `networks`
  // makes Utila derive the Solana address as part of wallet creation.
  const apiBaseUrl = normalizeUtilaApiBaseUrl(config.apiBaseUrl);
  const token = await mintUtilaAccessToken(
    runtime,
    serviceAccountEmail,
    serviceAccountPrivateKeyPem
  );

  const response = await runtime.fetch(
    `${apiBaseUrl}/v2/vaults/${encodeURIComponent(vaultId)}/wallets`,
    {
      method: "POST",
      headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
      body: JSON.stringify({
        displayName: options.displayName ?? "SDP Wallet",
        networks: [network],
      }),
    }
  );

  if (!response.ok) {
    const detail = await safeReadUtilaError(response);
    throw new SigningError(
      `Utila CreateWallet failed (${response.status}): ${detail}`,
      "NETWORK_ERROR"
    );
  }

  const body = (await response.json()) as UtilaCreateWalletResponse;
  const walletId = extractUtilaWalletId(body.wallet?.name);
  const address = body.wallet?.solanaDetails?.address;

  if (!walletId || !address) {
    throw new SigningError(
      "Utila CreateWallet response missing wallet id or Solana address",
      "NETWORK_ERROR"
    );
  }

  return { walletId, address, vaultId, network };
}

interface UtilaCreateWalletResponse {
  wallet?: {
    name?: string;
    solanaDetails?: { address?: string };
  };
}

const UTILA_API_AUDIENCE = "https://api.utila.io/";
const DEFAULT_UTILA_API_BASE_URL = "https://api.utila.io";

/** Mint a short-lived Utila service-account JWT for the REST API. */
async function mintUtilaAccessToken(
  runtime: CustodyProvisioningRuntime,
  serviceAccountEmail: string,
  serviceAccountPrivateKeyPem: string
): Promise<string> {
  const privateKey = await importPKCS8(normalizePem(serviceAccountPrivateKeyPem), "RS256");
  const now = Math.floor(runtime.now() / 1000);
  return new SignJWT({})
    .setProtectedHeader({ alg: "RS256" })
    .setSubject(serviceAccountEmail)
    .setAudience(UTILA_API_AUDIENCE)
    .setIssuedAt(now)
    .setExpirationTime(now + 5 * 60)
    .sign(privateKey);
}

function normalizeUtilaApiBaseUrl(value?: string): string {
  const url = value ?? DEFAULT_UTILA_API_BASE_URL;
  let end = url.length;

  while (end > 0 && url.charCodeAt(end - 1) === 47) {
    end -= 1;
  }

  return url.slice(0, end);
}

function normalizeUtilaVaultId(value: string): string {
  const trimmed = value.trim();
  const marker = "/vaults/";
  const markerIndex = trimmed.lastIndexOf(marker);
  const resourceId = markerIndex === -1 ? trimmed : trimmed.slice(markerIndex + marker.length);
  return resourceId.startsWith("vaults/") ? resourceId.slice("vaults/".length) : resourceId;
}

/** Extract the short wallet id from a `vaults/{vault}/wallets/{wallet}` resource name. */
function extractUtilaWalletId(name?: string): string | undefined {
  if (!name) return undefined;
  const marker = "/wallets/";
  const index = name.lastIndexOf(marker);
  return index === -1 ? name : name.slice(index + marker.length);
}

async function safeReadUtilaError(response: Response): Promise<string> {
  try {
    return (await response.text()).slice(0, 300);
  } catch {
    return response.statusText;
  }
}

interface FireblocksRequestParams {
  apiBaseUrl: string;
  apiKey: string;
  apiSecretPem: string;
  method: "GET" | "POST";
  uri: string;
  body?: unknown;
  allowStatuses?: number[];
}

interface FireblocksAddressesParams {
  apiBaseUrl: string;
  apiKey: string;
  apiSecretPem: string;
  vaultAccountId: string;
  assetId: string;
}

interface TurnkeyRequestParams {
  method: "POST";
  path: string;
  apiBaseUrl: string;
  apiPublicKey: string;
  apiPrivateKey: string;
  body: Record<string, unknown>;
}

async function fetchFireblocksAddressesWithRetry(
  runtime: CustodyProvisioningRuntime,
  params: FireblocksAddressesParams
): Promise<VaultAddressesResponse> {
  const maxAttempts = 5;
  const delayMs = 1000;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    const response = await fireblocksRequest<VaultAddressesResponse>(runtime, {
      apiBaseUrl: params.apiBaseUrl,
      apiKey: params.apiKey,
      apiSecretPem: params.apiSecretPem,
      method: "GET",
      uri: `/v1/vault/accounts/${params.vaultAccountId}/${params.assetId}/addresses_paginated?limit=1`,
    });

    if (response?.addresses?.length) {
      return response;
    }

    if (attempt < maxAttempts) {
      await runtime.sleep(delayMs);
    }
  }

  return { addresses: [] };
}

async function fireblocksRequest<T>(
  runtime: CustodyProvisioningRuntime,
  params: FireblocksRequestParams
): Promise<T> {
  const bodyStr = params.body ? JSON.stringify(params.body) : "";
  const token = await createFireblocksJwt(
    runtime,
    params.apiKey,
    params.apiSecretPem,
    params.uri,
    bodyStr
  );

  const response = await runtime.fetch(`${params.apiBaseUrl}${params.uri}`, {
    method: params.method,
    headers: {
      Authorization: `Bearer ${token}`,
      "X-API-Key": params.apiKey,
      ...(params.body ? { "Content-Type": "application/json" } : {}),
    },
    body: params.body ? bodyStr : undefined,
  });

  if (!response.ok && !(params.allowStatuses ?? []).includes(response.status)) {
    const errorText = await readErrorResponseText(response);
    throw new SigningError(
      `Fireblocks API error: ${response.status} - ${errorText}`,
      "PROVIDER_NOT_CONFIGURED"
    );
  }

  return parseJsonResponse<T>(response);
}

async function createFireblocksJwt(
  runtime: CustodyProvisioningRuntime,
  apiKey: string,
  privateKeyPem: string,
  uri: string,
  body: string
): Promise<string> {
  const privateKey = await importPKCS8(privateKeyPem, "RS256");
  const bodyHash = await sha256Hex(runtime, body);
  const nonce = runtime.randomUUID();
  const now = Math.floor(runtime.now() / 1000);

  return new SignJWT({ bodyHash, nonce, uri })
    .setProtectedHeader({ alg: "RS256" })
    .setSubject(apiKey)
    .setIssuedAt(now)
    .setExpirationTime(now + 30)
    .sign(privateKey);
}

interface PrivyRequestParams {
  apiBaseUrl: string;
  authHeader: string;
  appId: string;
  allowNotFound?: boolean;
  idempotencyKey?: string;
  storedCredentialRequest?: boolean;
  method: "GET" | "POST";
  path: string;
  body?: unknown;
}

async function privyRequest<T>(
  runtime: CustodyProvisioningRuntime,
  params: PrivyRequestParams
): Promise<T> {
  try {
    const response = await runtime.fetch(`${params.apiBaseUrl}${params.path}`, {
      method: params.method,
      headers: {
        Authorization: params.authHeader,
        "privy-app-id": params.appId,
        ...(params.idempotencyKey ? { "privy-idempotency-key": params.idempotencyKey } : {}),
        ...(params.body ? { "Content-Type": "application/json" } : {}),
      },
      body: params.body ? JSON.stringify(params.body) : undefined,
      signal: params.storedCredentialRequest
        ? AbortSignal.timeout(PRIVY_REQUEST_TIMEOUT_MS)
        : undefined,
    });

    if (!response.ok) {
      const errorText = await readErrorResponseText(response);
      const code =
        response.status === 401 && params.storedCredentialRequest
          ? "PROVIDER_CREDENTIAL_INVALID"
          : response.status === 404 && params.allowNotFound
            ? "NOT_FOUND"
            : response.status >= 500 && params.storedCredentialRequest
              ? "NETWORK_ERROR"
              : "PROVIDER_NOT_CONFIGURED";
      throw new SigningError(`Privy API error: ${response.status} - ${errorText}`, code);
    }

    return parseJsonResponse<T>(response);
  } catch (error) {
    if (error instanceof SigningError) {
      throw error;
    }

    throw new SigningError(
      `Failed to call Privy API: ${error instanceof Error ? error.message : "Unknown error"}`,
      "NETWORK_ERROR",
      error instanceof Error ? error : undefined
    );
  }
}

async function findPrivyWalletByExternalId(
  runtime: CustodyProvisioningRuntime,
  params: Pick<PrivyRequestParams, "apiBaseUrl" | "authHeader" | "appId"> & {
    externalId: string;
  }
): Promise<ProvisionPrivyResult | undefined> {
  let wallet: PrivyWalletResponse;
  try {
    wallet = await privyRequest<PrivyWalletResponse>(runtime, {
      apiBaseUrl: params.apiBaseUrl,
      allowNotFound: true,
      authHeader: params.authHeader,
      appId: params.appId,
      method: "GET",
      path: `/wallets/ext_wal_${encodeURIComponent(params.externalId)}`,
      storedCredentialRequest: true,
    });
  } catch (error) {
    if (error instanceof SigningError && error.code === "NOT_FOUND") {
      return undefined;
    }
    throw error;
  }

  return validatePrivyExternalWallet(wallet, params.externalId);
}

function validatePrivyWalletPayload(wallet: PrivyWalletResponse): ProvisionPrivyResult {
  if (!wallet.id || !wallet.address) {
    throw new SigningError("Privy wallet outcome is unknown", "NETWORK_ERROR");
  }
  return { walletId: wallet.id, address: wallet.address };
}

function validatePrivyExternalWallet(
  wallet: PrivyWalletResponse,
  externalId: string
): ProvisionPrivyResult {
  const result = validatePrivyWalletPayload(wallet);
  if (wallet.archived_at != null) {
    throw new SigningError("Privy external wallet is archived", "CONFLICT");
  }
  if (!wallet.chain_type || !wallet.external_id) {
    throw new SigningError("Privy wallet identity is unknown", "NETWORK_ERROR");
  }
  if (wallet.chain_type !== "solana" || wallet.external_id !== externalId) {
    throw new SigningError("Privy external wallet identity does not match", "CONFLICT");
  }
  return result;
}

async function turnkeyRequest<T>(
  runtime: CustodyProvisioningRuntime,
  params: TurnkeyRequestParams
): Promise<T> {
  const body = JSON.stringify(params.body);
  const stamper = new ApiKeyStamper({
    apiPrivateKey: params.apiPrivateKey,
    apiPublicKey: params.apiPublicKey,
  });
  // ApiKeyStamper is currently synchronous, but normalize in case the SDK
  // ever changes stamp() to return a Promise.
  const stamp = await Promise.resolve(stamper.stamp(body));

  try {
    const response = await runtime.fetch(`${params.apiBaseUrl}${params.path}`, {
      method: params.method,
      headers: {
        "Content-Type": "application/json",
        [stamp.stampHeaderName]: stamp.stampHeaderValue,
      },
      body,
    });

    if (!response.ok) {
      const errorText = await readErrorResponseText(response);
      throw new SigningError(
        `Turnkey API error: ${response.status} - ${errorText}`,
        "PROVIDER_NOT_CONFIGURED"
      );
    }

    return parseJsonResponse<T>(response);
  } catch (error) {
    if (error instanceof SigningError) {
      throw error;
    }

    throw new SigningError(
      `Failed to call Turnkey API: ${error instanceof Error ? error.message : "Unknown error"}`,
      "NETWORK_ERROR",
      error instanceof Error ? error : undefined
    );
  }
}

function buildTurnkeyPrivateKeyName(runtime: CustodyProvisioningRuntime, value: string): string {
  const suffix = randomHex(runtime, 2);
  let normalized = normalizeProviderNameFragment(value);

  if (!normalized) {
    normalized = "org";
  }

  let name = `sdp-${normalized}-${suffix}`.slice(0, 60);
  name = trimTrailingHyphens(name);

  if (!name) {
    name = `sdp-${randomHex(runtime, 3)}`;
  }

  return name;
}

function findSolanaAddress(
  addresses:
    | Array<{
        format?: string;
        address?: string;
      }>
    | undefined
): string | undefined {
  if (!addresses?.length) return undefined;

  const solana = addresses.find((entry) => entry.format === "ADDRESS_FORMAT_SOLANA");
  if (solana?.address) {
    return solana.address;
  }

  return addresses.find((entry) => Boolean(entry.address))?.address;
}
