/**
 * Settlement monitor — runs every 6h (folded into hourly cron at 00/06/12/18 UTC).
 *
 * Check 1: Cloudflare Queue billable-op rate (GraphQL, last 24h).
 * Check 2: Stuck vendor_payout_releases distribution (Postgres aggregate).
 *
 * Alerts admins via Resend; dedup state in ops_alert_state (fail-soft if table missing).
 */

import { createDbService, type DrizzleDb } from '@/server/services/db.js';
import React from 'react';
import { eq, sql } from 'drizzle-orm';
import { captureCaught } from '@/server/observability/capture.server';
import { opsAlertState } from '../db/schema.js';
import { upsertOpsAlertState } from '../db/queries/ops-alert-state.js';
import { sendEmail } from '../email/resend.js';
import { resolveAdminSupportRecipients } from '../support/notifications-admin.js';
import type { CronEnv } from './deal-expiry.js';
import { formatNumber } from '@/lib/format';

// ─── Types & constants ───────────────────────────────────────────────────────

export type AlertSeverity = 'info' | 'warn' | 'page';

export type SettlementMonitorEnv = CronEnv & {
  CF_ANALYTICS_TOKEN?: string;
  CF_ACCOUNT_ID?: string;
  RESEND_FROM_EMAIL?: string;
  ADMIN_EMAIL?: string;
};

export const QUEUE_OPS_TIER = 10_000;
export const QUEUE_OPS_WARN = 6_000;
export const QUEUE_OPS_PAGE = 8_000;
export const STUCK_MAX_ATTEMPTS_CEILING = 20;
export const STUCK_AGE_MS = 7 * 24 * 60 * 60 * 1000;
export const STUCK_ENQUEUED_RELEASING_WARN = 5;
export const STUCK_ENQUEUED_RELEASING_PAGE = 15;

const SEVERITY_RANK: Record<AlertSeverity, number> = { info: 0, warn: 1, page: 2 };
export const ALERT_COOLDOWN_MS = 24 * 60 * 60 * 1000;

// limit:1000 is safe — groups are by {queueId, actionType, outcome} only (~tens of rows per account).
const QUEUE_GRAPHQL_QUERY =
  'query($a:String!,$s:Time!,$u:Time!){viewer{accounts(filter:{accountTag:$a}){queueMessageOperationsAdaptiveGroups(limit:1000, filter:{datetime_geq:$s, datetime_leq:$u}){sum{billableOperations} dimensions{queueId actionType outcome}}}}}';

export interface QueueOpRateResult {
  mode: 'ok' | 'degraded';
  totalOps?: number;
  pctOfTier?: number;
  severity: AlertSeverity;
  reason?: string;
}

export interface StuckRowStatusAgg {
  status: string;
  n: number;
  maxAttempts: number;
  fundsPending: number;
  oldest: Date | null;
}

export interface StuckRowCheckResult {
  severity: AlertSeverity;
  reasons: string[];
  byStatus: StuckRowStatusAgg[];
  noAcctCount: number;
  enqueuedReleasingCount: number;
  globalMaxAttempts: number;
  globalOldest: Date | null;
}

interface PendingAlert {
  alertKey: string;
  severity: AlertSeverity;
}

// ─── Pure evaluators (exported for unit tests) ───────────────────────────────

export function maxSeverity(a: AlertSeverity, b: AlertSeverity): AlertSeverity {
  return SEVERITY_RANK[a] >= SEVERITY_RANK[b] ? a : b;
}

export function evaluateQueueOpSeverity(totalOps: number): AlertSeverity {
  if (totalOps >= QUEUE_OPS_PAGE) return 'page';
  if (totalOps >= QUEUE_OPS_WARN) return 'warn';
  return 'info';
}

export function shouldFireAlert(
  severity: AlertSeverity,
  lastState: { lastSeverity: AlertSeverity; lastFiredAt: Date } | null,
  now: Date = new Date(),
  cooldownMs: number = ALERT_COOLDOWN_MS,
): boolean {
  if (severity === 'info') return false;
  if (!lastState) return true;
  if (SEVERITY_RANK[severity] > SEVERITY_RANK[lastState.lastSeverity]) return true;
  return now.getTime() - lastState.lastFiredAt.getTime() > cooldownMs;
}

// Effective cooldown for a firing result: a 'page' from an adapter that opts into a
// shorter cooldown uses it; everything else uses the standard 24h. Pure → unit-testable.
export function effectiveCooldownMs(
  severity: AlertSeverity,
  adapter: { pageCooldownMs?: number },
): number {
  return severity === 'page' && adapter.pageCooldownMs != null
    ? adapter.pageCooldownMs
    : ALERT_COOLDOWN_MS;
}

export function evaluateStuckRows(
  rows: StuckRowStatusAgg[],
  now: Date = new Date(),
): StuckRowCheckResult {
  let enqueuedReleasingCount = 0;
  let globalMaxAttempts = 0;
  let globalOldest: Date | null = null;
  const reasons: string[] = [];
  let severity: AlertSeverity = 'info';

  for (const row of rows) {
    if (row.status === 'enqueued' || row.status === 'releasing') {
      enqueuedReleasingCount += row.n;
    }
    globalMaxAttempts = Math.max(globalMaxAttempts, row.maxAttempts);
    if (row.oldest) {
      const d = row.oldest instanceof Date ? row.oldest : new Date(row.oldest);
      if (!globalOldest || d < globalOldest) globalOldest = d;
    }
  }

  if (globalMaxAttempts > STUCK_MAX_ATTEMPTS_CEILING) {
    reasons.push(
      `max claim_attempts=${globalMaxAttempts} exceeds ceiling ${STUCK_MAX_ATTEMPTS_CEILING}`,
    );
    severity = maxSeverity(severity, 'page');
  }

  if (globalOldest && now.getTime() - globalOldest.getTime() > STUCK_AGE_MS) {
    const ageDays = Math.floor((now.getTime() - globalOldest.getTime()) / (24 * 60 * 60 * 1000));
    reasons.push(`oldest stuck row is ${ageDays} days old (${globalOldest.toISOString()})`);
    severity = maxSeverity(severity, 'page');
  }

  if (enqueuedReleasingCount >= STUCK_ENQUEUED_RELEASING_PAGE) {
    reasons.push(`${enqueuedReleasingCount} enqueued/releasing rows (re-drive churn)`);
    severity = maxSeverity(severity, 'page');
  } else if (enqueuedReleasingCount >= STUCK_ENQUEUED_RELEASING_WARN) {
    reasons.push(`${enqueuedReleasingCount} enqueued/releasing rows`);
    severity = maxSeverity(severity, 'warn');
  }

  return {
    severity,
    reasons,
    byStatus: rows,
    noAcctCount: 0,
    enqueuedReleasingCount,
    globalMaxAttempts,
    globalOldest,
  };
}

export function buildAlertSubject(
  queue: QueueOpRateResult,
  stuck: StuckRowCheckResult,
  overallSeverity: AlertSeverity,
): string {
  if (queue.mode === 'ok' && queue.totalOps != null && queue.pctOfTier != null) {
    if (overallSeverity !== 'info' && queue.severity !== 'info') {
      return `[multideal-ops] settlements: ${formatNumber(queue.totalOps, 'en')} queue ops/day (${queue.pctOfTier}% tier)`;
    }
  }
  if (stuck.severity !== 'info' && stuck.reasons.length > 0) {
    return `[multideal-ops] settlements: stuck payout rows (${overallSeverity})`;
  }
  if (queue.mode === 'degraded') {
    return `[multideal-ops] settlements: monitor degraded (${overallSeverity})`;
  }
  return `[multideal-ops] settlements: alert (${overallSeverity})`;
}

// ─── IO helpers ──────────────────────────────────────────────────────────────

export async function fetchQueueOpRate(
  env: Pick<SettlementMonitorEnv, 'CF_ANALYTICS_TOKEN' | 'CF_ACCOUNT_ID'>,
  now: Date = new Date(),
  fetchFn: typeof fetch = fetch,
): Promise<QueueOpRateResult> {
  if (!env.CF_ANALYTICS_TOKEN || !env.CF_ACCOUNT_ID) {
    return {
      mode: 'degraded',
      severity: 'warn',
      reason: 'CF_ANALYTICS_TOKEN or CF_ACCOUNT_ID not configured',
    };
  }

  const until = now;
  const since = new Date(now.getTime() - 24 * 60 * 60 * 1000);

  try {
    const resp = await fetchFn('https://api.cloudflare.com/client/v4/graphql', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${env.CF_ANALYTICS_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: QUEUE_GRAPHQL_QUERY,
        variables: {
          a: env.CF_ACCOUNT_ID,
          s: since.toISOString(),
          u: until.toISOString(),
        },
      }),
    });

    if (!resp.ok) {
      let body = '';
      try {
        body = await resp.text();
      } catch (err) {
        captureCaught(err, { scope: 'cron.settlement-monitor.graphql-body', severity: 'info' });
      }
      return {
        mode: 'degraded',
        severity: 'warn',
        reason: `GraphQL HTTP ${resp.status}${body ? `: ${body.slice(0, 200)}` : ''}`,
      };
    }

    const data = (await resp.json()) as {
      errors?: Array<{ message?: string }>;
      data?: {
        viewer?: {
          accounts?: Array<{
            queueMessageOperationsAdaptiveGroups?: Array<{
              sum?: { billableOperations?: number };
            }>;
          }>;
        };
      };
    };

    if (data.errors?.length) {
      return {
        mode: 'degraded',
        severity: 'warn',
        reason: data.errors[0]?.message ?? 'GraphQL returned errors',
      };
    }

    const groups = data.data?.viewer?.accounts?.[0]?.queueMessageOperationsAdaptiveGroups;
    if (groups == null) {
      return {
        mode: 'degraded',
        severity: 'warn',
        reason:
          'CF GraphQL returned 200 but no recognizable viewer.accounts[0].queueMessageOperationsAdaptiveGroups node (token revoked/under-scoped?)',
      };
    }

    let totalOps = 0;
    for (const g of groups) {
      totalOps += g.sum?.billableOperations ?? 0;
    }

    const severity = evaluateQueueOpSeverity(totalOps);
    const pctOfTier = Math.round((totalOps / QUEUE_OPS_TIER) * 100);
    return { mode: 'ok', totalOps, pctOfTier, severity };
  } catch (err) {
    return {
      mode: 'degraded',
      severity: 'warn',
      reason: err instanceof Error ? err.message : 'GraphQL fetch failed',
    };
  }
}

type StuckRowDbRow = {
  status: string;
  n: number;
  max_attempts: number;
  funds_pending: number;
  oldest: string | Date | null;
};

export async function queryStuckRowDistribution(
  db: DrizzleDb,
): Promise<{ alertable: StuckRowStatusAgg[]; noAcctCount: number }> {
  const agg = await db.execute<StuckRowDbRow>(sql`
    SELECT status,
           count(*)::int                                                AS n,
           max(claim_attempts)::int                                     AS max_attempts,
           count(*) FILTER (WHERE last_error LIKE 'funds_pending%')::int AS funds_pending,
           min(COALESCE(held_at, enqueued_at))                          AS oldest
      FROM vendor_payout_releases
     WHERE status IN ('held', 'enqueued', 'releasing')
       AND starts_with(vendor_acct_id, 'acct_')
     GROUP BY status
  `);

  const noAcct = await db.execute<{ n: number }>(sql`
    SELECT count(*)::int AS n
      FROM vendor_payout_releases
     WHERE status IN ('held', 'enqueued', 'releasing')
       AND NOT starts_with(vendor_acct_id, 'acct_')
  `);

  const alertable: StuckRowStatusAgg[] = agg.rows.map((r) => ({
    status: r.status,
    n: r.n,
    maxAttempts: r.max_attempts,
    fundsPending: r.funds_pending,
    oldest: r.oldest ? (r.oldest instanceof Date ? r.oldest : new Date(r.oldest)) : null,
  }));

  return { alertable, noAcctCount: noAcct.rows[0]?.n ?? 0 };
}

async function readAlertState(
  db: DrizzleDb,
  alertKey: string,
): Promise<{ lastSeverity: AlertSeverity; lastFiredAt: Date } | null> {
  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;
    const lastFiredAt =
      row.lastFiredAt instanceof Date ? row.lastFiredAt : new Date(row.lastFiredAt);
    return {
      lastSeverity: row.lastSeverity as AlertSeverity,
      lastFiredAt,
    };
  } catch (err) {
    console.error('[settlement-monitor] ops_alert_state read failed', alertKey, err);
    return null;
  }
}

async function writeAlertState(
  db: DrizzleDb,
  alertKey: string,
  severity: AlertSeverity,
  firedAt: Date,
): Promise<void> {
  try {
    await upsertOpsAlertState(db, { alertKey, lastSeverity: severity, lastFiredAt: firedAt });
  } catch (err) {
    console.error('[settlement-monitor] ops_alert_state write failed', alertKey, err);
  }
}

function formatQueueSection(queue: QueueOpRateResult): string {
  if (queue.mode === 'degraded') {
    return `Queue op-rate: DEGRADED — settlement monitor could not check queue op-rate: ${queue.reason ?? 'unknown'}`;
  }
  return `Queue op-rate (24h): ${formatNumber(queue.totalOps, 'en')} billable ops (${queue.pctOfTier ?? '?'}% of ${formatNumber(QUEUE_OPS_TIER, 'en')}/day tier) — severity ${queue.severity}`;
}

function formatStuckSection(stuck: StuckRowCheckResult): string {
  const lines = [
    `Stuck payout rows (acct_* only): severity ${stuck.severity}`,
    `  enqueued+releasing: ${stuck.enqueuedReleasingCount}`,
    `  max claim_attempts: ${stuck.globalMaxAttempts}`,
    `  oldest: ${stuck.globalOldest?.toISOString() ?? 'n/a'}`,
    `  backfill/no_acct rows (excluded from thresholds): ${stuck.noAcctCount}`,
  ];
  if (stuck.reasons.length > 0) {
    lines.push(`  triggers: ${stuck.reasons.join('; ')}`);
  }
  for (const row of stuck.byStatus) {
    lines.push(
      `  ${row.status}: n=${row.n} max_attempts=${row.maxAttempts} funds_pending=${row.fundsPending} oldest=${row.oldest?.toISOString() ?? 'n/a'}`,
    );
  }
  return lines.join('\n');
}

function buildAlertEmailBody(queue: QueueOpRateResult, stuck: StuckRowCheckResult): string {
  return `${formatQueueSection(queue)}\n\n${formatStuckSection(stuck)}`;
}

function buildAlertReactBody(queue: QueueOpRateResult, stuck: StuckRowCheckResult) {
  const body = buildAlertEmailBody(queue, stuck);
  return React.createElement(
    'div',
    null,
    body
      .split('\n')
      .map((line, i) =>
        React.createElement(
          'p',
          { key: i, style: { margin: '4px 0', fontFamily: 'monospace' } },
          line,
        ),
      ),
  );
}

function collectPendingAlerts(
  queue: QueueOpRateResult,
  stuck: StuckRowCheckResult,
  now: Date,
  readState: (key: string) => Promise<{ lastSeverity: AlertSeverity; lastFiredAt: Date } | null>,
): Promise<PendingAlert[]> {
  return (async () => {
    const pending: PendingAlert[] = [];

    if (queue.mode === 'degraded') {
      const key = 'settlements:queue_ops_degraded';
      const state = await readState(key);
      if (shouldFireAlert('warn', state, now)) {
        pending.push({ alertKey: key, severity: 'warn' });
      }
    } else if (queue.severity !== 'info') {
      const key = 'settlements:queue_ops';
      const state = await readState(key);
      if (shouldFireAlert(queue.severity, state, now)) {
        pending.push({ alertKey: key, severity: queue.severity });
      }
    }

    if (stuck.severity !== 'info') {
      const key = 'settlements:stuck_rows';
      const state = await readState(key);
      if (shouldFireAlert(stuck.severity, state, now)) {
        pending.push({ alertKey: key, severity: stuck.severity });
      }
    }

    return pending;
  })();
}

// ─── Entry point ─────────────────────────────────────────────────────────────

export async function tickSettlementMonitor(env: SettlementMonitorEnv): Promise<void> {
  try {
    const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
    const now = new Date();

    const queue = await fetchQueueOpRate(env, now);
    const { alertable, noAcctCount } = await queryStuckRowDistribution(db);
    const stuck = evaluateStuckRows(alertable, now);
    stuck.noAcctCount = noAcctCount;

    const readState = (key: string) => readAlertState(db, key);
    const pending = await collectPendingAlerts(queue, stuck, now, readState);

    if (pending.length === 0) return;

    if (!env.RESEND_API_KEY || !env.RESEND_FROM_EMAIL) {
      console.error('[settlement-monitor] RESEND not configured — alert suppressed', {
        pending: pending.map((p) => p.alertKey),
      });
      return;
    }

    const recipients = await resolveAdminSupportRecipients(db, env);
    if (recipients.length === 0) {
      console.error('[settlement-monitor] no admin recipients — alert suppressed', {
        pending: pending.map((p) => p.alertKey),
      });
      return;
    }

    const overallSeverity = pending.reduce<AlertSeverity>(
      (max, p) => maxSeverity(max, p.severity),
      'info',
    );
    const subject = buildAlertSubject(queue, stuck, overallSeverity);
    const react = buildAlertReactBody(queue, stuck);

    let anySendSucceeded = false;
    for (const to of recipients) {
      const result = await sendEmail(
        {
          RESEND_API_KEY: env.RESEND_API_KEY,
          RESEND_FROM_EMAIL: env.RESEND_FROM_EMAIL,
          ENVIRONMENT: (env as { ENVIRONMENT?: string }).ENVIRONMENT,
          EMAIL_MOCK_DB: (env as { EMAIL_MOCK_DB?: D1Database }).EMAIL_MOCK_DB,
        },
        { to, subject, react },
      );
      if (result.success) {
        anySendSucceeded = true;
      } else {
        console.error('[settlement-monitor] email send failed', { to, error: result.error });
      }
    }

    if (anySendSucceeded) {
      for (const alert of pending) {
        await writeAlertState(db, alert.alertKey, alert.severity, now);
      }
    }
  } catch (err) {
    captureCaught(err, { scope: 'cron.settlement-monitor', severity: 'error' });
  }
}
