/**
 * applyEffects — interprets CartCheckoutEffect[] inside the workflow tx.
 *
 * PERSISTENCE ORDERING (critical for retry semantics):
 *   1. reserve-line × N (DB insert purchase + decrement deal stock)
 *      After all lines for a vendor: createOrder + claimForCharge (T2 write-path).
 *   2. clear-cart (DB delete cart_items for user)
 *   3. enqueue-outbox per outbox row id (AFTER tx commit — caller invokes)
 *
 * T2: After all reserve-line writes in a vendor group, createOrder + claimForCharge
 *     are called inside the same transaction to produce order + order_line + vendor_split
 *     rows alongside the existing purchases rows (T7 handles cutover).
 *
 * Note: caller MUST pass `tx` so all reserve-line writes and clear-cart are
 * inside the same Drizzle transaction as the validation FOR UPDATE locks.
 *
 * Returns { orders, outboxIds } so the orchestrator can:
 *   - finalize each purchase (QR generation + PENDING→COMPLETED) via the W1
 *     purchase machine
 *   - enqueue all outbox rows after the tx commits
 */

import { createHash } from 'node:crypto';
import { and, eq, sql } from 'drizzle-orm';
import type { DrizzleClient } from '@/server/db/client.js';
import { dealSkus } from '@/server/db/schema.js';
import { clearCart } from '@/server/db/queries/cart.js';
import { refreshDealCache } from '@/server/domain/variants/cache.js';
import { createOrder, claimForCharge, type OrdersSchema } from '@platform-modules/commerce-orders';
import type { Transaction } from '@platform-modules/db';
import type { CartCheckoutEffect } from './effects.js';

// ─── Context ─────────────────────────────────────────────────────────────────

export interface ApplyEffectsContext {
  /** MUST be the tx handle to keep writes inside the validation transaction. */
  tx: DrizzleClient;
  userId: string;
  paymentMethodId?: string;
  /** Cart session idempotency key — used to derive per-vendor order idem key. */
  cartSessionId: string;
}

export interface OrderCreated {
  orderId: string;
  vendorId: string;
  /** Order total in agorot (Σ unitPrice * qty per line — adjusted for integer division). */
  total: bigint;
  orderLineIds: string[];
  dealIds: string[];
  firstOrderLineId: string;
  firstDealId: string;
}

export interface ApplyEffectsResult {
  orders: OrderCreated[];
  outboxIds: string[];
}

// ─── assertNever ─────────────────────────────────────────────────────────────

function assertNever(x: never): never {
  throw new Error(`Unhandled CartCheckoutEffect kind: ${JSON.stringify(x)}`);
}

// ─── Executor ────────────────────────────────────────────────────────────────

export async function applyEffects(
  ctx: ApplyEffectsContext,
  effects: CartCheckoutEffect[],
): Promise<ApplyEffectsResult> {
  const { tx, userId, cartSessionId } = ctx;
  const orders: OrderCreated[] = [];
  const outboxIds: string[] = [];

  // ── Group reserve-line effects by vendorId (stable insertion order) ────────
  const vendorOrder: string[] = [];
  const vendorGroups = new Map<string, (CartCheckoutEffect & { kind: 'reserve-line' })[]>();

  for (const effect of effects) {
    if (effect.kind === 'reserve-line') {
      if (!vendorGroups.has(effect.vendorId)) {
        vendorOrder.push(effect.vendorId);
        vendorGroups.set(effect.vendorId, []);
      }
      vendorGroups.get(effect.vendorId)!.push(effect);
    }
  }

  // ── Process each vendor group ─────────────────────────────────────────────
  for (const vendorId of vendorOrder) {
    const groupEffects = vendorGroups.get(vendorId)!;

    // Sort by dealSkuId ascending for stable idempotency key.
    // Nulls sort last (legacy pre-variant items).
    const sortedEffects = [...groupEffects].sort((a, b) => {
      if (!a.dealSkuId && !b.dealSkuId) return 0;
      if (!a.dealSkuId) return 1;
      if (!b.dealSkuId) return -1;
      return a.dealSkuId.localeCompare(b.dealSkuId);
    });

    const groupDealIds: string[] = [];

    // ── Per-line: CAS stock decrement ─────────────────────────────────────
    for (const effect of sortedEffects) {
      groupDealIds.push(effect.dealId);

      // dealSkuId is required post-contract. Null = data error → throw immediately.
      if (effect.dealSkuId === null) {
        throw new Error(`OVERSELL:no_sku — dealSkuId missing for deal ${effect.dealId}`);
      }

      // SKU-level CAS decrement — prevents oversell atomically.
      const dec = await tx
        .update(dealSkus)
        .set({ quantitySold: sql`${dealSkus.quantitySold} + ${effect.qty}` })
        .where(
          and(
            eq(dealSkus.id, effect.dealSkuId),
            sql`${dealSkus.quantitySold} + ${effect.qty} <= ${dealSkus.quantityTotal}`,
          ),
        )
        .returning({ id: dealSkus.id });

      if (dec.length === 0) {
        throw new Error(`OVERSELL:${effect.dealSkuId}`);
      }

      // Refresh denormalised deal-level aggregate columns (min/max price, stock_remaining).
      await refreshDealCache(tx, effect.dealId);
    }

    // ── Compute order financials ─────────────────────────────────────────────
    // T2: these run alongside purchases writes — not replacing them.
    // adjustedLineTotal = unitPrice * qty ensures assertLineTotals invariant
    // (unitPrice is integer-divided so the product may differ from raw agorot
    // by at most qty-1 agorot per line — acceptable rounding).
    let commissionTotal = 0n;
    const lines = sortedEffects.map((effect) => {
      const rawLineTotal = BigInt(Math.round(parseFloat(effect.amountPaid) * 100));
      const unitPrice = rawLineTotal / BigInt(effect.qty);
      const adjustedLineTotal = unitPrice * BigInt(effect.qty);
      commissionTotal += BigInt(Math.round(parseFloat(effect.commissionAmount) * 100));
      return {
        variantId: effect.dealSkuId!,
        kind: 'voucher' as const,
        qty: effect.qty,
        unitPrice,
        lineTotal: adjustedLineTotal,
        currency: 'ILS',
        vendorId,
      };
    });

    const subtotal = lines.reduce((acc, l) => acc + l.lineTotal, 0n);
    const total = subtotal; // tax = 0n, discount = 0n → total = subtotal
    const vendorSplitAmount = total - commissionTotal;
    if (vendorSplitAmount < 0n) {
      throw new Error(
        `[apply-effects] vendorSplitAmount negative (commission ${commissionTotal} > total ${total}) for vendor ${vendorId}`,
      );
    }

    // Per-vendor idempotency key — stable across retries regardless of line order.
    const idempotencyKey = createHash('sha256')
      .update(`cart:${userId}:${cartSessionId}:vendor:${vendorId}`)
      .digest('hex');

    // ── createOrder + claimForCharge (T2 write-path) ─────────────────────────
    const orderTx = tx as unknown as Transaction<OrdersSchema>;
    const newOrder = await createOrder(orderTx, {
      idempotencyKey,
      buyerRef: { userId },
      currency: 'ILS',
      priceMode: 'inclusive',
      subtotal,
      tax: 0n,
      discount: 0n,
      total,
      lines,
      splits: [
        { vendorId, amount: vendorSplitAmount, funder: 'vendor' },
        { vendorId: null, amount: commissionTotal, funder: 'platform' },
      ],
    });
    await claimForCharge(orderTx, newOrder.id);

    const orderLineIds = newOrder.lines.map((l) => l.id);
    const firstOrderLineId = newOrder.lines[0]?.id ?? '';
    const firstDealId = groupDealIds[0] ?? '';

    orders.push({
      orderId: newOrder.id,
      vendorId,
      total,
      orderLineIds,
      dealIds: groupDealIds,
      firstOrderLineId,
      firstDealId,
    });
  }

  // ── clear-cart ────────────────────────────────────────────────────────────
  for (const effect of effects) {
    if (effect.kind === 'clear-cart') {
      await clearCart(tx, effect.userId);
    }
  }

  // ── assertNever guard — catches any future effect kind not handled above ──
  for (const effect of effects) {
    switch (effect.kind) {
      case 'reserve-line':
      case 'clear-cart':
        break;
      default:
        assertNever(effect);
    }
  }

  return { orders, outboxIds };
}
