/**
 * crashGuard.ts — process-level last-resort crash guard (THIN / DELETABLE 护栏).
 *
 * WHY THIS EXISTS:
 *   0.3.15+ enables WS robustness knobs on createLarkChannel (handshakeTimeoutMs,
 *   wsConfig.pingTimeout — see lark/channelClient.ts). When a handshake hangs and
 *   that timeout fires, the SDK aborts the underlying `_WebSocket`, which emits a
 *   RAW 'error' event on the socket. That low-level socket has no 'error' listener
 *   we can reliably attach (node-sdk 1.67.0's WSClient is not an EventEmitter and
 *   keeps the raw ws in a closure — no public accessor). So Node's default rule
 *   applies: an 'error' EventEmitter event with no listener is RE-THROWN as an
 *   uncaughtException (events.js:486 `throw er`). With one bridge process serving
 *   multiple bots, that single unhandled throw kills the WHOLE process → every bot
 *   drops at once (observed twice in one day on the mini). There was no net.
 *
 * WHAT IT DOES:
 *   Install last-resort uncaughtException / unhandledRejection handlers that LOG
 *   the full error (incl. stack) and DELIBERATELY DO NOT call process.exit — the
 *   bridge must stay alive ("bridge never suicides" 铁律). A transient WS handshake
 *   timeout must not take the whole fleet offline.
 *   ONE exception: a sustained crash STORM (see createCrashStormBreaker below)
 *   trips a circuit breaker and exits 1 — that pattern means "wedged", not
 *   "weathering a transient", and a supervisor restart is the better outcome.
 *
 * HONEST TRADE-OFF (not zero-risk):
 *   uncaughtException is, in the general case, a signal that the process MAY be in
 *   an indeterminate state, and swallowing it can in theory mask a real corruption.
 *   We accept that because (a) the dominant real-world trigger here is a benign,
 *   well-understood transient (the raw WS 'error' above), (b) we NEVER swallow
 *   silently — every catch logs the full stack so anomalies stay visible, and
 *   (c) an external supervisor (launchd / start-bridge.sh) restarts the process if
 *   it ever does wedge. Staying up for the common case beats a fleet-wide outage.
 *
 * DELETABILITY:
 *   This is a thin, isolated module. Once the transport layer owns these errors
 *   directly (e.g. the split-out `@larksuite/channel` package attaching its own
 *   ws 'error' listener, or a node-sdk that no longer re-throws), delete this file
 *   and its single call site in main.ts.
 *
 * BOUNDARY:
 *   The guard's own logging is the last line of defence — if the injected logger
 *   ITSELF throws, that throw is NOT caught here and would re-enter the crash path
 *   (a second uncaughtException). The default logger is `console`, whose `.error`
 *   does not throw in normal operation, so this is a non-issue in production; it's
 *   only a caveat for an exotic custom logger.
 */
import { inspect } from "node:util";

/** Default logger — overridable in tests so we can assert "logged, never exited". */
export interface CrashGuardLogger {
  error: (...args: unknown[]) => void;
}

/** Handle uncaughtException: log full stack, NEVER exit. Exported for testing. */
export function handleUncaughtException(
  err: Error,
  origin: string,
  log: CrashGuardLogger = console,
): void {
  log.error(
    `[larkway] uncaughtException (origin=${origin}) — bridge STAYS UP (crash guard). ` +
      `Likely a transient WS handshake-timeout raw socket error; if it recurs, investigate:`,
    err,
  );
}

/** Handle unhandledRejection: log full reason, NEVER exit. Exported for testing. */
export function handleUnhandledRejection(reason: unknown, log: CrashGuardLogger = console): void {
  // An Error reason is passed through (full stack stays visible). A non-Error
  // reason is rendered with util.inspect so an object keeps its code/detail
  // fields instead of collapsing to "[object Object]" via String().
  const rendered = reason instanceof Error ? reason : inspect(reason, { depth: 4 });
  log.error(
    "[larkway] unhandledRejection — bridge STAYS UP (crash guard). Full reason:",
    rendered,
  );
}

// ── crash-storm circuit breaker ──────────────────────────────────────────────
// "Never suicide" is right for the common case (one benign transient every now
// and then), but with NO lower bound a genuinely wedged process — a corrupted
// state throwing the same error on every tick — would sit forever logging while
// the status.json heartbeat keeps reporting green ("僵而不死"). The breaker
// puts a floor under that: a sustained storm (far above anything a WS
// handshake blip can produce) exits non-zero so a supervisor restarts us with
// a clean process. Hosts running bare `larkway start` still fare better with a
// visible dead process than an invisible wedged one.

// Threshold sizing: the benign trigger produces ~0.5 hits/min/bot (one raw WS
// 'error' per handshake-timeout abort; the SDK reconnects at 120s + jitter, no
// tight loop — verified against node-sdk 1.67.0). Even 10 simultaneously
// black-holed bots ≈ 5-10 hits/min. A wedged process throwing per tick
// produces hundreds/min, so 50 keeps a wide benign margin without hurting
// detection.
const BREAKER_THRESHOLD = 50; // crashes…
const BREAKER_WINDOW_MS = 60_000; // …within this window → trip

/**
 * Returns a `note()` that records one crash and reports whether the breaker
 * tripped. Exported for testing; registerCrashGuard wires it to process.exit.
 */
export function createCrashStormBreaker(
  threshold = BREAKER_THRESHOLD,
  windowMs = BREAKER_WINDOW_MS,
): () => boolean {
  const hits: number[] = [];
  return function note(): boolean {
    const now = Date.now();
    hits.push(now);
    // Drop entries older than the window before judging. (The array can
    // briefly exceed the threshold under a sub-second storm; bounded by the
    // trip itself — once note() returns true the process exits.)
    while (hits.length > 0 && hits[0]! < now - windowMs) hits.shift();
    return hits.length >= threshold;
  };
}

/**
 * Register the process-level crash guard. Idempotent-friendly: callers should
 * invoke once near startup (main.ts). Does not return a teardown — the guard is
 * meant to live for the whole process lifetime.
 */
export function registerCrashGuard(log: CrashGuardLogger = console): void {
  const noteCrash = createCrashStormBreaker();
  const maybeTrip = (): void => {
    if (!noteCrash()) return;
    log.error(
      `[larkway] crash-storm breaker TRIPPED: ≥${BREAKER_THRESHOLD} crash-guard hits ` +
        `within ${BREAKER_WINDOW_MS / 1000}s — the process is likely wedged, not weathering ` +
        `a transient. Exiting 1 so a supervisor can restart with a clean process.`,
    );
    process.exit(1);
  };
  process.on("uncaughtException", (err: Error, origin: string) => {
    handleUncaughtException(err, origin, log);
    maybeTrip();
  });
  process.on("unhandledRejection", (reason: unknown) => {
    handleUnhandledRejection(reason, log);
    maybeTrip();
  });
}
