/**
 * applyEffects — interprets PurchaseEffect[] and calls real side-effect helpers.
 *
 * PERSISTENCE ORDERING (critical for retry semantics):
 *   1. decide(ctx, event) → nextState + effects[]  [done by orchestrator before calling applyEffects]
 *   2. DB write: complete-payment (update users) OR delete-pending (no-op after T7)
 *   3. DB write: insertOutboxRow for each enqueue-outbox effect
 *   4. enqueueOutbox for each outbox row
 *   5. check-sold-out (DB read+conditional write)
 *   6. send-vendor-push (non-idempotent external I/O, best-effort)
 *
 * This is the ONLY place in purchase flows that calls:
 *   - insertOutboxRow / enqueueOutbox
 *   - checkAndMarkSoldOut
 *   - push.sendToVendor
 */

import { eq, sql } from 'drizzle-orm';
import type { DrizzleClient } from '@/server/db/client.js';
import { insertOutboxRow } from '@/server/db/queries/outbox.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import { users } from '@/server/db/schema.js';
import type { PushClient } from '@/server/push/types.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import { refreshDealCache } from '@/server/domain/variants/cache.js';
import { formatShekelFloat } from '@/lib/money.js';
import type { PurchaseEffect } from './effects.js';

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

export interface ApplyEffectsContext {
  db: DrizzleClient;
  push: PushClient;
  /** Caller supplies checkAndMarkSoldOut to avoid circular import. */
  checkAndMarkSoldOut: (db: DrizzleClient, dealId: string) => Promise<void>;
}

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

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

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

/**
 * Applies a list of effects produced by decide().
 * Called by the workflow orchestrator AFTER the charge returns and BEFORE returning
 * to the caller (except for send-vendor-push which is best-effort).
 *
 * Ordering within this function matches the canonical persistence ordering above.
 */
export async function applyEffects(
  ctx: ApplyEffectsContext,
  effects: PurchaseEffect[],
): Promise<void> {
  const { db, push, checkAndMarkSoldOut } = ctx;

  // 1. complete-payment or delete-pending (DB state write)
  for (const effect of effects) {
    if (effect.kind === 'complete-payment') {
      // T7: purchases table removed; payment completion handled by commerce-orders.
      // Refresh deal cache and increment user purchase count as before.
      await refreshDealCache(db, effect.dealId);

      if (effect.userId) {
        await db
          .update(users)
          .set({ purchaseCount: sql`purchase_count + 1` })
          .where(eq(users.id, effect.userId));
      }
    } else if (effect.kind === 'delete-pending') {
      // T7: no-op — pending order rows are managed by commerce-orders.
    }
  }

  // 2+3. Outbox DB inserts + queue sends
  for (const effect of effects) {
    if (effect.kind === 'enqueue-outbox') {
      const { id: outboxId } = await insertOutboxRow(db, {
        aggregateType: effect.aggregateType,
        aggregateId: effect.aggregateId,
        eventType: effect.eventType,
        payload: effect.payload,
      });
      await enqueueOutbox(outboxId);
    }
  }

  // 4. Sold-out check (DB read + conditional write)
  for (const effect of effects) {
    if (effect.kind === 'check-sold-out') {
      await checkAndMarkSoldOut(db, effect.dealId);
    }
  }

  // 5. Vendor push (best-effort — must not roll back purchase on failure)
  for (const effect of effects) {
    if (effect.kind === 'send-vendor-push') {
      try {
        await push.sendToVendor(effect.vendorId, {
          title: 'מכירה חדשה!',
          body: `${effect.dealTitle} - ${formatShekelFloat(effect.amountPaid)}`,
          url: `/vendor/purchases/${effect.purchaseId}`,
          tag: 'new_sale',
          data: {
            dealTitle: effect.dealTitle,
            amount: effect.amountPaid,
            purchaseId: effect.purchaseId,
          },
        });
      } catch (err) {
        captureCaught(err, { scope: 'server.domain.purchase.apply-effects', severity: 'warning' });
      }
    }
  }

  // Exhaustiveness guard — catch unhandled effect kinds at runtime
  for (const effect of effects) {
    switch (effect.kind) {
      case 'complete-payment':
      case 'delete-pending':
      case 'enqueue-outbox':
      case 'check-sold-out':
      case 'send-vendor-push':
        // handled above
        break;
      default:
        assertNever(effect);
    }
  }
}
