/**
 * Cron: Marketing Lifecycle Sweep — runs daily at 06:00 UTC.
 *
 * Sweeps for buyer candidates qualifying for lifecycle email triggers and
 * enqueues outbox rows for each. Uses cursor pagination (PAGE_SIZE per batch)
 * to stay within CF Worker CPU limits.
 *
 * Triggers covered:
 *   1. buyer.welcome.day0  — new buyers who have never received the welcome email
 *   2. buyer.welcome.day3  — buyers 3+ days old with no purchase and no day-3 email
 *   3. buyer.digest.weekly — all opted-in buyers (6-day cooldown in query layer)
 */

import { createDbService } from '@/server/services/db.js';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import { insertOutboxRow } from '../db/queries/outbox.js';
import { enqueueOutbox } from '../queues/outbox-producer.js';
import {
  getCursor,
  setCursor,
  getBuyerWelcomeDay0Candidates,
  getBuyerWelcomeDay3Candidates,
  getBuyerWeeklyDigestCandidates,
  hasTriggered,
  recordTriggered,
} from '../db/queries/marketing.js';
import type { CronEnv } from './deal-expiry.js';

const PAGE_SIZE = 100;

export const runMarketingLifecycle = withSentry(
  async function runMarketingLifecycle(env: CronEnv): Promise<void> {
    if (!env.RESEND_MARKETING_FROM_EMAIL) {
      return;
    }

    const db = env.db ?? createDbService({ DATABASE_URL: env.DATABASE_URL });

    // ── Welcome day-0 sweep ──────────────────────────────────────────────────
    try {
      let cursor = await getCursor(db, 'marketing.sweep.welcome.day0');
      while (true) {
        const batch = await getBuyerWelcomeDay0Candidates(db, cursor, PAGE_SIZE);
        if (!batch.length) break;
        for (const c of batch) {
          if (c.notifPrefs?.marketing?.all !== true) continue;
          if (await hasTriggered(db, c.userId, 'buyer.welcome.day0')) continue;
          await recordTriggered(db, c.userId, 'buyer.welcome.day0');
          const { id: outboxId } = await insertOutboxRow(db, {
            aggregateType: 'user',
            aggregateId: c.userId,
            eventType: 'buyer.welcome.day0',
            payload: { userId: c.userId },
          });
          await enqueueOutbox(outboxId);
        }
        cursor = batch[batch.length - 1]!.userId;
        await setCursor(db, 'marketing.sweep.welcome.day0', cursor);
        if (batch.length < PAGE_SIZE) break;
      }
    } catch (err) {
      captureCaught(err, { scope: 'server.cron.marketing-lifecycle.day0', severity: 'warning' });
    }

    // ── Welcome day-3 sweep ──────────────────────────────────────────────────
    try {
      let cursor = await getCursor(db, 'marketing.sweep.welcome.day3');
      while (true) {
        const batch = await getBuyerWelcomeDay3Candidates(db, cursor, PAGE_SIZE);
        if (!batch.length) break;
        for (const c of batch) {
          if (c.notifPrefs?.marketing?.all !== true) continue;
          if (await hasTriggered(db, c.userId, 'buyer.welcome.day3')) continue;
          await recordTriggered(db, c.userId, 'buyer.welcome.day3');
          const { id: outboxId } = await insertOutboxRow(db, {
            aggregateType: 'user',
            aggregateId: c.userId,
            eventType: 'buyer.welcome.day3',
            payload: { userId: c.userId, deals: [] },
          });
          await enqueueOutbox(outboxId);
        }
        cursor = batch[batch.length - 1]!.userId;
        await setCursor(db, 'marketing.sweep.welcome.day3', cursor);
        if (batch.length < PAGE_SIZE) break;
      }
    } catch (err) {
      captureCaught(err, { scope: 'server.cron.marketing-lifecycle.day3', severity: 'warning' });
    }

    // ── Weekly digest sweep ──────────────────────────────────────────────────
    try {
      const weekStr = getISOWeekStr(new Date());
      let cursor = await getCursor(db, `marketing.sweep.digest.${weekStr}`);
      while (true) {
        const batch = await getBuyerWeeklyDigestCandidates(db, cursor, PAGE_SIZE);
        if (!batch.length) break;
        for (const c of batch) {
          if (c.notifPrefs?.marketing?.all !== true) continue;
          if (await hasTriggered(db, c.userId, 'buyer.digest.weekly')) continue;
          await recordTriggered(db, c.userId, 'buyer.digest.weekly');
          const { id: outboxId } = await insertOutboxRow(db, {
            aggregateType: 'user',
            aggregateId: c.userId,
            eventType: 'buyer.digest.weekly',
            payload: { userId: c.userId, deals: [], weekStr },
          });
          await enqueueOutbox(outboxId);
        }
        cursor = batch[batch.length - 1]!.userId;
        await setCursor(db, `marketing.sweep.digest.${weekStr}`, cursor);
        if (batch.length < PAGE_SIZE) break;
      }
    } catch (err) {
      captureCaught(err, {
        scope: 'server.cron.marketing-lifecycle.digest',
        severity: 'warning',
      });
    }
  },
  { name: 'cron.marketing-lifecycle', kind: 'cron' },
);

// ── ISO week string helper (no external dep) ─────────────────────────────────

function getISOWeekStr(date: Date): string {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
  return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
}
