/**
 * applyEffects — interprets VendorDealEffect[] and calls real side-effect helpers.
 *
 * PERSISTENCE ORDERING (critical for retry semantics):
 *   1. set-deal-state (DB update — dealState (+ optional contentHash, approvedAt, approvedBy))
 *   2. enqueue-llm-job (DB insert llm_jobs + LLM_JOBS_QUEUE send)
 *   3. arm-deal-alarm (idempotent CF-DO call)
 *
 * Side effects are pinned to this module — it is the ONLY place in vendor-deal
 * flows that calls:
 *   - deals UPDATE (state transitions)
 *   - enqueueLlmJob + sendLlmJobToQueue
 *   - armDealAlarm
 *
 * Returns the updated deal row (when set-deal-state fires) so the orchestrator
 * can echo the post-transition row back to API callers.
 */

import { eq } from 'drizzle-orm';
import type { DrizzleClient } from '@/server/db/client.js';
import { deals } from '@/server/db/schema.js';
import { enqueueLlmJob } from '@/server/ai/llm.js';
import { sendLlmJobToQueue } from '@/server/queues/llm-jobs-producer.js';
import type { JobRunnerNudgeCtx } from '@/server/queues/llm-jobs-producer.js';
import { armDealAlarm } from '@/server/do-client.js';
import { env } from '@/server/env.js';
import { invalidateCatalog } from '@/server/cache/invalidate.js';
import { enqueueDealTranslation } from '@/server/translation/jobs/enqueue.js';
import type { VendorDealEffect } from './effects.js';
import { captureCaught } from '@/server/observability/capture.server.js';

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

export interface ApplyEffectsContext {
  db: DrizzleClient;
  waitUntil?: JobRunnerNudgeCtx['waitUntil'];
}

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

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

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

/**
 * Returns the updated deal row when set-deal-state fires (otherwise null).
 * Orchestrator passes this back to the caller to preserve old function shapes
 * (createDeal/pauseDeal/etc. all return the row).
 */
export async function applyEffects(
  ctx: ApplyEffectsContext,
  effects: VendorDealEffect[],
): Promise<typeof deals.$inferSelect | null> {
  const { db } = ctx;
  let updatedRow: typeof deals.$inferSelect | null = null;

  // 1. set-deal-state
  for (const effect of effects) {
    if (effect.kind === 'set-deal-state') {
      const patch: Partial<typeof deals.$inferInsert> = { dealState: effect.nextState };
      if (effect.contentHash !== undefined) patch.contentHash = effect.contentHash;
      if (effect.approvedAt !== undefined) patch.approvedAt = effect.approvedAt;
      if (effect.approvedBy !== undefined) patch.approvedBy = effect.approvedBy;

      const [row] = await db
        .update(deals)
        .set(patch)
        .where(eq(deals.id, effect.dealId))
        .returning();
      updatedRow = row ?? null;
      await invalidateCatalog(db, { scope: 'deal', dealId: effect.dealId });
      if (effect.nextState === 'ACTIVE') {
        await enqueueDealTranslation(db, effect.dealId).catch((e: unknown) =>
          console.error('[apply-effects] enqueueDealTranslation failed:', e),
        );
      }
    }
  }

  // 2. enqueue-llm-job
  for (const effect of effects) {
    if (effect.kind === 'enqueue-llm-job') {
      const jobId = await enqueueLlmJob(db, {
        jobType: 'DEAL_MODERATION',
        targetId: effect.dealId,
        targetType: 'DEAL',
        inputPayload: {
          dealId: effect.dealId,
          vendorId: effect.vendorId,
          title: effect.inputs.title,
          description: effect.inputs.description,
          categoryId: effect.inputs.categoryId,
          dealType: effect.inputs.dealType,
          originalPrice: effect.inputs.originalPrice,
          discountedPrice: effect.inputs.discountedPrice,
          discountPercent: effect.inputs.discountPercent,
          primaryImageKey: effect.inputs.primaryImageKey,
        },
      });
      await sendLlmJobToQueue(
        env,
        jobId,
        ctx.waitUntil ? { waitUntil: (promise) => ctx.waitUntil?.(promise) } : undefined,
      );
    }
  }

  // 3. arm-deal-alarm
  for (const effect of effects) {
    if (effect.kind === 'arm-deal-alarm') {
      try {
        await armDealAlarm(env, effect.dealId, effect.windowEnd);
      } catch (err) {
        captureCaught(err, { scope: 'armDealAlarm.effect', extra: { dealId: effect.dealId } });
      }
    }
  }

  // Exhaustiveness guard
  for (const effect of effects) {
    switch (effect.kind) {
      case 'set-deal-state':
      case 'enqueue-llm-job':
      case 'arm-deal-alarm':
        break;
      default:
        assertNever(effect);
    }
  }

  return updatedRow;
}
