/**
 * Proactive fleet alert system for Token.
 *
 * Checks fleet health (device connectivity, model availability) and budget
 * thresholds, returning a list of active alerts. These alerts are injected
 * into Token's system prompt so it can proactively warn users, and surfaced
 * in the dashboard UI as dismissible badges.
 *
 * Alerts are computed dynamically from current DB state — they are not
 * persisted. Acknowledgment is tracked in-memory with a 1-hour TTL.
 */

import { db } from "@/lib/db";
import { logger } from "@/lib/logger";
import { getTokenSpending } from "@/lib/token/analytics";
import { isInMaintenanceWindow } from "@/lib/token/scheduling";
import { incAlerts } from "@/lib/token/metrics";

/* ── Types ─────────────────────────────────────────────────────────── */

export type AlertSeverity = "info" | "warning" | "critical";
export type AlertCategory = "device" | "budget" | "fleet" | "model";

export interface TokenAlert {
  id: string;
  severity: AlertSeverity;
  category: AlertCategory;
  title: string;
  message: string;
  deviceId?: string;
  createdAt: Date;
  acknowledged: boolean;
}

/* ── Acknowledgment store (in-memory, per-process) ─────────────────── */

const ACK_TTL_MS = 60 * 60 * 1000; // 1 hour

interface AckEntry {
  at: number;
}

const ackMap = new Map<string, AckEntry>();

/** Build the composite key for the ack map. */
function ackKey(userId: string, alertId: string): string {
  return `${userId}:${alertId}`;
}

/** Mark an alert as acknowledged for a user (1-hour TTL). */
export function acknowledgeAlert(userId: string, alertId: string): void {
  // Prune expired entries lazily (every 100 acks)
  if (ackMap.size > 0 && ackMap.size % 100 === 0) {
    pruneAcks();
  }
  ackMap.set(ackKey(userId, alertId), { at: Date.now() });
}

/** Check whether an alert has been acknowledged and the ack is still valid. */
function isAcknowledged(userId: string, alertId: string): boolean {
  const entry = ackMap.get(ackKey(userId, alertId));
  if (!entry) return false;
  if (Date.now() - entry.at > ACK_TTL_MS) {
    ackMap.delete(ackKey(userId, alertId));
    return false;
  }
  return true;
}

/** Remove expired acknowledgments. */
function pruneAcks(): void {
  const now = Date.now();
  for (const [key, entry] of ackMap) {
    if (now - entry.at > ACK_TTL_MS) {
      ackMap.delete(key);
    }
  }
}

/* ── Exported for testing only ─────────────────────────────────────── */

export function _resetAcks(): void {
  ackMap.clear();
}

/* ── Alert checker ─────────────────────────────────────────────────── */

const DEVICE_OFFLINE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Check all alert conditions and return active alerts for a user.
 *
 * Queries devices and budget config, then evaluates six alert rules.
 * Each alert gets a deterministic ID so the frontend can deduplicate.
 */
export async function checkAlerts(userId: string): Promise<TokenAlert[]> {
  const now = new Date();
  const alerts: TokenAlert[] = [];

  // Load devices and config in parallel
  const [devices, config, spending] = await Promise.all([
    db.device.findMany({
      where: { userId },
      select: {
        id: true,
        name: true,
        status: true,
        models: true,
        lastSeen: true,
      },
    }),
    db.tokenConfig.findUnique({ where: { id: "singleton" } }),
    getTokenSpending(userId),
  ]);

  // --- 1. No Devices (info) ---
  if (devices.length === 0) {
    alerts.push({
      id: "fleet-no-devices",
      severity: "info",
      category: "fleet",
      title: "No devices connected",
      message: "Set up your first device with `llmhosts up` to get started.",
      createdAt: now,
      acknowledged: isAcknowledged(userId, "fleet-no-devices"),
    });
  }

  // --- Device-level alerts ---
  let totalModels = 0;

  for (const device of devices) {
    const msSinceSeen = now.getTime() - device.lastSeen.getTime();
    let models: string[] = [];
    try {
      models = JSON.parse(device.models) as string[];
    } catch {
      // Treat unparseable models as empty
    }
    totalModels += models.length;

    // --- 2. Device Offline (critical) ---
    // Suppress offline alert when device is in a scheduled maintenance window
    const inMaintenance =
      msSinceSeen > DEVICE_OFFLINE_THRESHOLD_MS
        ? await isInMaintenanceWindow(device.id)
        : false;

    if (
      device.status !== "offline" &&
      msSinceSeen > DEVICE_OFFLINE_THRESHOLD_MS &&
      !inMaintenance
    ) {
      const minutesAgo = Math.round(msSinceSeen / 60_000);
      const timeAgo = minutesAgo >= 60
        ? `${Math.round(minutesAgo / 60)}h ${minutesAgo % 60}m ago`
        : `${minutesAgo}m ago`;

      const alertId = `device-offline-${device.id}`;
      alerts.push({
        id: alertId,
        severity: "critical",
        category: "device",
        title: `${device.name} appears offline`,
        message: `Last seen ${timeAgo}. Check connectivity or run \`llmhosts doctor\` on that machine.`,
        deviceId: device.id,
        createdAt: now,
        acknowledged: isAcknowledged(userId, alertId),
      });
    }

    // --- 6. Device Degraded (warning) ---
    if (device.status === "degraded") {
      const alertId = `device-degraded-${device.id}`;
      alerts.push({
        id: alertId,
        severity: "warning",
        category: "device",
        title: `${device.name} is degraded`,
        message: "Device is responding slowly or with errors. Consider restarting the engine.",
        deviceId: device.id,
        createdAt: now,
        acknowledged: isAcknowledged(userId, alertId),
      });
    }
  }

  // --- 5. No Models (warning) — has devices but zero models ---
  if (devices.length > 0 && totalModels === 0) {
    alerts.push({
      id: "model-none-loaded",
      severity: "warning",
      category: "model",
      title: "No models loaded",
      message: "Your devices have no models loaded. Ask me to recommend models for your hardware.",
      createdAt: now,
      acknowledged: isAcknowledged(userId, "model-none-loaded"),
    });
  }

  // --- Budget alerts ---
  const budget = config?.monthlyBudgetUsd ?? 50.0;
  const pct = spending.budgetUsedPct;
  const remaining = spending.budgetRemaining;

  // --- 4. Budget Critical (critical) — > 95% ---
  if (pct > 95) {
    alerts.push({
      id: "budget-critical",
      severity: "critical",
      category: "budget",
      title: "Budget nearly exhausted",
      message: `You've used ${pct.toFixed(1)}% of your budget. Only $${remaining.toFixed(2)} remaining. Token will stop responding at 100%.`,
      createdAt: now,
      acknowledged: isAcknowledged(userId, "budget-critical"),
    });
  }
  // --- 3. Budget Warning (warning) — > 80% but <= 95% ---
  else if (pct > 80) {
    alerts.push({
      id: "budget-warning",
      severity: "warning",
      category: "budget",
      title: "Approaching budget limit",
      message: `You've used ${pct.toFixed(1)}% of your $${budget.toFixed(0)}/month budget. $${remaining.toFixed(2)} remaining.`,
      createdAt: now,
      acknowledged: isAcknowledged(userId, "budget-warning"),
    });
  }

  // Emit Prometheus metrics for each alert
  for (const alert of alerts) {
    incAlerts(alert.severity);
  }

  logger.debug(
    {
      action: "token_alerts_check",
      user_id: userId,
      alert_count: alerts.length,
      critical: alerts.filter((a) => a.severity === "critical").length,
      warning: alerts.filter((a) => a.severity === "warning").length,
    },
    "Fleet alerts checked",
  );

  return alerts;
}

/* ── System prompt formatter ───────────────────────────────────────── */

/**
 * Format alerts into a section suitable for injection into the system prompt.
 *
 * Only includes the section when there are warning or critical alerts.
 * Info-only alerts are omitted to keep the prompt compact.
 */
export function formatAlertContext(alerts: TokenAlert[]): string {
  const significant = alerts.filter(
    (a) => a.severity === "critical" || a.severity === "warning",
  );

  if (significant.length === 0) {
    return "";
  }

  const lines = ["## Active Alerts"];

  // Sort critical first, then warning
  const sorted = [...significant].sort((a, b) => {
    const order: Record<AlertSeverity, number> = { critical: 0, warning: 1, info: 2 };
    return order[a.severity] - order[b.severity];
  });

  for (const alert of sorted) {
    const tag = alert.severity.toUpperCase();
    lines.push(`- [${tag}] ${alert.title}: ${alert.message}`);
  }

  lines.push(
    "",
    "When relevant to the user's question, proactively mention these alerts and suggest fixes.",
  );

  return lines.join("\n");
}
