import { createDbService, type DrizzleDb } from '@/server/services/db.js';
import { eq } from 'drizzle-orm';
import { opsAlertState } from '@/server/db/schema.js';
import {
  shouldFireAlert,
  maxSeverity,
  effectiveCooldownMs,
} from '@/server/cron/settlement-monitor.js';
import type { AlertSeverity } from '@/server/cron/settlement-monitor.js';
import { CHECK_REGISTRY } from './checks/index.js';
import { getChannelAdapter } from './channels/index.js';
import { listEnabledChannelsForSeverity } from '@/server/db/queries/alert-channels.js';
import {
  listMonitorCheckRows,
  ensureMonitorCheckRow,
  recordCheckRun,
} from '@/server/db/queries/monitor-checks.js';
import { recordCronSuccess, recordCronError } from '@/server/db/queries/cron-runs.js';
import { upsertOpsAlertState } from '@/server/db/queries/ops-alert-state.js';
import { pingDeadMan } from './dead-man.js';
import { redactError } from './redact.js';
import type { CheckAdapter, CheckResult, MonitorEnv, AlertPayload } from './types.js';

export function synthesizeRejected(key: string, err: unknown): CheckResult {
  // redactError scrubs DSN/PII from an adapter that threw past its own catch (allSettled backstop).
  return {
    key,
    severity: 'warn',
    degraded: true,
    title: key,
    detail: redactError(err, 'check threw'),
  };
}

export function isDue(lastRunAt: Date | null, intervalHours: number, now: Date): boolean {
  if (!lastRunAt) return true;
  return now.getTime() - lastRunAt.getTime() >= intervalHours * 3_600_000;
}

async function readState(db: DrizzleDb, alertKey: string) {
  try {
    const rows = await db
      .select({ lastSeverity: opsAlertState.lastSeverity, lastFiredAt: opsAlertState.lastFiredAt })
      .from(opsAlertState)
      .where(eq(opsAlertState.alertKey, alertKey))
      .limit(1);
    const row = rows[0];
    if (!row) return null;
    return {
      lastSeverity: row.lastSeverity as AlertSeverity,
      lastFiredAt: row.lastFiredAt instanceof Date ? row.lastFiredAt : new Date(row.lastFiredAt),
    };
  } catch (err) {
    void err;
    return null;
  }
}

async function writeState(db: DrizzleDb, alertKey: string, severity: AlertSeverity, firedAt: Date) {
  try {
    await upsertOpsAlertState(db, { alertKey, lastSeverity: severity, lastFiredAt: firedAt });
  } catch (err) {
    console.error(
      '[ops-monitor] state write failed',
      alertKey,
      redactError(err, 'state write failed'),
    );
  } // redactError: a DB insert error can carry the DSN/password into the worker log (Hard Rule 8)
}

/** Deliver one firing result to all eligible channels; return true if ≥1 send ok. */
async function deliver(db: DrizzleDb, result: CheckResult, env: MonitorEnv): Promise<boolean> {
  if (result.severity === 'info') return false;
  const channels = await listEnabledChannelsForSeverity(db, result.severity);
  if (channels.length === 0) {
    console.error('[ops-monitor] no channels for severity', result.severity, result.key);
    return false;
  }
  const payload: AlertPayload = {
    severity: result.severity,
    subject: `[multideal-ops] ${result.severity.toUpperCase()}: ${result.title}`,
    results: [result],
    kind: 'realtime',
  };
  let anyOk = false;
  for (const ch of channels) {
    // Per-channel try: one channel's decrypt/send failure must not skip the remaining channels.
    try {
      const adapter = getChannelAdapter(ch.kind);
      if (!adapter) continue;
      const res = await adapter.send(payload, ch, env);
      if (res.ok) anyOk = true;
      else console.error('[ops-monitor] channel send failed', ch.id, res.error);
    } catch (err) {
      // decrypt now happens inside adapter.send and is still caught by this per-channel try.
      console.error(
        '[ops-monitor] channel delivery threw',
        ch.id,
        err instanceof Error ? err.name : 'unknown',
      );
    }
  }
  return anyOk;
}

export async function tickOpsMonitor(env: MonitorEnv): Promise<void> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const now = new Date();
  try {
    // Sync registry → ensure a config row exists for each check (idempotent).
    for (const adapter of CHECK_REGISTRY) {
      await ensureMonitorCheckRow(db, adapter.key, {
        enabled: adapter.enabledByDefault ?? true,
        config: adapter.defaultConfig,
        severityFloor: adapter.defaultSeverityFloor ?? null,
        intervalHours: adapter.defaultIntervalHours,
      });
    }
    const rows = await listMonitorCheckRows(db);
    const rowByKey = new Map(rows.map((r) => [r.key, r]));

    const due: CheckAdapter[] = CHECK_REGISTRY.filter((a) => {
      const row = rowByKey.get(a.key);
      if (row && !row.enabled) return false;
      const interval = row?.intervalHours ?? a.defaultIntervalHours;
      return isDue(row?.lastRunAt ? new Date(row.lastRunAt) : null, interval, now);
    });

    const settled = await Promise.allSettled(
      due.map((a) => {
        const row = rowByKey.get(a.key);
        const config = { ...a.defaultConfig, ...((row?.config as Record<string, unknown>) ?? {}) };
        return a.run({ db, env, now, config });
      }),
    );

    for (let i = 0; i < due.length; i++) {
      const adapter = due[i]!;
      const s = settled[i]!;
      let result: CheckResult =
        s.status === 'fulfilled'
          ? s.value
          : synthesizeRejected(adapter.key, (s as PromiseRejectedResult).reason);
      // Apply optional per-check severity floor: raise an actual FINDING to at least the
      // configured floor. Never applied to a healthy 'info' result — the floor must not
      // manufacture an alert on a clean run (that would false-page and poison alert state).
      const floor = rowByKey.get(adapter.key)?.severityFloor as AlertSeverity | null | undefined;
      if (floor && result.severity !== 'info') {
        result = { ...result, severity: maxSeverity(result.severity, floor) };
      }

      await recordCheckRun(db, adapter.key, result.severity, result.detail, now);

      const alertKey = `check:${adapter.key}`;
      const state = await readState(db, alertKey);
      const cooldownMs = effectiveCooldownMs(result.severity, adapter);
      if (shouldFireAlert(result.severity, state, now, cooldownMs)) {
        const delivered = await deliver(db, result, env);
        if (delivered) await writeState(db, alertKey, result.severity, now);
      }
    }
    await recordCronSuccess(db, 'ops-monitor', 60, now);
    await pingDeadMan(env);
  } catch (err) {
    await recordCronError(
      db,
      'ops-monitor',
      60,
      err instanceof Error ? err.message : 'unknown',
      now,
    ).catch((recordErr) => {
      void recordErr;
    }); // recordCronError scrubs (cron-runs.ts:629) → safe to persist err.message
    console.error('[ops-monitor] tick failed', redactError(err, 'tick failed')); // redactError: raw err in the worker log can carry the DSN (Hard Rule 8)
  }
}
