/**
 * applyEffects — interprets GroupReservationEffect[] and calls real side-effect helpers.
 *
 * PERSISTENCE ORDERING (critical for retry semantics):
 *   1. mark-cancelled (DB update reservation.status)
 *   2. release-hold (best-effort provider call — no DB write)
 *   3. decrement-count (DB update group_deal counter)
 *   4. record-threshold-met / record-threshold-lost (delegate via callbacks)
 *   5. promote-waitlist (DB update — pops next entry)
 *   6. enqueue-outbox per effect (DB insert + queue send)
 *   7. send-user-push / send-vendor-push (best-effort external I/O)
 *
 * This is the ONLY place in group-reservation flows that calls:
 *   - groupReservationQueries.updateStatus / groupDealQueries.decrementReservationCount
 *   - groupWaitlistQueries.promoteNext
 *   - insertOutboxRow / enqueueOutbox
 *   - getPaymentProvider(env).releaseHold
 *   - push.sendToUser / push.sendToVendor
 */

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 * as groupReservationQueries from '@/server/db/queries/group-reservations.js';
import * as groupDealQueries from '@/server/db/queries/group-deals.js';
import * as groupWaitlistQueries from '@/server/db/queries/group-waitlist.js';
import { getPaymentProvider } from '@/server/payments/get-provider.js';
import { env } from '@/server/env.js';
import type { PushClient } from '@/server/push/types.js';
import type { DoClient } from '@/server/services/types.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import type { GroupReservationEffect } from './effects.js';

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

export interface ApplyEffectsContext {
  db: DrizzleClient;
  push: PushClient;
  doClient: DoClient;
  /**
   * Callers supply these to avoid circular import with workflows/group-deal.ts.
   * group-reservation is forbidden to modify domain/group-deal/** — these
   * public-API callbacks let the executor delegate group-deal transitions
   * without depending on its internals.
   */
  recordThresholdMet: (
    db: DrizzleClient,
    doClient: DoClient,
    groupDealId: string,
    dealId: string,
    vendorId: string,
    newCount: number,
  ) => Promise<void>;
  recordThresholdLost: (
    db: DrizzleClient,
    doClient: DoClient,
    groupDealId: string,
    dealId: string,
    vendorId: string,
    newCount: number,
    minGroupSize: number,
  ) => Promise<void>;
}

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

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

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

export async function applyEffects(
  ctx: ApplyEffectsContext,
  effects: GroupReservationEffect[],
): Promise<void> {
  const { db, push, doClient, recordThresholdMet, recordThresholdLost } = ctx;

  // 1. mark-cancelled
  for (const effect of effects) {
    if (effect.kind === 'mark-cancelled') {
      await groupReservationQueries.updateStatus(db, effect.reservationId, 'CANCELLED');
    }
  }

  // 2. release-hold (best-effort Stripe PI cancel, fire-and-forget)
  for (const effect of effects) {
    if (effect.kind === 'release-hold') {
      try {
        await (
          await getPaymentProvider(env)
        ).releaseHold({
          reservationId: effect.reservationId,
          providerHoldId: effect.providerHoldId,
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'server.domain.group-reservation.apply-effects',
          severity: 'warning',
        });
      }
    }
  }

  // 3. decrement-count
  for (const effect of effects) {
    if (effect.kind === 'decrement-count') {
      await groupDealQueries.decrementReservationCount(db, effect.groupDealId, effect.quantity);
    }
  }

  // 4. record-threshold-met / record-threshold-lost (delegate)
  for (const effect of effects) {
    if (effect.kind === 'record-threshold-met') {
      await recordThresholdMet(
        db,
        doClient,
        effect.groupDealId,
        effect.dealId,
        effect.vendorId,
        effect.newCount,
      );
    } else if (effect.kind === 'record-threshold-lost') {
      await recordThresholdLost(
        db,
        doClient,
        effect.groupDealId,
        effect.dealId,
        effect.vendorId,
        effect.newCount,
        effect.minGroupSize,
      );
    }
  }

  // 5. promote-waitlist
  for (const effect of effects) {
    if (effect.kind === 'promote-waitlist') {
      await groupWaitlistQueries.promoteNext(db, effect.groupDealId);
    }
  }

  // 6. enqueue-outbox
  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);
    }
  }

  // 7. send-user-push / send-vendor-push
  for (const effect of effects) {
    if (effect.kind === 'send-user-push') {
      try {
        await push.sendToUser(effect.userId, {
          title: effect.title,
          body: effect.body,
          url: effect.url,
          tag: effect.tag,
          data: effect.data,
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'server.domain.group-reservation.apply-effects',
          severity: 'warning',
        });
      }
    } else if (effect.kind === 'send-vendor-push') {
      try {
        await push.sendToVendor(effect.vendorId, {
          title: effect.title,
          body: effect.body,
          url: effect.url,
          tag: effect.tag,
          data: effect.data,
        });
      } catch (err) {
        captureCaught(err, {
          scope: 'server.domain.group-reservation.apply-effects',
          severity: 'warning',
        });
      }
    }
  }

  // Exhaustiveness guard
  for (const effect of effects) {
    switch (effect.kind) {
      case 'mark-cancelled':
      case 'release-hold':
      case 'decrement-count':
      case 'record-threshold-met':
      case 'record-threshold-lost':
      case 'promote-waitlist':
      case 'enqueue-outbox':
      case 'send-user-push':
      case 'send-vendor-push':
        break;
      default:
        assertNever(effect);
    }
  }
}
