// Crash / uncaught-error recovery.
//
// The React <ErrorBoundary> already catches render errors. This module covers
// the OTHER half: async code outside React (realtime handlers, Promises
// that never hit a .catch, setTimeout callbacks, LiveKit event listeners,
// etc.). Those bubble to window.onerror / onunhandledrejection and would
// otherwise disappear silently in production.
//
// Behaviour:
//   - First error: emit a toast + log.
//   - Repeated identical errors within DEDUPE_MS: swallow (no spam).
//   - More than BURST_LIMIT distinct errors within BURST_WINDOW_MS: force a
//     full page reload. Something is systematically broken and the UI
//     probably can't recover in-place.

export interface CrashEntry {
  id: string;
  message: string;
  stack: string | null;
  source: 'window' | 'promise';
  at: number;
}

type Listener = (entry: CrashEntry) => void;

const DEDUPE_MS = 10_000;
const BURST_WINDOW_MS = 30_000;
const BURST_LIMIT = 8;

const listeners = new Set<Listener>();
const recent = new Map<string, number>(); // message hash → last seen
let burst: number[] = [];
let installed = false;

function hash(message: string): string {
  let h = 0;
  for (let i = 0; i < message.length; i++) {
    h = ((h << 5) - h + message.charCodeAt(i)) | 0;
  }
  return h.toString(36);
}

function emit(entry: CrashEntry): void {
  // Dedupe on the message hash so a handler that fires every animation
  // frame doesn't drown the UI in toasts.
  const key = hash(entry.message);
  const now = entry.at;
  const last = recent.get(key);
  if (last && now - last < DEDUPE_MS) return;
  recent.set(key, now);
  // Purge stale dedupe entries to keep the map bounded.
  for (const [k, ts] of recent) {
    if (now - ts > DEDUPE_MS * 3) recent.delete(k);
  }

  // Burst detection: drop samples older than window, count what's left.
  burst = burst.filter((t) => now - t < BURST_WINDOW_MS);
  burst.push(now);
  if (burst.length >= BURST_LIMIT) {
    console.error('[crash-recovery] burst threshold reached — reloading');
    // Let existing toast handlers see the final entry first, then reload.
    // Delay lets the log flush + any pending realtime ack go through.
    window.setTimeout(() => window.location.reload(), 500);
  }

  for (const fn of listeners) {
    try {
      fn(entry);
    } catch (err) {
      // Listener itself threw — log but don't recurse.
      console.error('[crash-recovery] listener threw', err);
    }
  }
}

function toEntry(
  err: unknown,
  source: CrashEntry['source'],
): CrashEntry {
  const message =
    err instanceof Error
      ? err.message
      : typeof err === 'string'
        ? err
        : (() => {
            try {
              return JSON.stringify(err);
            } catch {
              return String(err);
            }
          })();
  const stack = err instanceof Error ? err.stack ?? null : null;
  return {
    id: crypto.randomUUID(),
    message: message || 'unknown error',
    stack,
    source,
    at: Date.now(),
  };
}

export function installCrashHandlers(): void {
  if (installed) return;
  installed = true;

  window.addEventListener('error', (ev: ErrorEvent) => {
    // Some events (ResizeObserver loop, benign extension injections) arrive
    // as errors without a real message. Skip those to avoid spam.
    if (!ev.message && !ev.error) return;
    emit(toEntry(ev.error ?? ev.message, 'window'));
  });

  window.addEventListener('unhandledrejection', (ev: PromiseRejectionEvent) => {
    emit(toEntry(ev.reason, 'promise'));
  });
}

export function subscribeCrashes(fn: Listener): () => void {
  listeners.add(fn);
  return () => {
    listeners.delete(fn);
  };
}

// Test / escape hatch — lets the reload threshold get checked in dev.
export function reportManualCrash(err: unknown): void {
  emit(toEntry(err, 'window'));
}
