/**
 * W4 — Host-retained money ops repoint onto module wallet_vesting / ledger_entries.
 * Mirrors donor referrals/clawback.ts, service.ts redeem, ledger.ts adjustment, payout-debit.ts restore.
 */
import { sql, eq } from 'drizzle-orm';
import { appendEntry, type LedgerSchema } from '@platform-modules/ledger';
import {
  recomputeWithdrawable,
  settleClawback,
  walletVesting,
  type VestingSchema,
} from '@platform-modules/ledger/vesting';
import { affiliateEntriesTable } from '@platform-modules/affiliate/schema';
import type { AffiliateSchema } from '@platform-modules/affiliate/schema';
import type { Transaction } from '@platform-modules/db';
import type * as hostSchema from '@/server/db/schema';
import { normMinor } from './backfill';
import { asOrderLineId, asPayoutId, asUserId, toModuleRef } from '@/server/platform-seams/ids.js';

type SqlRows<T extends Record<string, unknown>> = { rows: T[] };

// Transaction is enforced at the TYPE: every export takes `tx: HostTransaction`
// (a `Transaction<…>` handle obtainable only via `db.transaction()`), mirroring the
// donor `…InTx` ops. Unlike `backfill` (which self-opens a tx and therefore needs a
// fail-closed `assertTransactionalDatabase`), these run inside a caller tx, so no
// runtime guard is reachable — the type IS the floor.

export type HostRetainedDeps = {
  now: () => Date;
};

export type HostTransaction = Transaction<typeof hostSchema>;

/** Bridge host drizzle tx to module ledger/vesting/affiliate API (companion tables share one DB tx). */
type ModuleMoneyTx = Transaction<
  typeof hostSchema & AffiliateSchema & LedgerSchema & VestingSchema
>;

export function asModuleMoneyTx(tx: HostTransaction): ModuleMoneyTx {
  return tx as unknown as ModuleMoneyTx;
}

export type ClawbackResult = {
  classification: 'pending' | 'matured';
  originalEntryId: string;
  clawbackEntryId: string;
  clawbackAmountMinor: bigint;
};

type OriginalEarnRow = {
  id: string;
  owner_id: string;
  amount_minor: bigint | number | string;
  entry_type: string;
  source_type: string;
  source_id: string;
  swept_at: Date | string | null;
  mature_at: Date | string | null;
};

export function buildIdempotencyKey(
  entryType: string,
  sourceType: string,
  sourceId: string,
): string {
  return `${entryType}:${sourceType}:${sourceId}`;
}

function toBigint(v: bigint | number | string): bigint {
  return normMinor(v);
}

/**
 * Classify clawback side from ledger_entry_vesting.swept_at (donor swept_at semantics).
 */
export function classifyClawback(row: { sweptAt: Date | null }): 'pending' | 'matured' {
  return row.sweptAt === null ? 'pending' : 'matured';
}

async function computeEligibleAndPaid(
  tx: HostTransaction,
  ownerId: string,
): Promise<{ eligibleMinor: bigint; paidMinor: bigint }> {
  const ownerRef = toModuleRef(asUserId(ownerId));
  const eligibleResult = (await tx.execute(sql`
    SELECT COALESCE(SUM(sub.delta), 0) AS eligible_minor
    FROM (
      SELECT le.delta
      FROM ledger_entry_vesting lev
      INNER JOIN ledger_entries le ON le.id = lev.entry_id
      INNER JOIN affiliate_entries ae ON ae.entry_id = lev.entry_id
      LEFT JOIN referrals r ON r.id = ae.referral_id
      WHERE ae.owner_id = ${ownerRef}
        AND ae.entry_type IN ('affiliate_commission', 'referral_reward')
        AND lev.withdrawable_at IS NOT NULL
        AND lev.withdrawable_at <= NOW()
        AND lev.swept_at IS NOT NULL
        AND (ae.referral_id IS NULL OR r.quarantined_at IS NULL)
      UNION ALL
      SELECT le.delta
      FROM affiliate_entries ae
      INNER JOIN ledger_entries le ON le.id = ae.entry_id
      WHERE ae.owner_id = ${ownerRef}
        AND ae.entry_type = 'refund_clawback'
        AND ae.consumed_at IS NOT NULL
      UNION ALL
      SELECT le.delta
      FROM affiliate_entries ae
      INNER JOIN ledger_entries le ON le.id = ae.entry_id
      WHERE ae.owner_id = ${ownerRef}
        AND ae.entry_type = 'redemption'
        AND ae.source_type <> 'affiliate_payout'
    ) sub
  `)) as SqlRows<{ eligible_minor: bigint | number | string }>;

  const paidResult = (await tx.execute(sql`
    SELECT COALESCE(SUM(amount_agorot), 0) AS paid_minor
    FROM affiliate_payouts
    WHERE user_id = ${ownerRef}
      AND status IN ('requested', 'approved', 'processing', 'paid')
  `)) as SqlRows<{ paid_minor: bigint | number | string }>;

  return {
    eligibleMinor: toBigint(eligibleResult.rows[0]?.eligible_minor ?? 0),
    paidMinor: toBigint(paidResult.rows[0]?.paid_minor ?? 0),
  };
}

export async function recomputeOwnerWithdrawable(
  tx: HostTransaction,
  ownerId: string,
): Promise<void> {
  const moduleTx = asModuleMoneyTx(tx);
  const ownerRef = toModuleRef(asUserId(ownerId));
  await recomputeWithdrawable(moduleTx, { ownerId: ownerRef }, async () =>
    computeEligibleAndPaid(tx, ownerRef),
  );
}

/**
 * Apply clawback against an original earn entry — donor clawback.ts parity on module tables.
 */
export async function applyClawbackInTx(
  tx: HostTransaction,
  deps: HostRetainedDeps,
  originalLedgerEntryId: string,
  refundAmountMinor: bigint,
  refundEventId?: string,
): Promise<ClawbackResult> {
  const origRows = (await tx.execute(sql`
    SELECT
      ae.entry_id AS id,
      ae.owner_id,
      le.delta AS amount_minor,
      ae.entry_type,
      ae.source_type,
      ae.source_id,
      lev.swept_at,
      lev.mature_at
    FROM affiliate_entries ae
    INNER JOIN ledger_entries le ON le.id = ae.entry_id
    LEFT JOIN ledger_entry_vesting lev ON lev.entry_id = ae.entry_id
    WHERE ae.entry_id = ${originalLedgerEntryId}
      AND ae.entry_type IN ('affiliate_commission', 'referral_reward')
    FOR UPDATE OF ae
  `)) as SqlRows<OriginalEarnRow>;

  if (!origRows.rows.length) {
    throw new Error(`CLAWBACK_ORIGINAL_NOT_FOUND: ${originalLedgerEntryId}`);
  }

  const orig = origRows.rows[0]!;
  const sweptAt =
    orig.swept_at === null || orig.swept_at === undefined
      ? null
      : orig.swept_at instanceof Date
        ? orig.swept_at
        : new Date(orig.swept_at);
  const classification = classifyClawback({ sweptAt });

  const clawbackSourceId = refundEventId
    ? `clawback:${orig.source_id}:${refundEventId}`
    : `clawback:${orig.source_id}`;
  const clawbackMemo = `clawback of ${orig.entry_type} entry ${orig.id}`;
  const idempotencyKey = buildIdempotencyKey('refund_clawback', orig.source_type, clawbackSourceId);

  const existingClawback = (await tx.execute(sql`
    SELECT ae.entry_id AS id, le.delta AS amount_minor
    FROM affiliate_entries ae
    INNER JOIN ledger_entries le ON le.id = ae.entry_id
    WHERE ae.entry_type = 'refund_clawback'
      AND ae.source_type = ${orig.source_type}
      AND ae.source_id = ${clawbackSourceId}
    LIMIT 1
  `)) as SqlRows<{ id: string; amount_minor: bigint | number | string }>;

  if (existingClawback.rows.length > 0) {
    const row = existingClawback.rows[0]!;
    const amount = toBigint(row.amount_minor);
    return {
      classification,
      originalEntryId: orig.id,
      clawbackEntryId: row.id,
      clawbackAmountMinor: amount < 0n ? -amount : amount,
    };
  }

  const sumRows = (await tx.execute(sql`
    SELECT COALESCE(SUM(-le.delta), 0) AS already_clawed
    FROM affiliate_entries ae
    INNER JOIN ledger_entries le ON le.id = ae.entry_id
    WHERE ae.entry_type = 'refund_clawback'
      AND ae.source_type = ${orig.source_type}
      AND ae.memo = ${clawbackMemo}
      AND ae.source_id != ${clawbackSourceId}
  `)) as SqlRows<{ already_clawed: bigint | number | string }>;

  const alreadyClawed = toBigint(sumRows.rows[0]?.already_clawed ?? 0);
  const originalEarnAmount = toBigint(orig.amount_minor);
  const remainingHeadroom = originalEarnAmount - alreadyClawed;
  const clawbackAmount =
    refundAmountMinor < remainingHeadroom
      ? refundAmountMinor
      : remainingHeadroom > 0n
        ? remainingHeadroom
        : 0n;

  if (clawbackAmount <= 0n) {
    return {
      classification,
      originalEntryId: orig.id,
      clawbackEntryId: 'none',
      clawbackAmountMinor: 0n,
    };
  }

  const nettingAt = classification === 'matured' ? deps.now() : null;
  const ownerRef = toModuleRef(asUserId(orig.owner_id));

  const { inserted, id } = await appendEntry(asModuleMoneyTx(tx), {
    key: idempotencyKey,
    delta: -clawbackAmount,
    reason: 'refund_clawback',
    ref: {
      sourceType: orig.source_type,
      sourceId: clawbackSourceId,
      userId: ownerRef,
      memo: clawbackMemo,
      originalEntryId: orig.id,
    },
  });

  let clawbackEntryId: string;
  if (inserted && id !== null) {
    clawbackEntryId = id;
    await tx.insert(affiliateEntriesTable).values({
      entryId: id,
      ownerId: ownerRef,
      entryType: 'refund_clawback',
      sourceType: orig.source_type,
      sourceId: clawbackSourceId,
      memo: clawbackMemo,
      consumedAt: nettingAt,
    });

    if (classification === 'pending') {
      await tx
        .update(walletVesting)
        .set({
          pendingMinor: sql`GREATEST(0, ${walletVesting.pendingMinor} - ${clawbackAmount})`,
          updatedAt: deps.now(),
        })
        .where(eq(walletVesting.ownerId, ownerRef));
    } else {
      await settleClawback(asModuleMoneyTx(tx), {
        ownerId: ownerRef,
        amountMinor: clawbackAmount,
      });
      await recomputeOwnerWithdrawable(tx, ownerRef);
    }
  } else {
    const existing = (await tx.execute(sql`
      SELECT ae.entry_id AS id
      FROM affiliate_entries ae
      WHERE ae.entry_type = 'refund_clawback'
        AND ae.source_type = ${orig.source_type}
        AND ae.source_id = ${clawbackSourceId}
      LIMIT 1
    `)) as SqlRows<{ id: string }>;
    clawbackEntryId = existing.rows.length > 0 ? existing.rows[0]!.id : 'unknown';
  }

  return {
    classification,
    originalEntryId: orig.id,
    clawbackEntryId,
    clawbackAmountMinor: clawbackAmount,
  };
}

/**
 * Apply wallet credit toward an order — donor service.ts redeemCreditForOrder parity.
 */
export async function redeemCreditForOrderInTx(
  tx: HostTransaction,
  _deps: HostRetainedDeps,
  args: { userId: string; orderTotalMinor: bigint; purchaseId: string },
): Promise<bigint> {
  const userRef = toModuleRef(asUserId(args.userId));
  const purchaseRef = toModuleRef(asOrderLineId(args.purchaseId));
  const [locked] = await tx
    .select({
      maturedMinor: walletVesting.maturedMinor,
      carriedDebtMinor: walletVesting.carriedDebtMinor,
    })
    .from(walletVesting)
    .where(eq(walletVesting.ownerId, userRef))
    .for('update');

  const maturedMinor = locked?.maturedMinor ?? 0n;
  const carriedDebtMinor = locked?.carriedDebtMinor ?? 0n;
  const balance = maturedMinor > carriedDebtMinor ? maturedMinor - carriedDebtMinor : 0n;
  const applied =
    balance <= 0n || args.orderTotalMinor <= 0n
      ? 0n
      : balance < args.orderTotalMinor
        ? balance
        : args.orderTotalMinor;
  if (applied <= 0n) return 0n;

  const idempotencyKey = buildIdempotencyKey('redemption', 'purchase', purchaseRef);
  const { inserted, id } = await appendEntry(asModuleMoneyTx(tx), {
    key: idempotencyKey,
    delta: -applied,
    reason: 'redemption',
    ref: {
      sourceType: 'purchase',
      sourceId: purchaseRef,
      userId: userRef,
    },
  });

  if (!inserted || id === null) return 0n;

  await tx.insert(affiliateEntriesTable).values({
    entryId: id,
    ownerId: userRef,
    entryType: 'redemption',
    sourceType: 'purchase',
    sourceId: purchaseRef,
    memo: purchaseRef,
  });

  await settleClawback(asModuleMoneyTx(tx), {
    ownerId: userRef,
    amountMinor: applied,
  });

  await recomputeOwnerWithdrawable(tx, userRef);
  return applied;
}

/**
 * Append an adjustment ledger row — log-only appendEntry + vesting projection for debits.
 */
export async function appendAdjustmentInTx(
  tx: HostTransaction,
  deps: HostRetainedDeps,
  args: {
    userId: string;
    amountMinor: bigint;
    sourceType: string;
    sourceId: string;
    memo?: string;
  },
): Promise<boolean> {
  const userRef = toModuleRef(asUserId(args.userId));
  const idempotencyKey = buildIdempotencyKey('adjustment', args.sourceType, args.sourceId);
  const { inserted, id } = await appendEntry(asModuleMoneyTx(tx), {
    key: idempotencyKey,
    delta: args.amountMinor,
    reason: 'adjustment',
    ref: {
      sourceType: args.sourceType,
      sourceId: args.sourceId,
      userId: userRef,
      ...(args.memo ? { memo: args.memo } : {}),
    },
  });

  if (!inserted || id === null) return false;

  await tx.insert(affiliateEntriesTable).values({
    entryId: id,
    ownerId: userRef,
    entryType: 'adjustment',
    sourceType: args.sourceType,
    sourceId: args.sourceId,
    memo: args.memo ?? null,
  });

  const lifetimeDelta = args.amountMinor > 0n ? args.amountMinor : 0n;
  if (lifetimeDelta > 0n) {
    await tx
      .insert(walletVesting)
      .values({
        ownerId: userRef,
        pendingMinor: 0n,
        maturedMinor: 0n,
        withdrawableMinor: 0n,
        lifetimeEarnedMinor: lifetimeDelta,
        updatedAt: deps.now(),
      })
      .onConflictDoUpdate({
        target: walletVesting.ownerId,
        set: {
          lifetimeEarnedMinor: sql`${walletVesting.lifetimeEarnedMinor} + ${lifetimeDelta}`,
          updatedAt: deps.now(),
        },
      });
  }

  if (args.amountMinor < 0n) {
    await settleClawback(asModuleMoneyTx(tx), {
      ownerId: userRef,
      amountMinor: -args.amountMinor,
    });
    await recomputeOwnerWithdrawable(tx, userRef);
  }

  return true;
}

/**
 * Restore withdrawable after a failed/cancelled payout debit — donor payout-debit.ts restore parity.
 */
export async function restorePayoutDebitInTx(
  tx: HostTransaction,
  deps: HostRetainedDeps,
  userId: string,
  payoutId: string,
  amountMinor: bigint,
): Promise<boolean> {
  if (amountMinor <= 0n) {
    throw new Error(`payout restoration amount must be positive; got: ${amountMinor}`);
  }

  const userRef = toModuleRef(asUserId(userId));
  const payoutRef = toModuleRef(asPayoutId(payoutId));
  const reversalKey = buildIdempotencyKey('adjustment', 'affiliate_payout_reversal', payoutRef);
  const { inserted, id } = await appendEntry(asModuleMoneyTx(tx), {
    key: reversalKey,
    delta: amountMinor,
    reason: 'adjustment',
    ref: {
      sourceType: 'affiliate_payout_reversal',
      sourceId: payoutRef,
      userId: userRef,
    },
  });

  if (!inserted || id === null) return false;

  await tx.select().from(walletVesting).where(eq(walletVesting.ownerId, userRef)).for('update');

  await tx
    .update(walletVesting)
    .set({
      maturedMinor: sql`${walletVesting.maturedMinor} + ${amountMinor}`,
      updatedAt: deps.now(),
    })
    .where(eq(walletVesting.ownerId, userRef));

  await tx
    .insert(affiliateEntriesTable)
    .values({
      entryId: id,
      ownerId: userRef,
      entryType: 'adjustment',
      sourceType: 'affiliate_payout_reversal',
      sourceId: payoutRef,
      memo: `payout_failure_restore:${payoutRef}`,
    })
    .onConflictDoNothing({ target: affiliateEntriesTable.entryId });

  await recomputeOwnerWithdrawable(tx, userRef);

  return true;
}
