// Configurable alert rules for market microstructure quality.
// - Spread (bps) crossing warn/critical thresholds per coin
// - Tick latency crossing warn/critical thresholds per coin
//
// Rules live in localStorage so they survive reloads without a backend
// round-trip. A default "global" rule applies to every symbol; per-symbol
// overrides win when present. Firing is de-duplicated with a cooldown so a
// briefly widened spread doesn't spam the toast column.

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import type { TickerState } from "@/hooks/use-delta-tickers";
import { firePushNotification } from "@/lib/push-notifications";

const STORAGE_KEY = "delta.alertRules.v1";

export type AlertSeverity = "warn" | "critical";
export type AlertKind = "spread" | "latency";

export interface AlertThresholds {
  /** Enable spread-bps rule. */
  spreadEnabled: boolean;
  /** Warn when spread (bps) is >= this value. */
  spreadWarnBps: number;
  /** Critical when spread (bps) is >= this value. */
  spreadCritBps: number;
  /** Enable tick-latency rule. */
  latencyEnabled: boolean;
  /** Warn when time since last tick is >= this value (ms). */
  latencyWarnMs: number;
  /** Critical when time since last tick is >= this value (ms). */
  latencyCritMs: number;
}

export interface AlertRulesState {
  /** Global default applied to every symbol without a specific override. */
  global: AlertThresholds;
  /** Per-symbol overrides, keyed by product symbol. */
  overrides: Record<string, AlertThresholds>;
  /** Cooldown between repeat toasts for the same (symbol, kind), in ms. */
  cooldownMs: number;
  /** Master switch — when off nothing fires. */
  enabled: boolean;
}

export const DEFAULT_THRESHOLDS: AlertThresholds = {
  spreadEnabled: true,
  spreadWarnBps: 20,
  spreadCritBps: 50,
  latencyEnabled: true,
  latencyWarnMs: 3_000,
  latencyCritMs: 10_000,
};

export const DEFAULT_RULES: AlertRulesState = {
  global: DEFAULT_THRESHOLDS,
  overrides: {},
  cooldownMs: 60_000,
  enabled: true,
};

function readRules(): AlertRulesState {
  if (typeof window === "undefined") return DEFAULT_RULES;
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (!raw) return DEFAULT_RULES;
    const parsed = JSON.parse(raw) as Partial<AlertRulesState>;
    return {
      global: { ...DEFAULT_THRESHOLDS, ...(parsed.global ?? {}) },
      overrides: parsed.overrides ?? {},
      cooldownMs: parsed.cooldownMs ?? DEFAULT_RULES.cooldownMs,
      enabled: parsed.enabled ?? true,
    };
  } catch {
    return DEFAULT_RULES;
  }
}

function writeRules(rules: AlertRulesState) {
  if (typeof window === "undefined") return;
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(rules));
  window.dispatchEvent(new CustomEvent("delta:alert-rules-changed"));
}

export function useAlertRules() {
  const [rules, setRules] = useState<AlertRulesState>(() => readRules());

  useEffect(() => {
    const onChange = () => setRules(readRules());
    window.addEventListener("delta:alert-rules-changed", onChange);
    window.addEventListener("storage", onChange);
    return () => {
      window.removeEventListener("delta:alert-rules-changed", onChange);
      window.removeEventListener("storage", onChange);
    };
  }, []);

  const update = useCallback((patch: Partial<AlertRulesState>) => {
    const next = { ...readRules(), ...patch };
    writeRules(next);
    setRules(next);
  }, []);

  const setGlobal = useCallback((patch: Partial<AlertThresholds>) => {
    const cur = readRules();
    const next = { ...cur, global: { ...cur.global, ...patch } };
    writeRules(next);
    setRules(next);
  }, []);

  const setOverride = useCallback((symbol: string, patch: Partial<AlertThresholds> | null) => {
    const cur = readRules();
    const overrides = { ...cur.overrides };
    if (patch === null) {
      delete overrides[symbol];
    } else {
      const base = overrides[symbol] ?? cur.global;
      overrides[symbol] = { ...base, ...patch };
    }
    const next = { ...cur, overrides };
    writeRules(next);
    setRules(next);
  }, []);

  const reset = useCallback(() => {
    writeRules(DEFAULT_RULES);
    setRules(DEFAULT_RULES);
  }, []);

  const resolve = useCallback(
    (symbol: string): AlertThresholds => rules.overrides[symbol] ?? rules.global,
    [rules],
  );

  return { rules, update, setGlobal, setOverride, reset, resolve };
}

/**
 * Symbols the alert engine needs live data for = the union of every symbol
 * with a per-symbol override, so the runner can subscribe even when the user
 * isn't on the dashboard. The global rule alone doesn't force a subscription
 * — it fires only against tickers the app already streams.
 */
export function useAlertWatchSymbols(): string[] {
  const { rules } = useAlertRules();
  return useMemo(() => Object.keys(rules.overrides).sort(), [rules.overrides]);
}

type Severity = AlertSeverity | null;

function spreadSeverity(bps: number | undefined, t: AlertThresholds): Severity {
  if (!t.spreadEnabled || bps == null || !Number.isFinite(bps)) return null;
  if (bps >= t.spreadCritBps) return "critical";
  if (bps >= t.spreadWarnBps) return "warn";
  return null;
}

function latencySeverity(ageMs: number, t: AlertThresholds): Severity {
  if (!t.latencyEnabled) return null;
  if (ageMs >= t.latencyCritMs) return "critical";
  if (ageMs >= t.latencyWarnMs) return "warn";
  return null;
}

function fmtAge(ms: number) {
  if (ms < 1000) return `${ms}ms`;
  const s = ms / 1000;
  if (s < 60) return `${s.toFixed(s < 10 ? 1 : 0)}s`;
  return `${Math.floor(s / 60)}m`;
}

/**
 * Evaluates configured rules against the given tickers on every animation
 * frame (throttled to ~1Hz) and emits sonner toasts when thresholds are
 * crossed. Repeated alerts for the same (symbol, kind) are suppressed until
 * `cooldownMs` elapses; severity escalations (warn → critical) bypass the
 * cooldown so the operator sees the worse state immediately.
 */
export function useAlertsEngine(tickers: Record<string, TickerState>) {
  const { rules, resolve } = useAlertRules();
  const lastFiredRef = useRef<Record<string, { at: number; severity: AlertSeverity }>>({});

  useEffect(() => {
    if (!rules.enabled) return;
    const iv = window.setInterval(() => {
      const now = Date.now();
      for (const symbol in tickers) {
        const t = tickers[symbol];
        const th = resolve(symbol);

        const checks: Array<{ kind: AlertKind; sev: AlertSeverity; detail: string }> = [];
        const sSev = spreadSeverity(t.spread_bps, th);
        if (sSev !== null) {
          checks.push({
            kind: "spread",
            sev: sSev,
            detail: `spread ${t.spread_bps?.toFixed(1)} bps (≥ ${sSev === "critical" ? th.spreadCritBps : th.spreadWarnBps})`,
          });
        }
        const age = now - (t.lastUpdate ?? now);
        const lSev = latencySeverity(age, th);
        if (lSev !== null) {
          checks.push({
            kind: "latency",
            sev: lSev,
            detail: `tick ${fmtAge(age)} stale (≥ ${fmtAge(lSev === "critical" ? th.latencyCritMs : th.latencyWarnMs)})`,
          });
        }

        for (const c of checks) {
          const key = `${symbol}:${c.kind}`;
          const prev = lastFiredRef.current[key];
          const escalated = !prev || (prev.severity === "warn" && c.sev === "critical");
          const cooled = !prev || now - prev.at >= rules.cooldownMs;
          if (!escalated && !cooled) continue;
          lastFiredRef.current[key] = { at: now, severity: c.sev };
          const title = `${symbol} · ${c.kind === "spread" ? "Spread alert" : "Latency alert"}`;
          if (c.sev === "critical") {
            toast.error(title, { description: c.detail, duration: 6_000 });
          } else {
            toast.warning(title, { description: c.detail, duration: 5_000 });
          }
          // Fire a background-visible OS notification too. `firePushNotification`
          // no-ops unless the user has opted in from the Alert Rules card.
          firePushNotification({ title, body: c.detail, severity: c.sev, tag: key });
        }

        // Auto-clear the cooldown key when conditions recover so the next
        // breach fires immediately rather than waiting out the window.
        if (!sSev) delete lastFiredRef.current[`${symbol}:spread`];
        if (!lSev) delete lastFiredRef.current[`${symbol}:latency`];
      }
    }, 1_000);
    return () => window.clearInterval(iv);
  }, [tickers, rules.enabled, rules.cooldownMs, resolve]);
}
