/**
 * applyEffects — interprets GroupDealEffect[] and calls real side-effect helpers.
 *
 * PERSISTENCE ORDERING (critical for retry semantics):
 *   1. machine.transition(ctx, event) → nextState + effects[]
 *   2. DB write: updateGroupState (+ extraFields) — sequential await (Neon HTTP, no transactions)
 *   3. DB write: insertOutboxRow for each enqueue-outbox effect — sequential await
 *   4. enqueueOutbox for each outbox row — after DB writes
 *   5. Non-DB effects (alarm arm/disarm, execute-deal) — after DB writes
 *
 * The workflow calls applyEffects AFTER the state write succeeds. If the process
 * crashes between step 2 and step 5, the outbox row is already written so the
 * cron backstop will retry. Alarm arm/disarm idempotency is guaranteed by the DO.
 *
 * This is the ONLY place in group-deal flows that calls:
 *   - armGroupDealAlarm / disarmGroupDealAlarm
 *   - insertOutboxRow / enqueueOutbox
 *   - executeGroupDeal (for execute-deal effects)
 *   - releaseGroupDealReservations
 */

import { insertOutboxRow } from '@/server/db/queries/outbox.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import type { GroupDealEffect } from './effects.js';
import type { DrizzleClient } from '@/server/db/client.js';
import type { MultidealEnv } from '@/server/env.js';
import type { DoClient } from '@/server/services/types.js';
import { captureCaught } from '@/server/observability/capture.server.js';

export interface ApplyEffectsContext {
  db: DrizzleClient;
  env: MultidealEnv;
  doClient: DoClient;
  /** Caller supplies executeGroupDeal to avoid circular import. */
  executeGroupDeal: (groupDealId: string) => Promise<unknown>;
  /** Caller supplies releaseGroupDealReservations. */
  releaseGroupDealReservations: (groupDealId: string) => Promise<void>;
}

/**
 * Applies a list of effects produced by machine.transition.
 * Called by workflows AFTER the DB state write.
 *
 * Ordering within this function:
 *   1. All enqueue-outbox effects (DB inserts + queue sends)
 *   2. Alarm arm/disarm (non-DB, idempotent)
 *   3. release-reservations
 *   4. execute-deal (last — most complex, catches own errors)
 */
export async function applyEffects(
  ctx: ApplyEffectsContext,
  effects: GroupDealEffect[],
): Promise<void> {
  const { db, doClient, executeGroupDeal, releaseGroupDealReservations } = ctx;

  // 1. Outbox inserts + queue sends (must come before non-DB side effects so
  //    the event is durable even if a later effect fails).
  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);
    }
  }

  // 2. Alarm arm/disarm
  for (const effect of effects) {
    if (effect.kind === 'arm-alarm') {
      try {
        await doClient.armGroupDealAlarm(effect.groupDealId, effect.at, effect.alarmType);
      } catch (err) {
        captureCaught(err, {
          scope: 'armGroupDealAlarm.effect',
          extra: { groupDealId: effect.groupDealId },
        });
      }
    } else if (effect.kind === 'disarm-alarm') {
      try {
        await doClient.disarmGroupDealAlarm(effect.groupDealId);
      } catch (err) {
        captureCaught(err, {
          scope: 'disarmGroupDealAlarm.effect',
          extra: { groupDealId: effect.groupDealId },
        });
      }
    }
  }

  // 3. Release reservations (before execute-deal, in case execute is skipped)
  for (const effect of effects) {
    if (effect.kind === 'release-reservations') {
      await releaseGroupDealReservations(effect.groupDealId);
    }
  }

  // 4. Execute deal (last — has its own error handling)
  for (const effect of effects) {
    if (effect.kind === 'execute-deal') {
      await executeGroupDeal(effect.groupDealId);
    }
  }
}
