import { events, JobLogLevel } from "../globalEvents";
import { WorkerQueue } from "../workers/queues";

const DEBUG_ANSI = {
  reset: "\x1b[0m",
  bold: "\x1b[1m",
  dim: "\x1b[2m",
  red: "\x1b[31m",
  green: "\x1b[32m",
  yellow: "\x1b[33m",
  blue: "\x1b[34m",
  magenta: "\x1b[35m",
  cyan: "\x1b[36m",
  gray: "\x1b[90m",
} as const;

function classifyEvent(eventName: string) {
  const lowerEventName = eventName.toLowerCase();

  if (lowerEventName.includes(":error") || lowerEventName.includes("failed")) {
    return {
      label: "ERROR",
      color: DEBUG_ANSI.red,
    };
  }

  if (lowerEventName.includes(":skipped")) {
    return {
      label: "SKIP",
      color: DEBUG_ANSI.yellow,
    };
  }

  if (lowerEventName.includes(":started")) {
    return {
      label: "START",
      color: DEBUG_ANSI.cyan,
    };
  }

  if (
    lowerEventName.includes(":completed") ||
    lowerEventName.includes(":finished")
  ) {
    return {
      label: "DONE",
      color: DEBUG_ANSI.green,
    };
  }

  if (lowerEventName.includes(":check")) {
    return {
      label: "CHECK",
      color: DEBUG_ANSI.blue,
    };
  }

  if (
    lowerEventName.includes(":created") ||
    lowerEventName.includes(":assigned")
  ) {
    return {
      label: "CREATE",
      color: DEBUG_ANSI.green,
    };
  }

  if (
    lowerEventName.includes(":updated") ||
    lowerEventName.includes(":refresh")
  ) {
    return {
      label: "UPDATE",
      color: DEBUG_ANSI.cyan,
    };
  }

  if (
    lowerEventName.includes(":deleted") ||
    lowerEventName.includes(":removed") ||
    lowerEventName.includes(":invalidated") ||
    lowerEventName.includes(":terminated")
  ) {
    return {
      label: "REMOVE",
      color: DEBUG_ANSI.magenta,
    };
  }

  if (lowerEventName.endsWith(":log")) {
    return {
      label: lowerEventName.startsWith("job:") ? "JOB" : "LOG",
      color: DEBUG_ANSI.cyan,
    };
  }

  return {
    label: "EVENT",
    color: DEBUG_ANSI.gray,
  };
}

function summarizePayload(args: unknown[]) {
  if (!args.length) {
    return "no payload";
  }

  if (args.length > 1) {
    return `args=${args.length}`;
  }

  const [payload] = args;

  if (payload instanceof Error) {
    return `error=${payload.message}`;
  }

  if (payload === null || payload === undefined) {
    return "payload=empty";
  }

  if (typeof payload !== "object" || Array.isArray(payload)) {
    return `payload=${String(payload)}`;
  }

  const entries = Object.entries(payload as Record<string, unknown>);
  const usefulEntries = entries
    .filter(([key, value]) => {
      const keyLooksUseful =
        /(count|total|updated|status|duration|id|type|queue|worker|message|path|stale|skipped|created|removed|orphaned|cache|active|opportunity)/i.test(
          key,
        );
      const valueLooksSimple =
        typeof value === "string" ||
        typeof value === "number" ||
        typeof value === "boolean";

      return keyLooksUseful && valueLooksSimple;
    })
    .slice(0, 6)
    .map(([key, value]) => `${key}=${String(value)}`);

  if (usefulEntries.length) {
    return usefulEntries.join(" ");
  }

  const keys = Object.keys(payload).slice(0, 6);

  return keys.length ? `payloadKeys=${keys.join(",")}` : "payload=object";
}

interface DebugFlow {
  key: string;
  title: string;
  status: string;
  color: string;
  startedAt: number;
  updatedAt: number;
  children: string[];
  errorDetail: string[] | null;
}

type ProcessLabel = "API" | "WORKER";

interface EventDebuggerOptions {
  processLabel?: ProcessLabel;
}

type ConsoleEntry = {
  timestamp: number;
  level: "log" | "info" | "warn" | "error" | "debug";
  args: unknown[];
};

const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

const FLOW_STATUS_ICONS: Record<string, string> = {
  DONE: "✓",
  SKIP: "⊘",
  ERROR: "✗",
};

const dashboardState = {
  active: new Map<string, DebugFlow>(),
  completed: [] as DebugFlow[],
  spinnerFrame: 0,
  renderLoopId: null as ReturnType<typeof setInterval> | null,
  scrollOffset: 0,
  maxScrollOffset: 0,
  consoleLogs: [] as ConsoleEntry[],
  workerConsoleLogs: [] as ConsoleEntry[],
  connectedWorkerSocketIds: new Set<string>(),
  processLabel: "API" as ProcessLabel,
};

const MAX_CONSOLE_LOGS = 100;

// Maps event-name prefixes (e.g. "cache:opportunities:refresh") to the job flowKey
// that "owns" them, so global events emitted during a job appear nested in that job's flow.
const jobEventPrefixMap = new Map<string, string>();

function queueTypeToEventPrefix(queueType: WorkerQueue): string | null {
  // Convert "cache/opportunities/refresh/active" -> "cache:opportunities:refresh"
  // (drop the last path segment which is just the variant name)
  const parts = queueType.split("/");
  if (parts.length < 2) return null;
  return parts.slice(0, -1).join(":");
}

function formatQueueTypeTitle(queueType: WorkerQueue): string {
  return queueType
    .split("/")
    .map((s) => s.replaceAll("_", " "))
    .join(" > ");
}

const RENDER_INTERVAL_MS = 80;

function startRenderLoop() {
  if (dashboardState.renderLoopId !== null) {
    return;
  }

  dashboardState.renderLoopId = setInterval(() => {
    const now = Date.now();
    const hasActive = dashboardState.active.size > 0;
    const hasVisible = dashboardState.completed.some(
      (f) => f.status === "ERROR" || now - f.updatedAt < COMPLETED_FLOW_TTL_MS,
    );

    if (!hasActive && !hasVisible) {
      stopRenderLoop();
      return;
    }

    renderDashboard();
  }, RENDER_INTERVAL_MS);
}

function stopRenderLoop() {
  if (dashboardState.renderLoopId === null) {
    return;
  }

  clearInterval(dashboardState.renderLoopId);
  dashboardState.renderLoopId = null;
}

const TERMINAL_EVENT_LABELS = new Set(["DONE", "SKIP", "ERROR"]);
const FLOW_EVENT_LABELS = new Set([
  "CHECK",
  "START",
  "UPDATE",
  "DONE",
  "SKIP",
  "ERROR",
  "LOG",
  "JOB",
]);
const JOB_LOG_LEVEL_COLORS: Record<JobLogLevel, string> = {
  INFO: DEBUG_ANSI.cyan,
  WARN: DEBUG_ANSI.yellow,
  ERROR: DEBUG_ANSI.red,
  DEBUG: DEBUG_ANSI.gray,
};
const MAX_FLOW_CHILDREN = Math.max(
  5,
  Number(Bun.env.EVENT_DEBUGGER_MAX_FLOW_CHILDREN ?? "20") || 20,
);
const COMPLETED_FLOW_TTL_MS = 5 * 60 * 1000;
const MAX_COMPLETED_FLOW_CHILDREN = Math.max(
  1,
  Number(Bun.env.EVENT_DEBUGGER_MAX_COMPLETED_CHILDREN ?? "4") || 4,
);
const MAX_EVENT_NAME_WIDTH = Math.max(
  48,
  Number(Bun.env.EVENT_DEBUGGER_EVENT_WIDTH ?? "96") || 96,
);
const MAX_SUMMARY_WIDTH = Math.max(
  72,
  Number(Bun.env.EVENT_DEBUGGER_SUMMARY_WIDTH ?? "180") || 180,
);

function formatClockTime(timestampMs: number) {
  return new Date(timestampMs).toISOString().slice(11, 19);
}

function formatElapsed(startedAt: number, updatedAt: number) {
  return `${((updatedAt - startedAt) / 1000).toFixed(1)}s`;
}

function truncateText(value: string, maxLength: number) {
  if (value.length <= maxLength) {
    return value;
  }

  return `${value.slice(0, maxLength - 3)}...`;
}

function isTerminalLabel(label: string) {
  return TERMINAL_EVENT_LABELS.has(label);
}

function extractErrorFromArgs(args: unknown[]) {
  if (!args.length) {
    return null;
  }

  const [payload] = args;

  if (payload instanceof Error) {
    return payload;
  }

  if (
    payload !== null &&
    typeof payload === "object" &&
    !Array.isArray(payload)
  ) {
    const obj = payload as Record<string, unknown>;

    if (obj.error instanceof Error) {
      return obj.error;
    }

    if (obj.error !== null && typeof obj.error === "object") {
      return obj.error as Record<string, unknown>;
    }
  }

  return null;
}

function formatErrorDetail(error: Error | Record<string, unknown>) {
  const isNativeError = error instanceof Error;
  const message = isNativeError
    ? error.message
    : String((error as Record<string, unknown>).message ?? "Unknown error");
  const stack = isNativeError
    ? (error.stack ?? null)
    : (((error as Record<string, unknown>).stack as
        | string
        | null
        | undefined) ?? null);

  const lines: string[] = [];

  lines.push(
    `  ${DEBUG_ANSI.red}${DEBUG_ANSI.bold}${message}${DEBUG_ANSI.reset}`,
  );

  if (stack) {
    const stackLines = stack.split("\n");
    const firstLineIsMessage = stackLines[0]?.includes(message);
    const traceLines = firstLineIsMessage ? stackLines.slice(1) : stackLines;

    traceLines
      .filter((line) => line.trim())
      .forEach((line) => {
        lines.push(`  ${DEBUG_ANSI.gray}${line.trim()}${DEBUG_ANSI.reset}`);
      });
  }

  return lines;
}

function shouldTrackFlowEvent(eventName: string, label: string) {
  if (FLOW_EVENT_LABELS.has(label)) {
    return true;
  }

  if (eventName.includes(":refresh:")) {
    return true;
  }

  // Track any global event that belongs to an active job flow
  for (const prefix of jobEventPrefixMap.keys()) {
    if (eventName === prefix || eventName.startsWith(`${prefix}:`)) {
      return true;
    }
  }

  return false;
}

function deriveFlowKey(eventName: string) {
  const segments = eventName.split(":");
  const refreshIndex = segments.indexOf("refresh");

  if (refreshIndex >= 0) {
    return segments.slice(0, refreshIndex + 1).join(":");
  }

  if (segments.length < 2) {
    return eventName;
  }

  return segments.slice(0, -1).join(":");
}

function formatFlowTitle(flowKey: string) {
  return flowKey
    .split(":")
    .map((segment) => segment.replaceAll("_", " "))
    .join(" > ");
}

function createFlow(params: {
  flowKey: string;
  label: string;
  color: string;
  timestampMs: number;
  title?: string;
}) {
  const { flowKey, label, color, timestampMs, title } = params;

  return {
    key: flowKey,
    title: title ?? formatFlowTitle(flowKey),
    status: label,
    color,
    startedAt: timestampMs,
    updatedAt: timestampMs,
    children: [],
    errorDetail: null,
  } satisfies DebugFlow;
}

function buildChildLine(params: {
  timestampMs: number;
  label: string;
  color: string;
  eventName: string;
  summary: string;
}) {
  const { timestampMs, label, color, eventName, summary } = params;
  const shortEventName = truncateText(eventName, MAX_EVENT_NAME_WIDTH);
  const shortSummary = truncateText(summary, MAX_SUMMARY_WIDTH);

  return `${DEBUG_ANSI.dim}${formatClockTime(timestampMs)}${DEBUG_ANSI.reset} ${color}${label.padEnd(6, " ")}${DEBUG_ANSI.reset} ${shortEventName} ${DEBUG_ANSI.gray}-> ${shortSummary}${DEBUG_ANSI.reset}`;
}

function appendFlowChild(flow: DebugFlow, line: string) {
  flow.children.push(line);
  flow.children = flow.children.slice(-MAX_FLOW_CHILDREN);
}

function renderFlowHeader(flow: DebugFlow, active: boolean) {
  const icon = active
    ? `${flow.color}${SPINNER_FRAMES[dashboardState.spinnerFrame % SPINNER_FRAMES.length]}${DEBUG_ANSI.reset}`
    : `${flow.color}${FLOW_STATUS_ICONS[flow.status] ?? "·"}${DEBUG_ANSI.reset}`;
  const elapsedEnd = active ? Date.now() : flow.updatedAt;

  return `${icon} ${DEBUG_ANSI.dim}${formatClockTime(flow.updatedAt)}${DEBUG_ANSI.reset} ${flow.color}${DEBUG_ANSI.bold}[${flow.status}]${DEBUG_ANSI.reset} ${DEBUG_ANSI.bold}${flow.title}${DEBUG_ANSI.reset} ${DEBUG_ANSI.gray}(${formatElapsed(flow.startedAt, elapsedEnd)})${DEBUG_ANSI.reset}`;
}

function stripAnsi(str: string): string {
  return str.replace(/\x1b\[[0-9;]*m/g, "");
}

function truncateToWidth(str: string, maxWidth: number): string {
  const stripped = stripAnsi(str);
  if (stripped.length <= maxWidth) {
    return str;
  }

  // Need to truncate while preserving ANSI codes
  let visibleLen = 0;
  let result = "";
  let i = 0;

  while (i < str.length && visibleLen < maxWidth - 3) {
    if (str[i] === "\x1b") {
      // Consume entire ANSI sequence
      const end = str.indexOf("m", i);
      if (end !== -1) {
        result += str.slice(i, end + 1);
        i = end + 1;
        continue;
      }
    }
    result += str[i];
    visibleLen++;
    i++;
  }

  return result + "..." + DEBUG_ANSI.reset;
}

function padToWidth(str: string, width: number): string {
  const stripped = stripAnsi(str);
  const padding = Math.max(0, width - stripped.length);
  return str + " ".repeat(padding);
}

function wrapTextToWidth(str: string, maxWidth: number): string[] {
  if (maxWidth <= 0) return [str];

  const stripped = stripAnsi(str);
  if (stripped.length <= maxWidth) {
    return [str];
  }

  const lines: string[] = [];
  let currentLine = "";
  let currentVisibleLen = 0;
  let i = 0;

  while (i < str.length) {
    if (str[i] === "\x1b") {
      // Consume entire ANSI sequence
      const end = str.indexOf("m", i);
      if (end !== -1) {
        currentLine += str.slice(i, end + 1);
        i = end + 1;
        continue;
      }
    }

    // Check if adding this character would exceed width
    if (currentVisibleLen >= maxWidth) {
      lines.push(currentLine);
      currentLine = "";
      currentVisibleLen = 0;
    }

    currentLine += str[i];
    currentVisibleLen++;
    i++;
  }

  if (currentLine) {
    lines.push(currentLine);
  }

  return lines.length ? lines : [str];
}

function centerToWidth(str: string, width: number): string {
  const visibleLength = stripAnsi(str).length;
  if (visibleLength >= width) return truncateToWidth(str, width);
  const leftPad = Math.floor((width - visibleLength) / 2);
  return " ".repeat(leftPad) + str;
}

function getProcessHeaderLines(label: ProcessLabel): string[] {
  const color = label === "WORKER" ? DEBUG_ANSI.yellow : DEBUG_ANSI.green;

  const banner =
    label === "WORKER"
      ? [
          "__        _____  ____  _  _______ ____  ",
          "\\ \\      / / _ \\|  _ \\| |/ / ____|  _ \\ ",
          " \\ \\ /\\ / / | | | |_) | ' /|  _| | |_) |",
          "  \\ V  V /| |_| |  _ <| . \\| |___|  _ < ",
          "   \\_/\\_/  \\___/|_| \\_\\_|\\_\\_____|_| \\_\\",
        ]
      : [
          "    _    ____ ___ ",
          "   / \\  |  _ \\_ _|",
          "  / _ \\ | |_) | | ",
          " / ___ \\|  __/| | ",
          "/_/   \\_\\_|  |___|",
        ];

  return banner.map(
    (line) => `${DEBUG_ANSI.bold}${color}${line}${DEBUG_ANSI.reset}`,
  );
}

function formatLogArgs(args: unknown[]) {
  return args
    .map((arg) => {
      if (typeof arg === "string") return arg;
      if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
      try {
        return JSON.stringify(arg);
      } catch {
        return String(arg);
      }
    })
    .join(" ");
}

function appendConsoleSection(params: {
  lines: string[];
  title: string;
  logs: ConsoleEntry[];
  width: number;
  emptyText: string;
}) {
  const { lines, title, logs, width, emptyText } = params;
  lines.push(`${DEBUG_ANSI.bold}${DEBUG_ANSI.cyan}${title}${DEBUG_ANSI.reset}`);
  lines.push("");

  if (logs.length === 0) {
    lines.push(`${DEBUG_ANSI.dim}${emptyText}${DEBUG_ANSI.reset}`);
    return;
  }

  logs.forEach((log) => {
    const timestamp = formatClockTime(log.timestamp);
    let color: string = DEBUG_ANSI.gray;
    let label = "LOG";

    switch (log.level) {
      case "info":
        color = DEBUG_ANSI.cyan;
        label = "INFO";
        break;
      case "warn":
        color = DEBUG_ANSI.yellow;
        label = "WARN";
        break;
      case "error":
        color = DEBUG_ANSI.red;
        label = "ERROR";
        break;
      case "debug":
        color = DEBUG_ANSI.gray;
        label = "DEBUG";
        break;
    }

    const formatted = formatLogArgs(log.args);
    const logPrefix = `${DEBUG_ANSI.dim}${timestamp}${DEBUG_ANSI.reset} ${color}${label.padEnd(5)}${DEBUG_ANSI.reset} `;
    const prefixWidth = stripAnsi(logPrefix).length;
    const contentWidth = width - prefixWidth;
    const wrappedLines = wrapTextToWidth(formatted, Math.max(10, contentWidth));

    lines.push(logPrefix + (wrappedLines[0] || ""));

    const indent = " ".repeat(prefixWidth);
    for (let i = 1; i < wrappedLines.length; i++) {
      lines.push(indent + wrappedLines[i]);
    }
  });
}

function mapJobLevelToConsoleLevel(
  level: JobLogLevel | undefined,
): ConsoleEntry["level"] {
  if (level === "WARN") return "warn";
  if (level === "ERROR") return "error";
  if (level === "DEBUG") return "debug";
  if (level === "INFO") return "info";
  return "log";
}

function renderDashboard() {
  if (!process.stdout.isTTY) {
    return;
  }

  dashboardState.spinnerFrame += 1;
  const now = Date.now();

  // Prune expired non-error completed flows
  dashboardState.completed = dashboardState.completed.filter(
    (f) => f.status === "ERROR" || now - f.updatedAt < COMPLETED_FLOW_TTL_MS,
  );

  const terminalCols = process.stdout.columns || 120;
  const terminalRows = process.stdout.rows || 24;
  const processHeaderLines = getProcessHeaderLines(dashboardState.processLabel);

  // Split: left 2/3 for events, right 1/3 for console logs
  const dividerWidth = 1;
  const leftWidth = Math.floor(((terminalCols - dividerWidth) * 2) / 3);
  const rightWidth = terminalCols - leftWidth - dividerWidth;

  // Build event lines (left panel)
  const completedFlows = [...dashboardState.completed];
  const activeFlows = [...dashboardState.active.values()];
  const activeJobFlows = activeFlows.filter((flow) =>
    flow.key.startsWith("job:"),
  ).length;
  const completedJobFlows = completedFlows.filter((flow) =>
    flow.key.startsWith("job:"),
  ).length;
  const failedJobFlows = completedFlows.filter(
    (flow) => flow.key.startsWith("job:") && flow.status === "ERROR",
  ).length;
  const headerStatsLines =
    dashboardState.processLabel === "WORKER"
      ? []
      : [
          `${DEBUG_ANSI.bold}Connected workers:${DEBUG_ANSI.reset} ${DEBUG_ANSI.green}${dashboardState.connectedWorkerSocketIds.size}${DEBUG_ANSI.reset}`,
          `${DEBUG_ANSI.bold}PG Boss jobs:${DEBUG_ANSI.reset} active ${DEBUG_ANSI.cyan}${activeJobFlows}${DEBUG_ANSI.reset} · completed ${DEBUG_ANSI.green}${completedJobFlows}${DEBUG_ANSI.reset} · failed ${DEBUG_ANSI.red}${failedJobFlows}${DEBUG_ANSI.reset}`,
        ];
  const eventLines: string[] = [];

  eventLines.push(
    `${DEBUG_ANSI.bold}${DEBUG_ANSI.cyan}━━━ EVENT FLOWS ━━━${DEBUG_ANSI.reset}`,
  );
  eventLines.push("");

  completedFlows.forEach((flow) => {
    eventLines.push(renderFlowHeader(flow, false));

    const childSlice =
      flow.status === "ERROR"
        ? flow.children
        : flow.children.slice(-MAX_COMPLETED_FLOW_CHILDREN);

    childSlice.forEach((line) => {
      eventLines.push(
        `  - ${DEBUG_ANSI.gray}final:${DEBUG_ANSI.reset} ${line}`,
      );
    });

    if (
      flow.status !== "ERROR" &&
      flow.children.length > MAX_COMPLETED_FLOW_CHILDREN
    ) {
      const hiddenCount = flow.children.length - MAX_COMPLETED_FLOW_CHILDREN;
      eventLines.push(
        `  ${DEBUG_ANSI.gray}... ${hiddenCount} older event(s) hidden${DEBUG_ANSI.reset}`,
      );
    }

    if (flow.errorDetail) {
      flow.errorDetail.forEach((line) => eventLines.push(line));
    }

    eventLines.push("");
  });

  activeFlows.forEach((flow, flowIndex) => {
    eventLines.push(renderFlowHeader(flow, true));
    flow.children.forEach((line) => {
      eventLines.push(`  - ${line}`);
    });

    if (flow.errorDetail) {
      flow.errorDetail.forEach((line) => eventLines.push(line));
    }

    if (flowIndex < activeFlows.length - 1) {
      eventLines.push("");
    }
  });

  if (completedFlows.length === 0 && activeFlows.length === 0) {
    eventLines.push(`${DEBUG_ANSI.dim}No active flows${DEBUG_ANSI.reset}`);
  }

  // Build console log lines (right panel)
  const consoleLines: string[] = [];
  appendConsoleSection({
    lines: consoleLines,
    title: "━━━ CONSOLE ━━━",
    logs: dashboardState.consoleLogs,
    width: rightWidth,
    emptyText: "No console output",
  });

  const headerDividerLine = `${DEBUG_ANSI.dim}${"─".repeat(Math.max(1, terminalCols))}${DEBUG_ANSI.reset}`;

  // Calculate scrolling for left panel
  const headerBlockRows =
    processHeaderLines.length + headerStatsLines.length + 1;
  const availableRows = Math.max(1, terminalRows - 1 - headerBlockRows);
  dashboardState.maxScrollOffset = Math.max(
    0,
    eventLines.length - availableRows,
  );
  dashboardState.scrollOffset = Math.max(
    0,
    Math.min(dashboardState.scrollOffset, dashboardState.maxScrollOffset),
  );

  const visibleEventLines = eventLines.slice(
    dashboardState.scrollOffset,
    dashboardState.scrollOffset + availableRows,
  );

  // For console, show most recent logs that fit
  const visibleConsoleLines = consoleLines.slice(-availableRows);

  // Move cursor to home and clear screen
  process.stdout.write("\x1b[H\x1b[J");

  processHeaderLines.forEach((line) => {
    process.stdout.write(`${centerToWidth(line, terminalCols)}\n`);
  });
  headerStatsLines.forEach((line) => {
    process.stdout.write(`${centerToWidth(line, terminalCols)}\n`);
  });
  process.stdout.write(`${headerDividerLine}\n`);

  // Render side by side
  const maxRows = Math.max(
    visibleEventLines.length,
    visibleConsoleLines.length,
  );
  const divider = `${DEBUG_ANSI.dim}│${DEBUG_ANSI.reset}`;

  for (let row = 0; row < availableRows && row < maxRows; row++) {
    const leftLine = visibleEventLines[row] ?? "";
    const rightLine = visibleConsoleLines[row] ?? "";

    const truncatedLeft = truncateToWidth(leftLine, leftWidth);
    const paddedLeft = padToWidth(truncatedLeft, leftWidth);
    // Don't truncate right side since we're wrapping it properly now
    const rightContent = rightLine;

    process.stdout.write(`${paddedLeft}${divider}${rightContent}\n`);
  }

  // Fill remaining rows if needed
  for (let row = maxRows; row < availableRows; row++) {
    const paddedLeft = " ".repeat(leftWidth);
    process.stdout.write(`${paddedLeft}${divider}\n`);
  }

  // Status bar at the bottom
  const canScrollUp = dashboardState.scrollOffset > 0;
  const canScrollDown =
    dashboardState.scrollOffset < dashboardState.maxScrollOffset;
  let scrollHint = "";

  if (canScrollUp && canScrollDown) {
    scrollHint = `↑/↓ scroll · ${dashboardState.scrollOffset + 1}-${dashboardState.scrollOffset + visibleEventLines.length}/${eventLines.length}`;
  } else if (canScrollUp) {
    scrollHint = `↑ scroll up · ${dashboardState.scrollOffset + 1}-${dashboardState.scrollOffset + visibleEventLines.length}/${eventLines.length}`;
  } else if (canScrollDown) {
    scrollHint = `↓ scroll down · 1-${visibleEventLines.length}/${eventLines.length}`;
  }

  const statusLeft = scrollHint
    ? `${DEBUG_ANSI.dim}[${scrollHint}]${DEBUG_ANSI.reset}`
    : "";
  const statusRight = `${DEBUG_ANSI.dim}[${dashboardState.processLabel} · c=clear, q=quit]${DEBUG_ANSI.reset}`;

  const statusLeftPadded = padToWidth(statusLeft, leftWidth);
  process.stdout.write(`${statusLeftPadded}${divider}${statusRight}`);
}

function logSingleLine(params: {
  timestampMs: number;
  label: string;
  color: string;
  eventName: string;
  summary: string;
}) {
  const { timestampMs, label, color, eventName, summary } = params;
  const shortEventName = truncateText(eventName, MAX_EVENT_NAME_WIDTH);
  const shortSummary = truncateText(summary, MAX_SUMMARY_WIDTH);

  const line = `${DEBUG_ANSI.dim}${formatClockTime(timestampMs)}${DEBUG_ANSI.reset} ${color}${label.padEnd(6, " ")}${DEBUG_ANSI.reset} ${shortEventName} ${DEBUG_ANSI.gray}-> ${shortSummary}${DEBUG_ANSI.reset}`;

  console.log(line);
}

function deriveJobAwareFlowKey(eventName: string, args: unknown[]): string {
  if (eventName.startsWith("job:")) {
    const payload = args[0];
    if (
      payload !== null &&
      typeof payload === "object" &&
      !Array.isArray(payload)
    ) {
      const { workerId } = payload as Record<string, unknown>;
      if (typeof workerId === "string") {
        return `job:${workerId}`;
      }
    }
    return "job";
  }

  // Check if this global event belongs to an active job by prefix
  for (const [prefix, jobFlowKey] of jobEventPrefixMap.entries()) {
    if (eventName === prefix || eventName.startsWith(`${prefix}:`)) {
      return jobFlowKey;
    }
  }

  return deriveFlowKey(eventName);
}

function extractSocketId(args: unknown[]): string | null {
  const [payload] = args;

  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    return null;
  }

  const data = payload as { socket?: { id?: unknown } };
  return typeof data.socket?.id === "string" ? data.socket.id : null;
}

let debuggerSetup = false;

export function setupEventDebugger(options?: EventDebuggerOptions) {
  if (debuggerSetup) return;
  debuggerSetup = true;

  dashboardState.processLabel = options?.processLabel ?? "API";

  const interactive =
    process.stdout.isTTY && process.env.EVENT_DEBUGGER_DYNAMIC !== "false";

  // Intercept console methods in interactive mode
  if (interactive) {
    const originalConsole = {
      log: console.log,
      info: console.info,
      warn: console.warn,
      error: console.error,
      debug: console.debug,
    };

    const captureConsole = (
      level: "log" | "info" | "warn" | "error" | "debug",
    ) => {
      return (...args: unknown[]) => {
        dashboardState.consoleLogs.push({
          timestamp: Date.now(),
          level,
          args,
        });

        // Keep only last N logs
        if (dashboardState.consoleLogs.length > MAX_CONSOLE_LOGS) {
          dashboardState.consoleLogs =
            dashboardState.consoleLogs.slice(-MAX_CONSOLE_LOGS);
        }

        // Trigger re-render
        renderDashboard();
      };
    };

    console.log = captureConsole("log");
    console.info = captureConsole("info");
    console.warn = captureConsole("warn");
    console.error = captureConsole("error");
    console.debug = captureConsole("debug");

    // Restore on cleanup
    const restoreConsole = () => {
      console.log = originalConsole.log;
      console.info = originalConsole.info;
      console.warn = originalConsole.warn;
      console.error = originalConsole.error;
      console.debug = originalConsole.debug;
    };

    // Enter alternate screen buffer for dynamic mode
    if (process.stdout.isTTY) {
      // Switch to alternate screen buffer
      process.stdout.write("\x1b[?1049h");
      // Hide cursor
      process.stdout.write("\x1b[?25l");

      // Enable raw mode for keyboard input
      if (process.stdin.isTTY) {
        process.stdin.setRawMode(true);
        process.stdin.resume();
        process.stdin.setEncoding("utf8");

        process.stdin.on("data", (key: string) => {
          // Ctrl+C or 'q' to quit
          if (key === "\u0003" || key === "q") {
            cleanup();
            process.exit(0);
          }

          // 'c' to clear console logs
          if (key === "c") {
            dashboardState.consoleLogs = [];
            dashboardState.workerConsoleLogs = [];
            renderDashboard();
          }

          // Arrow up or 'k'
          if (key === "\u001b[A" || key === "k") {
            dashboardState.scrollOffset = Math.max(
              0,
              dashboardState.scrollOffset - 1,
            );
            renderDashboard();
          }

          // Arrow down or 'j'
          if (key === "\u001b[B" || key === "j") {
            dashboardState.scrollOffset = Math.min(
              dashboardState.maxScrollOffset,
              dashboardState.scrollOffset + 1,
            );
            renderDashboard();
          }

          // Page Up
          if (key === "\u001b[5~") {
            const pageSize = (process.stdout.rows || 24) - 5;
            dashboardState.scrollOffset = Math.max(
              0,
              dashboardState.scrollOffset - pageSize,
            );
            renderDashboard();
          }

          // Page Down
          if (key === "\u001b[6~") {
            const pageSize = (process.stdout.rows || 24) - 5;
            dashboardState.scrollOffset = Math.min(
              dashboardState.maxScrollOffset,
              dashboardState.scrollOffset + pageSize,
            );
            renderDashboard();
          }

          // Home or 'g'
          if (key === "\u001b[H" || key === "g") {
            dashboardState.scrollOffset = 0;
            renderDashboard();
          }

          // End or 'G'
          if (key === "\u001b[F" || key === "G") {
            dashboardState.scrollOffset = dashboardState.maxScrollOffset;
            renderDashboard();
          }
        });
      }

      // Restore on exit
      const cleanup = () => {
        // Restore console
        restoreConsole();
        // Restore raw mode
        if (process.stdin.isTTY) {
          process.stdin.setRawMode(false);
          process.stdin.pause();
        }
        // Show cursor
        process.stdout.write("\x1b[?25h");
        // Switch back to main screen buffer
        process.stdout.write("\x1b[?1049l");
      };

      process.on("exit", cleanup);
      process.on("SIGINT", () => {
        cleanup();
        process.exit(0);
      });
      process.on("SIGTERM", () => {
        cleanup();
        process.exit(0);
      });
    }
  }

  events.any((eventName, ...args) => {
    const formattedEventName = String(eventName);
    const timestampMs = Date.now();

    if (formattedEventName === "worker:io:connection") {
      const socketId = extractSocketId(args);
      if (socketId) {
        dashboardState.connectedWorkerSocketIds.add(socketId);
        dashboardState.workerConsoleLogs.push({
          timestamp: timestampMs,
          level: "info",
          args: [`connected socket=${socketId}`],
        });
      }
    }

    if (formattedEventName === "worker:io:disconnect") {
      const socketId = extractSocketId(args);
      if (socketId) {
        dashboardState.connectedWorkerSocketIds.delete(socketId);
        dashboardState.workerConsoleLogs.push({
          timestamp: timestampMs,
          level: "warn",
          args: [`disconnected socket=${socketId}`],
        });
      }
    }

    const { label, color: baseColor } = classifyEvent(formattedEventName);
    const summary = summarizePayload(args);
    const flowKey = deriveJobAwareFlowKey(formattedEventName, args);
    const trackInFlow = shouldTrackFlowEvent(formattedEventName, label);

    // For job:log, override color by log level from payload
    const isJobLog = formattedEventName === "job:log";
    const isJobEvent = formattedEventName.startsWith("job:");
    const jobPayload = isJobEvent
      ? (args[0] as
          | { level?: JobLogLevel; workerId?: string; queueType?: WorkerQueue }
          | undefined)
      : null;
    const effectiveColor =
      isJobLog && jobPayload?.level
        ? (JOB_LOG_LEVEL_COLORS[jobPayload.level] ?? baseColor)
        : baseColor;
    const effectiveLabel =
      isJobLog && jobPayload?.level ? jobPayload.level : label;

    if (isJobLog && jobPayload?.workerId) {
      const jobLogPayload = args[0] as
        | {
            message?: string;
            queueType?: WorkerQueue;
            level?: JobLogLevel;
            workerId?: string;
          }
        | undefined;
      const workerTag = `#${jobPayload.workerId.slice(0, 8)}`;
      const queueTag = jobLogPayload?.queueType
        ? ` ${formatQueueTypeTitle(jobLogPayload.queueType)}`
        : "";
      const message = jobLogPayload?.message ?? summary;

      dashboardState.workerConsoleLogs.push({
        timestamp: timestampMs,
        level: mapJobLevelToConsoleLevel(jobPayload.level),
        args: [`${workerTag}${queueTag} ${message}`],
      });
    }

    if (dashboardState.workerConsoleLogs.length > MAX_CONSOLE_LOGS) {
      dashboardState.workerConsoleLogs =
        dashboardState.workerConsoleLogs.slice(-MAX_CONSOLE_LOGS);
    }

    if (!interactive) {
      logSingleLine({
        timestampMs,
        label: effectiveLabel,
        color: effectiveColor,
        eventName: formattedEventName,
        summary,
      });

      if (label === "ERROR") {
        const errorObj = extractErrorFromArgs(args);

        if (errorObj) {
          formatErrorDetail(errorObj).forEach((line) => console.log(line));
        }
      }

      return;
    }

    if (!trackInFlow) {
      // In interactive mode, non-flow events still trigger a re-render
      // to keep the display fresh. The event itself won't appear in flows.
      renderDashboard();
      return;
    }

    let existingFlow = dashboardState.active.get(flowKey);

    // If no active flow for a job-keyed event, it arrived late (after job:finished
    // already fired via the sync event bus). Route to the completed entry if present,
    // otherwise discard — never create a new active flow that has no terminator.
    if (
      !existingFlow &&
      flowKey.startsWith("job:") &&
      formattedEventName !== "job:started"
    ) {
      const completedFlow = dashboardState.completed.find(
        (f) => f.key === flowKey,
      );
      if (completedFlow) {
        completedFlow.children = [
          ...completedFlow.children,
          buildChildLine({
            timestampMs,
            label: effectiveLabel,
            color: effectiveColor,
            eventName: formattedEventName,
            summary,
          }),
        ].slice(-MAX_FLOW_CHILDREN);
        renderDashboard();
      }
      return;
    }

    if (!existingFlow) {
      // Build a descriptive title for job flows
      const jobFlowTitle =
        flowKey.startsWith("job:") &&
        jobPayload?.queueType &&
        jobPayload?.workerId
          ? `[JOB] ${formatQueueTypeTitle(jobPayload.queueType)} ${DEBUG_ANSI.dim}#${jobPayload.workerId.slice(0, 8)}${DEBUG_ANSI.reset}`
          : undefined;

      existingFlow = createFlow({
        flowKey,
        label,
        color: baseColor,
        timestampMs,
        title: jobFlowTitle,
      });
      dashboardState.active.set(flowKey, existingFlow);
      startRenderLoop();

      // Register event prefix so global events emitted during this job nest here
      if (flowKey.startsWith("job:") && jobPayload?.queueType) {
        const prefix = queueTypeToEventPrefix(jobPayload.queueType);
        if (prefix) {
          jobEventPrefixMap.set(prefix, flowKey);
        }
      }
    } else if (
      // Update title once we have the full queueType + workerId (e.g. from job:started)
      flowKey.startsWith("job:") &&
      jobPayload?.queueType &&
      jobPayload?.workerId &&
      existingFlow.title === formatFlowTitle(flowKey)
    ) {
      existingFlow.title = `[JOB] ${formatQueueTypeTitle(jobPayload.queueType)} ${DEBUG_ANSI.dim}#${jobPayload.workerId.slice(0, 8)}${DEBUG_ANSI.reset}`;
      // Register prefix if not done yet
      const prefix = queueTypeToEventPrefix(jobPayload.queueType);
      if (prefix && !jobEventPrefixMap.has(prefix)) {
        jobEventPrefixMap.set(prefix, flowKey);
      }
    }

    existingFlow.updatedAt = timestampMs;

    appendFlowChild(
      existingFlow,
      buildChildLine({
        timestampMs,
        label: effectiveLabel,
        color: effectiveColor,
        eventName: formattedEventName,
        summary,
      }),
    );

    if (label === "ERROR") {
      const errorObj = extractErrorFromArgs(args);
      existingFlow.errorDetail = errorObj ? formatErrorDetail(errorObj) : null;
    }

    // Only terminate a job flow on explicit job lifecycle events, not on
    // nested global events that happen to carry DONE/SKIP labels.
    const isJobTerminalEvent =
      formattedEventName === "job:finished" ||
      formattedEventName === "job:error";
    const canTerminate =
      isTerminalLabel(label) &&
      (!flowKey.startsWith("job:") || isJobTerminalEvent);

    if (canTerminate) {
      existingFlow.status = label;
      existingFlow.color = baseColor;
      dashboardState.active.delete(flowKey);

      // Unregister job event prefix when the job terminates and
      // dispose of any orphaned active flows that share that prefix.
      for (const [prefix, mappedKey] of jobEventPrefixMap.entries()) {
        if (mappedKey === flowKey) {
          jobEventPrefixMap.delete(prefix);

          // Remove any active flow whose key starts with this prefix
          // (could have been created before the prefix map was registered).
          for (const activeKey of dashboardState.active.keys()) {
            if (activeKey === prefix || activeKey.startsWith(`${prefix}:`)) {
              dashboardState.active.delete(activeKey);
            }
          }
        }
      }

      const completedEntry = {
        ...existingFlow,
        children: [...existingFlow.children],
        errorDetail: existingFlow.errorDetail
          ? [...existingFlow.errorDetail]
          : null,
      };

      const existingIndex = dashboardState.completed.findIndex(
        (f) => f.key === flowKey,
      );

      if (existingIndex >= 0) {
        dashboardState.completed[existingIndex] = completedEntry;
      } else {
        dashboardState.completed.push(completedEntry);
      }
    }

    renderDashboard();
  });
}
