/**
 * Cron dispatcher — invoked by the worker's `scheduled()` handler.
 *
 * Cloudflare cron triggers fire on this worker (multideal-preview); the custom
 * entry `src/worker.ts` exposes a `scheduled` export that calls
 * `dispatchCron(event.cron, env, ctx)` directly.
 *
 * Cron → handler mapping (mirrors wrangler.toml triggers):
 *   every 30 min    → frequent bundle incl. process-llm-jobs + process-translation-jobs backstops
 *   0 * * * *       → sold-out-gold-expire
 *   0 0 * * *       → midnight bundle (purge-stale-drafts, session-cleanup, audience-sync, …)
 *   0 6 * * *       → marketing-lifecycle + vendor-lifecycle
 *
 * LLM jobs: queue consumer is primary; 30-min cron backstop reaps stuck RUNNING,
 * drains lost queue deliveries (MAX_JOBS_PER_RUN=1), and runs SLA/retry steps.
 */

// All cron handler imports are dynamic (inside bundle handlers) to keep
// them out of the cold-start module graph. Only type imports are static.
import { scheduleRegistry } from '../platform/schedule-registry.js';
import type { ReconciliationEnv } from './alarm-reconciliation.js';
import type { CronEnv } from './deal-expiry.js';
import type { DailyNotifStatsEnv } from './daily-notif-stats.js';
import type { MultidealEnv } from '../env.js';
import type { MonitorEnv } from '../monitor/types.js';
import { getDb, warmDb } from '../do-host/lib/db.js';

// ---------------------------------------------------------------------------
// Scheduled event handler (for use in a Cloudflare Worker entry point)
// ---------------------------------------------------------------------------

export interface ScheduledEvent {
  scheduledTime: number;
  cron: string;
}

export async function scheduled(
  event: ScheduledEvent,
  env: CronEnv,
  ctx: { waitUntil: (p: Promise<unknown>) => void },
): Promise<void> {
  const handler = dispatchCron(event.cron, env);
  ctx.waitUntil(handler);
  await handler;
}

export async function dispatchScheduled(
  event: ScheduledController,
  env: Env,
  ctx: ExecutionContext,
): Promise<void> {
  const md = env as unknown as MultidealEnv;
  const db = getDb({ DATABASE_URL: md.DATABASE_URL });
  await warmDb(db);
  const handler = dispatchCron(event.cron, {
    ...(env as unknown as Parameters<typeof dispatchCron>[1]),
    db,
  });
  ctx.waitUntil(handler);
  await handler;
}

// ---------------------------------------------------------------------------
// Dispatcher: maps cron expression → handler
// ---------------------------------------------------------------------------

async function recordBundleHeartbeat(
  env: CronEnv,
  cronKey: string,
  expectedMin: number,
  results: PromiseSettledResult<unknown>[],
) {
  try {
    const { getDb } = await import('../db/client.js');
    const { recordCronSuccess, recordCronError } = await import('../db/queries/cron-runs.js');
    const db = getDb({ DATABASE_URL: env.DATABASE_URL });
    const now = new Date();
    const firstReject = results.find((r) => r.status === 'rejected') as
      | PromiseRejectedResult
      | undefined;
    if (firstReject)
      await recordCronError(
        db,
        cronKey,
        expectedMin,
        String(firstReject.reason).slice(0, 500),
        now,
      );
    else await recordCronSuccess(db, cronKey, expectedMin, now);
  } catch (err) {
    const { redactError } = await import('../monitor/redact.js'); // dynamic — match file's dynamic-only convention
    console.error(
      '[cron] heartbeat record failed',
      cronKey,
      redactError(err, 'heartbeat record failed'),
    ); // never log raw err — DB error can carry the DSN (Hard Rule 8)
  }
}

export async function dispatchCron(cron: string, env: CronEnv): Promise<void> {
  const entry = scheduleRegistry.find(
    (candidate) => candidate.trigger.kind === 'cron' && candidate.trigger.expr === cron,
  );
  if (!entry || typeof entry.handler !== 'string') {
    throw new Error(`No cron handler registered for ${cron}`);
  }
  return (
    cronHandlers[entry.handler]?.(env) ??
    Promise.reject(new Error(`Unknown cron handler ${entry.handler}`))
  );
}

const cronHandlers: Record<string, (env: CronEnv) => Promise<void>> = {
  runFrequentBundle,
  runMidnightBundle,
  runHourlyBundle,
  runLifecycleBundle,
};

async function runFrequentBundle(env: CronEnv): Promise<void> {
  // Frequent bundle — LLM backstop is 1 job/run (reaper + SLA); queue consumer is primary.
  const [
    { runDealExpiry },
    { runGroupDealDeadline },
    { runScheduledPublish },
    { runPersonalDealTimeout },
    { runProcessOutbox },
    { runGroupPartialTimeout },
    { runProcessLlmJobs },
    { runProcessTranslationJobs },
    { runReturnsSla },
    { tickFrequentSettlement },
    { runMaturationSweep },
    { runAffiliateTick },
    { runWarmLayout },
    { runBackInStockSweep },
    { toInventoryDb, sweepStaleReservations },
    { runWarmRoutes },
    { runRefundIntentsSweep },
    { completeFulfilledOrders },
  ] = await Promise.all([
    import('./deal-expiry.js'),
    import('./group-deal-deadline.js'),
    import('./scheduled-publish.js'),
    import('./personal-deal-timeout.js'),
    import('./process-outbox.js'),
    import('./group-partial-timeout.js'),
    import('./process-llm-jobs.js'),
    import('./process-translation-jobs.js'),
    import('./returns-sla.js'),
    import('./tick-frequent-settlement.js'),
    import('./maturation-sweep.js'),
    import('./affiliate-tick.js'),
    import('./warm-layout.js'),
    import('./back-in-stock-sweep.js'),
    import('../stock/inventory-platform.js'),
    import('./warm-routes.js'),
    import('./refund-intents-sweep.js'),
    import('./complete-fulfilled-orders.js'),
  ]);
  return Promise.allSettled([
    runDealExpiry(env),
    runGroupDealDeadline(env),
    runScheduledPublish(env),
    runPersonalDealTimeout(env),
    runProcessOutbox(env),
    runGroupPartialTimeout(env),
    runProcessLlmJobs(env),
    runProcessTranslationJobs(env),
    runReturnsSla(env),
    tickFrequentSettlement(env),
    runMaturationSweep(env),
    runAffiliateTick(env),
    runWarmLayout(env),
    runBackInStockSweep(env),
    (async () => {
      const { getDb } = await import('../db/client.js');
      const db = getDb({ DATABASE_URL: env.DATABASE_URL });
      await sweepStaleReservations(toInventoryDb(db), new Date());
    })(),
    runWarmRoutes(env),
    runRefundIntentsSweep(env as unknown as MultidealEnv),
    completeFulfilledOrders(env as unknown as MultidealEnv),
  ]).then(async (results) => {
    for (const r of results) {
      if (r.status === 'rejected') console.error('[cron/*/30] handler rejected:', r.reason);
    }
    await recordBundleHeartbeat(env, 'cron-30min', 30, results);
  });
}

async function runMidnightBundle(env: CronEnv): Promise<void> {
  // Midnight UTC mega-bundle — see header comment for full list.
  const [
    { runPurgeStaleDrafts },
    { runPlaceJ5 },
    { runSessionCleanup },
    { runAlarmReconciliation },
    { runVendorRecommendationsCron },
    { runRateLimitGc },
    { runRefreshAddressCache },
    { runEmailVerificationsSweep },
    { runDailyNotifStats },
    { runGroupHoldRefresh },
    { runAttachmentsGc },
    { sweepStuckShipments, detectLostShipments },
    { reconcileVendorBalances },
    { runAudienceSync },
    { tickOpsDigest },
  ] = await Promise.all([
    import('./purge-stale-drafts.js'),
    import('./place-j5.js'),
    import('./session-cleanup.js'),
    import('./alarm-reconciliation.js'),
    import('../ai/vendor-recommendations.js'),
    import('./rate-limit-gc.js'),
    import('./refresh-address-cache.js'),
    import('./email-verifications-sweep.js'),
    import('./daily-notif-stats.js'),
    import('./group-hold-refresh.js'),
    import('./attachments-gc.js'),
    import('../workflows/shipment-sweeps.js'),
    import('../workflows/settlement-reconcile.js'),
    import('./audience-sync.js'),
    import('../monitor/digest.js'),
  ]);
  return Promise.allSettled([
    runPurgeStaleDrafts(env),
    runPlaceJ5(env),
    runSessionCleanup(env),
    runAlarmReconciliation(env as unknown as ReconciliationEnv),
    runVendorRecommendationsCron(env),
    runRateLimitGc(env),
    runRefreshAddressCache(env),
    runEmailVerificationsSweep(env),
    runDailyNotifStats(env as unknown as DailyNotifStatsEnv),
    runGroupHoldRefresh(env),
    runAttachmentsGc(env),
    sweepStuckShipments(env as unknown as MultidealEnv),
    detectLostShipments(env as unknown as MultidealEnv),
    reconcileVendorBalances(env as unknown as MultidealEnv),
    runAudienceSync(env),
    tickOpsDigest(env as unknown as MonitorEnv),
  ]).then(async (results) => {
    for (const r of results) {
      if (r.status === 'rejected') console.error('[cron/0 0] handler rejected:', r.reason);
    }
    await recordBundleHeartbeat(env, 'cron-midnight', 1440, results);
  });
}

async function runHourlyBundle(env: CronEnv): Promise<void> {
  const { runSoldOutGoldExpire } = await import('./sold-out-gold-expire.js');
  await runSoldOutGoldExpire(env);
  try {
    const { tickOpsMonitor } = await import('../monitor/orchestrator.js');
    await tickOpsMonitor(env as unknown as MonitorEnv); // env superset satisfies MonitorEnv
  } catch (err) {
    const { redactError } = await import('../monitor/redact.js'); // dynamic — match file convention
    console.error('[cron/0 * * *] ops-monitor rejected:', redactError(err, 'ops-monitor rejected')); // never log raw err — DB error can carry the DSN
  }
}

async function runLifecycleBundle(env: CronEnv): Promise<void> {
  const [{ runMarketingLifecycle }, { runVendorLifecycle }] = await Promise.all([
    import('./marketing-lifecycle.js'),
    import('./vendor-lifecycle.js'),
  ]);
  return Promise.allSettled([runMarketingLifecycle(env), runVendorLifecycle(env)]).then(
    async (results) => {
      for (const r of results) {
        if (r.status === 'rejected') console.error('[cron/0 6] handler rejected:', r.reason);
      }
      await recordBundleHeartbeat(env, 'cron-0600', 1440, results);
    },
  );
}
