/**
 * Server-side frontend log ring — the data source behind GET /__bloby/control/logs/frontend
 * (the agent's "tail frontend / devtools logs") and the friendly "Copy error" flow.
 *
 * Two independent producers feed ONE in-memory ring, so the tail is never empty regardless of
 * how the frontend broke:
 *   1. The Vite dev server's customLogger (supervisor/vite-dev.ts) — COMPILE/transform errors,
 *      captured even when the browser never ran a line of JS (hard compile failure / blank page).
 *   2. The browser (supervisor/workspace-guard.js) POSTing window.onerror / unhandledrejection /
 *      console.error / console.warn / Vite-overlay text to POST /__bloby/control/fe-log — RUNTIME
 *      errors, which Vite never sees.
 *
 * Memory-only by design: the agent reads it over the loopback endpoint (no workspace file to grow
 * unbounded, pollute the dir, or self-trigger Vite's watcher). It is the current session's frontend
 * error trail; a supervisor restart clears it (frontend errors are transient by nature).
 */

export type FrontendLogKind =
  | 'error'
  | 'unhandledrejection'
  | 'console.error'
  | 'console.warn'
  | 'vite-error'
  | 'vite-warn'
  | 'vite-overlay';

export interface FrontendLogEntry {
  t: number;
  kind: FrontendLogKind;
  text: string;
  stack?: string;
}

const RING_MAX = 500;
const TEXT_CAP = 4000; // per-field clamp so one giant stack can't blow the ring's memory

const ring: FrontendLogEntry[] = [];

// Collapse the same message arriving repeatedly in a short window. The guard re-evaluates the Vite
// overlay on a 1.5s tick, and a crash loop can spam identical errors — without this the ring fills
// with one repeated line and pushes out the useful history.
let lastKey = '';
let lastAt = 0;

/** Append one frontend log entry to the ring. Best-effort, never throws, drops empty text.
 *  text is newline-stripped: the browser-facing POST /__bloby/control/fe-log endpoint is
 *  unauthenticated, and tailFrontendLog renders one entry per line — an embedded newline would let a
 *  remote caller forge a fake `<ts> [kind] ...` line that the (Bash-capable) agent reads as genuine.
 *  Collapsing newlines to a marker keeps each entry to exactly one line. (stack keeps its newlines:
 *  the renderer indents every stack line, so it can't masquerade as an un-indented log header.) */
export function appendFrontendLog(kind: FrontendLogKind, text: string, stack?: string): void {
  const clean = (text == null ? '' : String(text)).slice(0, TEXT_CAP).replace(/[\r\n]+/g, ' ⏎ ').trim();
  if (!clean) return;
  const stk = stack ? String(stack).slice(0, TEXT_CAP) : undefined;

  const key = kind + '|' + clean;
  const now = Date.now();
  if (key === lastKey && now - lastAt < 4000) { lastAt = now; return; }
  lastKey = key;
  lastAt = now;

  ring.push({ t: now, kind, text: clean, stack: stk });
  while (ring.length > RING_MAX) ring.shift();
}

/** Render the last `maxLines` ring lines as text (newest last). Each entry is one header line
 *  (`<ISO ts> [kind] text`) plus optional indented stack lines. */
export function tailFrontendLog(maxLines = 100): string {
  const lines: string[] = [];
  for (const e of ring) {
    lines.push(`${new Date(e.t).toISOString()} [${e.kind}] ${e.text}`);
    if (e.stack) lines.push('    ' + e.stack.replace(/\n/g, '\n    '));
  }
  return lines.slice(-Math.max(0, maxLines)).join('\n');
}

/** Number of entries currently buffered (surfaced as `clients`-independent count). */
export function getFrontendLogCount(): number {
  return ring.length;
}
