/**
 * Cron: Alarm Reconciliation — runs daily at 03:00 (alongside session-cleanup + veteran-promotion).
 *
 * Scans all 5 entity types for open rows whose DO alarm is not set.
 * If unarmed: re-arms the alarm, upserts a system_health_findings row.
 * If armed: resolves any existing system_health_findings row for that entity.
 *
 * This cron runs FOREVER — it is a permanent safety net against future buggy
 * call sites that forget to arm an alarm on create/update. A non-empty finding
 * list is a bug signal, not a normal state.
 *
 * Entities scanned:
 *   deal               — dealState IN ('ACTIVE','PAUSED') AND windowEnd IS NOT NULL AND windowEnd > NOW()
 *   group_deal         — groupState IN ('COLLECTING','THRESHOLD_MET','EXTENDED','PARTIAL_PENDING') AND deadline > NOW()
 *   personal_offer     — status = 'PENDING' AND responseDeadline > NOW()
 *   scheduled_publish  — scheduledVersionId IS NOT NULL AND scheduledAt > NOW()
 *   gold_window        — soldOutGoldExpiresAt IS NOT NULL AND soldOutGoldExpiresAt > NOW()
 */

import { createDbService } from '@/server/services/db.js';
import { and, inArray, isNotNull, gt, eq, isNull } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import {
  deals,
  personalDealRequests,
  pageLayoutPointers,
  systemHealthFindings,
} from '../db/schema.js';
import { resolveFinding, touchFinding } from '../db/queries/system-health.js';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface ReconciliationEnv {
  DATABASE_URL: string;
  DEAL_DO: DurableObjectNamespace;
  GROUP_DEAL_DO: DurableObjectNamespace;
  PERSONAL_OFFER_DO: DurableObjectNamespace;
  SCHEDULED_PUBLISH_DO: DurableObjectNamespace;
  GOLD_WINDOW_DO: DurableObjectNamespace;
}

interface EntityCandidate {
  entityType: string;
  entityId: string;
  alarmAt: Date;
  /** For GroupDealDO: 'deadline' | 'partial_decision' */
  alarmKind?: string;
}

interface FindingRow {
  id: string;
  entityType: string;
  entityId: string;
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

function getNs(env: ReconciliationEnv, entityType: string): DurableObjectNamespace {
  switch (entityType) {
    case 'deal':
      return env.DEAL_DO;
    case 'group_deal':
      return env.GROUP_DEAL_DO;
    case 'personal_offer':
      return env.PERSONAL_OFFER_DO;
    case 'scheduled_publish':
      return env.SCHEDULED_PUBLISH_DO;
    case 'gold_window':
      return env.GOLD_WINDOW_DO;
    default:
      throw new Error(`Unknown entity type: ${entityType}`);
  }
}

async function checkAlarmArmed(ns: DurableObjectNamespace, entityId: string): Promise<boolean> {
  try {
    const stub = ns.get(ns.idFromName(entityId));
    const res = await stub.fetch('https://do/status', { method: 'GET' });
    if (!res.ok) return false;
    const body = (await res.json()) as { at: number | null };
    return body.at !== null;
  } catch (err) {
    captureCaught(err, { scope: 'server.cron.alarm-reconciliation', severity: 'warning' });
    // If DO is unreachable (not yet provisioned, secrets missing), treat as unarmed.
    return false;
  }
}

async function armAlarm(
  ns: DurableObjectNamespace,
  entityId: string,
  at: Date,
  kind?: string,
): Promise<void> {
  const stub = ns.get(ns.idFromName(entityId));
  const body: Record<string, unknown> = { at: at.getTime() };
  if (kind) body.kind = kind;
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
}

// ─── Main ─────────────────────────────────────────────────────────────────────

export const runAlarmReconciliation = withSentry(
  async function runAlarmReconciliation(env: ReconciliationEnv): Promise<void> {
    const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
    const now = new Date();

    // ── 1. Collect open entities ───────────────────────────────────────────────

    const candidates: EntityCandidate[] = [];

    // Deals: ACTIVE or PAUSED with a future windowEnd
    const openDeals = await db
      .select({ id: deals.id, windowEnd: deals.windowEnd })
      .from(deals)
      .where(
        and(
          inArray(deals.dealState, ['ACTIVE', 'PAUSED']),
          isNotNull(deals.windowEnd),
          gt(deals.windowEnd, now),
        ),
      );
    for (const d of openDeals) {
      candidates.push({ entityType: 'deal', entityId: d.id, alarmAt: d.windowEnd! });
    }

    // Gold windows: soldOutGoldExpiresAt IS NOT NULL and future
    const openGoldWindows = await db
      .select({ id: deals.id, soldOutGoldExpiresAt: deals.soldOutGoldExpiresAt })
      .from(deals)
      .where(and(isNotNull(deals.soldOutGoldExpiresAt), gt(deals.soldOutGoldExpiresAt, now)));
    for (const d of openGoldWindows) {
      candidates.push({
        entityType: 'gold_window',
        entityId: d.id,
        alarmAt: d.soldOutGoldExpiresAt!,
      });
    }

    // Group deals: active states with a future effective deadline
    const openGroupDeals = await db.execute(sql`
    SELECT
      gd.id,
      gd.group_state,
      gd.extended_deadline,
      gd.partial_decision_deadline,
      d.window_end
    FROM group_deals gd
    JOIN deals d ON d.id = gd.deal_id
    WHERE gd.group_state IN ('COLLECTING','THRESHOLD_MET','EXTENDED','PARTIAL_PENDING')
      AND COALESCE(gd.extended_deadline, d.window_end) > NOW()
  `);
    for (const row of openGroupDeals.rows as Array<{
      id: string;
      group_state: string;
      extended_deadline: Date | null;
      partial_decision_deadline: Date | null;
      window_end: Date | null;
    }>) {
      if (row.group_state === 'PARTIAL_PENDING' && row.partial_decision_deadline) {
        candidates.push({
          entityType: 'group_deal',
          entityId: row.id,
          alarmAt: row.partial_decision_deadline,
          alarmKind: 'partial_decision',
        });
      } else {
        const deadline = row.extended_deadline ?? row.window_end;
        if (deadline) {
          candidates.push({
            entityType: 'group_deal',
            entityId: row.id,
            alarmAt: deadline,
            alarmKind: 'deadline',
          });
        }
      }
    }

    // Personal offers: PENDING with a future responseDeadline
    const openPersonalOffers = await db
      .select({
        id: personalDealRequests.id,
        responseDeadline: personalDealRequests.responseDeadline,
      })
      .from(personalDealRequests)
      .where(
        and(
          eq(personalDealRequests.status, 'PENDING'),
          gt(personalDealRequests.responseDeadline, now),
        ),
      );
    for (const p of openPersonalOffers) {
      candidates.push({
        entityType: 'personal_offer',
        entityId: p.id,
        alarmAt: p.responseDeadline,
      });
    }

    // Scheduled publishes: scheduledVersionId IS NOT NULL and scheduledAt > NOW()
    const openScheduledPublishes = await db
      .select({ page: pageLayoutPointers.page, scheduledAt: pageLayoutPointers.scheduledAt })
      .from(pageLayoutPointers)
      .where(
        and(
          isNotNull(pageLayoutPointers.scheduledVersionId),
          isNotNull(pageLayoutPointers.scheduledAt),
          gt(pageLayoutPointers.scheduledAt, now),
        ),
      );
    for (const s of openScheduledPublishes) {
      candidates.push({
        entityType: 'scheduled_publish',
        entityId: s.page, // keyed by page slug
        alarmAt: s.scheduledAt!,
      });
    }

    // ── 2. Fetch existing unresolved findings ──────────────────────────────────

    const existingFindings = await db
      .select({
        id: systemHealthFindings.id,
        entityType: systemHealthFindings.entityType,
        entityId: systemHealthFindings.entityId,
      })
      .from(systemHealthFindings)
      .where(
        and(
          eq(systemHealthFindings.kind, 'unarmed_entity'),
          isNull(systemHealthFindings.resolvedAt),
        ),
      );

    const findingMap = new Map<string, FindingRow>(
      existingFindings.map((f) => [`${f.entityType}:${f.entityId}`, f]),
    );

    // ── 3. Check each candidate ────────────────────────────────────────────────

    for (const candidate of candidates) {
      const ns = getNs(env, candidate.entityType);
      const isArmed = await checkAlarmArmed(ns, candidate.entityId);
      const key = `${candidate.entityType}:${candidate.entityId}`;
      const existing = findingMap.get(key);

      if (!isArmed) {
        // Re-arm
        try {
          await armAlarm(ns, candidate.entityId, candidate.alarmAt, candidate.alarmKind);
        } catch (err) {
          // Log but don't throw — continue scanning other entities
          captureCaught(err, { scope: 'server.cron.alarm-reconciliation.arm' });
        }

        // Upsert finding
        if (existing) {
          // Bump lastSeenAt
          await touchFinding(db, {
            kind: 'unarmed_entity',
            entityType: candidate.entityType,
            entityId: candidate.entityId,
            detail: 'auto-armed at reconciliation',
            at: now,
          });
        } else {
          await touchFinding(db, {
            kind: 'unarmed_entity',
            entityType: candidate.entityType,
            entityId: candidate.entityId,
            detail: 'auto-armed at reconciliation',
            at: now,
          });
        }
      } else if (existing) {
        await resolveFinding(db, existing.id);
      }
    }
  },
  { name: 'cron.alarm-reconciliation', kind: 'cron' },
);
