/**
 * TOCTOU-safe affiliate payout ledger debit — shared by withdraw, fraud hold, and admin release.
 */

import { and, eq, like, sql } from 'drizzle-orm';
import type { TxDrizzleClient } from '@/server/db/client.js';
import { affiliatePayouts } from '@/server/db/schema.js';
import { cancelAffiliatePayout } from '@/server/db/queries/referrals/referral-writes.js';
import {
  debitPayoutInTx as moduleDebitPayoutInTx,
  ensurePayoutLedgerDebit as moduleEnsurePayoutLedgerDebit,
  verifyPayoutReadyForSettlement as moduleVerifyPayoutReadyForSettlement,
  PayoutLedgerMismatchError as ModulePayoutLedgerMismatchError,
} from '@platform-modules/affiliate';
import {
  asModuleMoneyTx,
  restorePayoutDebitInTx as bridgeRestorePayoutDebitInTx,
  type HostRetainedDeps,
} from '@/server/affiliate-module/host-retained.js';

export type PayoutLedgerTx = Parameters<Parameters<TxDrizzleClient['transaction']>[0]>[0];

const hostRetainedDeps: HostRetainedDeps = { now: () => new Date() };

/** Sentinel thrown inside a transaction so callers can map it to HTTP 409. */
export class InsufficientBalanceError extends Error {
  constructor(message = 'INSUFFICIENT_BALANCE') {
    super(message);
    this.name = 'InsufficientBalanceError';
  }
}

/** Sentinel thrown while the locked wallet shows no non-negative net balance. */
export class NegativeBalanceError extends InsufficientBalanceError {
  constructor() {
    super('NEGATIVE_BALANCE');
    this.name = 'NegativeBalanceError';
  }
}

/** Sentinel when payout ledger row is missing or amount does not match. */
export class PayoutLedgerMismatchError extends Error {
  constructor() {
    super('PAYOUT_LEDGER_MISMATCH');
    this.name = 'PayoutLedgerMismatchError';
  }
}

function isModuleInsufficientBalance(err: unknown): boolean {
  return (
    err instanceof Error &&
    (err as { _affiliateError?: string })._affiliateError === 'InsufficientBalanceError'
  );
}

function mapModulePayoutMoneyError(err: unknown): never {
  if (err instanceof ModulePayoutLedgerMismatchError) {
    throw new PayoutLedgerMismatchError();
  }
  if (isModuleInsufficientBalance(err)) {
    throw new InsufficientBalanceError();
  }
  throw err;
}

/**
 * Lock wallet_vesting, verify net balance and payout sufficiency, append redemption ledger row,
 * decrement maturedMinor, recompute withdrawableMinor, and link affiliate_payouts.ledger_entry_id.
 *
 * Live path — delegates to module debitPayoutInTx (FOR UPDATE via debitWithRead,
 * idempotency key redemption:affiliate_payout:<payoutId>, ledger_entry_id marker preserved).
 */
export async function debitPayoutInTx(
  tx: PayoutLedgerTx,
  userId: string,
  payoutId: string,
  amountAgorot: number,
): Promise<string> {
  try {
    const walletRows = (await tx.execute(sql`
      SELECT matured_minor, carried_debt_minor, withdrawable_minor
      FROM wallet_vesting
      WHERE owner_id = ${userId}
      FOR UPDATE
    `)) as {
      rows: Array<{
        matured_minor: bigint | number | string | null;
        carried_debt_minor: bigint | number | string | null;
        withdrawable_minor: bigint | number | string | null;
      }>;
    };
    const wallet = walletRows.rows[0];
    if (!wallet) {
      throw new NegativeBalanceError();
    }
    const netMinor = BigInt(wallet.matured_minor ?? 0) - BigInt(wallet.carried_debt_minor ?? 0);
    const withdrawableMinor = BigInt(wallet.withdrawable_minor ?? 0);
    if (netMinor <= 0n) throw new NegativeBalanceError();
    // netMinor and withdrawableMinor are two independent projections of the SAME
    // spendable balance (matured net of carried debt) — the ceiling is their minimum,
    // never their sum. Mirrors the module's own guard: amount > withdrawable || amount > matured.
    const requested = BigInt(amountAgorot);
    if (requested > netMinor || requested > withdrawableMinor) {
      throw new InsufficientBalanceError();
    }

    return await moduleDebitPayoutInTx(asModuleMoneyTx(tx), userId, payoutId, BigInt(amountAgorot));
  } catch (err) {
    mapModulePayoutMoneyError(err);
  }
}

/** Debit at most once — no-op when ledger_entry_id is already set. */
export async function ensurePayoutLedgerDebit(
  tx: PayoutLedgerTx,
  payout: { id: string; userId: string; amountAgorot: number; ledgerEntryId: string | null },
): Promise<string> {
  if (payout.ledgerEntryId) return payout.ledgerEntryId;
  try {
    return await moduleEnsurePayoutLedgerDebit(asModuleMoneyTx(tx), {
      id: payout.id,
      userId: payout.userId,
      amountAgorot: BigInt(payout.amountAgorot),
      ledgerEntryId: payout.ledgerEntryId,
    });
  } catch (err) {
    mapModulePayoutMoneyError(err);
  }
}

/**
 * Admin settlement guard: lock wallet, ensure redemption debit exists, verify ledger amount.
 * When debit is still missing, re-checks withdrawable under FOR UPDATE.
 */
export async function verifyPayoutReadyForSettlement(
  tx: PayoutLedgerTx,
  payout: { id: string; userId: string; amountAgorot: number; ledgerEntryId: string | null },
): Promise<string> {
  try {
    return await moduleVerifyPayoutReadyForSettlement(asModuleMoneyTx(tx), {
      id: payout.id,
      userId: payout.userId,
      amountAgorot: BigInt(payout.amountAgorot),
      ledgerEntryId: payout.ledgerEntryId,
    });
  } catch (err) {
    mapModulePayoutMoneyError(err);
  }
}

/**
 * Restore wallet balances after a failed/cancelled payout that already debited the ledger.
 * Live path — host-retained bridge (adjustment:affiliate_payout_reversal:<payoutId> key).
 */
export async function restorePayoutDebitInTx(
  tx: PayoutLedgerTx,
  userId: string,
  payoutId: string,
  amountAgorot: number,
): Promise<boolean> {
  return bridgeRestorePayoutDebitInTx(tx, hostRetainedDeps, userId, payoutId, BigInt(amountAgorot));
}

/**
 * Cancel fraud-held payouts for an enrollment — restore wallet iff already debited.
 * Reuses restorePayoutDebitInTx (canonical key adjustment:affiliate_payout_reversal:<id>).
 */
export async function cancelFraudHeldPayoutsInTx(
  tx: PayoutLedgerTx,
  enrollmentId: string,
): Promise<void> {
  const heldPayouts = await tx
    .select({
      id: affiliatePayouts.id,
      userId: affiliatePayouts.userId,
      amountAgorot: affiliatePayouts.amountAgorot,
      ledgerEntryId: affiliatePayouts.ledgerEntryId,
    })
    .from(affiliatePayouts)
    .where(
      and(
        eq(affiliatePayouts.enrollmentId, enrollmentId),
        eq(affiliatePayouts.status, 'requested'),
        like(affiliatePayouts.failureReason, 'fraud:%'),
      ),
    )
    .for('update');

  for (const payout of heldPayouts) {
    await cancelAffiliatePayout(tx, payout.id);

    if (payout.ledgerEntryId !== null) {
      await restorePayoutDebitInTx(tx, payout.userId, payout.id, payout.amountAgorot);
    }
  }
}
