/**
 * settlement-release-writer.ts
 *
 * Inserts vendor_payout_releases rows when a Stripe charge.succeeded event is
 * processed.  One row per purchaseId; amounts split proportionally to each
 * line's total (largest-remainder — no agora dropped).
 *
 * For ITEM deal types the release is held in perpetuity (2099-12-31) until the
 * delivery-confirmation workflow advances the row to 'releasing'.
 */

import { eq, inArray, sql } from 'drizzle-orm';
import type { DrizzleClient, DrizzleDb, TxDrizzleClient } from '@/server/db/client.js';
import { vendorPayoutReleases, systemConfig, order, orderLine } from '@/server/db/schema.js';
import type { DealType } from '@/lib/deal-types';
import { asOrderLineId, asVendorId, type OrderLineId } from '@/server/platform-seams/ids.js';
import {
  asSettlementReleaseCtxId,
  computeReleaseAt,
  decide,
  triggerReasonFor,
} from '@/server/domain/settlement-release/machine.js';
import type { SettlementReleaseEffect } from '@/server/domain/settlement-release/effects.js';

export { computeReleaseAt, triggerReasonFor };

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

async function getSysConfigDays(db: DrizzleClient, key: string, fallback: number): Promise<number> {
  const [row] = await db.select().from(systemConfig).where(eq(systemConfig.key, key));
  if (!row) return fallback;
  const parsed = parseInt(row.value, 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------

export interface WriteReleasesArgs {
  /** Drizzle client scoped to the request/handler. */
  db: DrizzleDb;
  paymentIntentId: string;
  chargeId: string;
  transferId: string;
  /** One purchase per cart line — one release row is created per ID. */
  purchaseIds: string[];
  vendorId: string;
  vendorAcctId: string;
  /** Total net (after Stripe fees) in agorot, split proportionally across purchaseIds. */
  netAmountAgorot: number;
  /** Platform application fee in agorot, split proportionally across purchaseIds. */
  applicationFeeAgorot: number;
  dealType: DealType;
}

export async function writeReleasesOnChargeSucceeded(args: WriteReleasesArgs): Promise<void> {
  const { db, purchaseIds } = args;
  const count = purchaseIds.length;
  if (count === 0) return;

  const holdFloorDays = await getSysConfigDays(db, 'settlement.hold_floor_days', 14);
  const heldAt = new Date();
  const decision = decide({
    ctxId: asSettlementReleaseCtxId(
      `settlement-release:${args.paymentIntentId}:${args.vendorId}:${args.chargeId}`,
    ),
    paymentIntentId: args.paymentIntentId,
    chargeId: args.chargeId,
    transferId: args.transferId,
    purchaseIds: args.purchaseIds.map((id) => asOrderLineId(id)),
    vendorId: asVendorId(args.vendorId),
    vendorAcctId: args.vendorAcctId,
    netAmountAgorot: args.netAmountAgorot,
    applicationFeeAgorot: args.applicationFeeAgorot,
    dealType: args.dealType,
    holdFloorDays,
    heldAt,
  });
  const effect = decision.effects[0];
  if (!effect) return;

  // Split proportionally to each line's total; largest-remainder assignment
  // keeps every agora accounted for (SUM(parts) === whole, exactly).
  await db.transaction(async (tx: TxDrizzleClient) => {
    const lineRows = (await tx
      .select({ id: orderLine.id, lineTotal: orderLine.lineTotal, createdAt: order.createdAt })
      .from(orderLine)
      .innerJoin(order, eq(orderLine.orderId, order.id))
      .where(inArray(orderLine.id, purchaseIds))) as Array<{
      id: string;
      lineTotal: number | string | bigint;
      createdAt: Date;
    }>;
    const totalByLine = new Map(lineRows.map((r) => [r.id, Number(r.lineTotal)]));
    const grandTotal = purchaseIds.reduce((s, id) => s + Number(totalByLine.get(id) ?? 0), 0);
    const periodMonths = new Set(
      lineRows.map((row) => new Date(row.createdAt).toISOString().slice(0, 7)),
    );

    // Sorted lock order prevents deadlock between concurrent multi-month writers
    const sortedMonths = [...periodMonths].sort();
    if (sortedMonths.length > 0) {
      await tx.execute(sql`
        SELECT pg_advisory_xact_lock(hashtext(${args.vendorId} || ':' || m.month))
        FROM unnest(ARRAY[${sql.join(
          sortedMonths.map((month) => sql`${month}`),
          sql`, `,
        )}]::text[]) AS m(month)
      `);
    }

    const splitProportional = (whole: number): number[] => {
      if (grandTotal <= 0) {
        const even = Math.floor(whole / count);
        return purchaseIds.map((_, i) => (i === count - 1 ? whole - even * (count - 1) : even));
      }
      const parts = purchaseIds.map((id) =>
        Math.floor((whole * Number(totalByLine.get(id) ?? 0)) / grandTotal),
      );
      let remainder = whole - parts.reduce((s, p) => s + p, 0);
      for (let i = 0; remainder > 0; i = (i + 1) % count, remainder--) {
        parts[i] = (parts[i] ?? 0) + 1;
      }
      return parts;
    };

    await tx
      .insert(vendorPayoutReleases)
      .values(
        applyRecordHeldRelease(
          effect,
          purchaseIds as OrderLineId[],
          splitProportional(effect.netAmountAgorot),
          splitProportional(effect.applicationFeeAgorot),
        ),
      )
      .onConflictDoNothing();
  });
}

function applyRecordHeldRelease(
  effect: SettlementReleaseEffect,
  purchaseIds: OrderLineId[],
  netParts: number[],
  feeParts: number[],
) {
  return purchaseIds.map((orderLineId, i) => ({
    orderLineId,
    vendorId: effect.vendorId,
    vendorAcctId: effect.vendorAcctId,
    paymentIntentId: effect.paymentIntentId,
    chargeId: effect.chargeId,
    transferId: effect.transferId,
    netAmountAgorot: netParts[i] ?? 0,
    applicationFeeAgorot: feeParts[i] ?? 0,
    status: 'held' as const,
    triggerReason: effect.triggerReason,
    heldAt: effect.heldAt,
    releaseAt: effect.releaseAt,
  }));
}
