import { defaultQaModelForMode, isQaFastModeEnabled } from "../../model-selection.js";
import { normalizeCaptureSavedView, normalizeCaptureSavedViews } from "./capture-saved-view.js";
import { formatErrorMessage } from "./errors.js";
import {
  type Bootstrap,
  type OutcomesEnvelope,
  type ReportEnvelope,
  type RunnerSelection,
  type Snapshot,
  type TabId,
  type CaptureEventsEnvelope,
  type CaptureCoverageEnvelope,
  type CaptureQueryEnvelope,
  type CaptureSessionsEnvelope,
  type CaptureStartupStatusEnvelope,
  type CaptureSavedView,
  type UiState,
  renderQaLabUi,
} from "./ui-render.js";
async function getJson<T>(path: string): Promise<T> {
  const response = await fetch(path);
  if (!response.ok) {
    throw new Error(`${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

async function getJsonNoStore<T>(path: string): Promise<T> {
  const response = await fetch(path, { cache: "no-store" });
  if (!response.ok) {
    throw new Error(`${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

async function postJson<T>(path: string, body: unknown): Promise<T> {
  const response = await fetch(path, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    const payload = (await response.json().catch(() => ({}))) as { error?: string };
    throw new Error(payload.error || `${response.status} ${response.statusText}`);
  }
  return (await response.json()) as T;
}

function countCaptureDimension(
  events: UiState["captureEvents"],
  pick: (event: UiState["captureEvents"][number]) => string | undefined,
) {
  const counts = new Map<string, number>();
  for (const event of events) {
    const value = pick(event)?.trim();
    if (!value) {
      continue;
    }
    counts.set(value, (counts.get(value) ?? 0) + 1);
  }
  return [...counts.entries()]
    .map(([value, count]) => ({ value, count }))
    .toSorted((left, right) => right.count - left.count || left.value.localeCompare(right.value));
}

function summarizeCaptureCoverageFromEvents(
  sessionIds: string[],
  events: UiState["captureEvents"],
): UiState["captureCoverage"] {
  const unlabeledEventCount = events.filter(
    (event) => !event.provider?.trim() && !event.api?.trim() && !event.model?.trim(),
  ).length;
  return {
    sessionId: sessionIds.join(","),
    totalEvents: events.length,
    unlabeledEventCount,
    providers: countCaptureDimension(events, (event) => event.provider),
    apis: countCaptureDimension(events, (event) => event.api),
    models: countCaptureDimension(events, (event) => event.model),
    hosts: countCaptureDimension(events, (event) => event.host),
    localPeers: countCaptureDimension(events, (event) => {
      const host = event.host?.trim();
      return host && /^(127\.0\.0\.1|localhost)(:\d+)?$/i.test(host) ? host : undefined;
    }),
  };
}

function defaultModelsForProviderMode(
  mode: RunnerSelection["providerMode"],
  bootstrap?: Bootstrap | null,
): Pick<RunnerSelection, "primaryModel" | "alternateModel" | "fastMode"> {
  const preferredLiveModel = bootstrap?.runnerCatalog.real[0]?.key;
  if (mode === "live-frontier") {
    const primaryModel = defaultQaModelForMode(mode, { preferredLiveModel });
    const alternateModel = defaultQaModelForMode(mode, { alternate: true, preferredLiveModel });
    return {
      primaryModel,
      alternateModel,
      fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }),
    };
  }
  const primaryModel = defaultQaModelForMode(mode);
  const alternateModel = defaultQaModelForMode(mode, { alternate: true });
  return {
    primaryModel,
    alternateModel,
    fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }),
  };
}

function detectTheme(): "light" | "dark" {
  const stored = localStorage.getItem("qa-lab-theme");
  if (stored === "light" || stored === "dark") {
    return stored;
  }
  return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}

function detectSidebarCollapsed(): boolean {
  return localStorage.getItem("qa-lab-sidebar-collapsed") === "1";
}

function detectSidebarPanel(): UiState["sidebarPanel"] {
  const stored = localStorage.getItem("qa-lab-sidebar-panel");
  return stored === "config" || stored === "run" ? stored : "scenarios";
}

const CAPTURE_SAVED_VIEWS_KEY = "qa-lab-capture-saved-views";

function loadCaptureSavedViews(): CaptureSavedView[] {
  try {
    const raw = localStorage.getItem(CAPTURE_SAVED_VIEWS_KEY);
    if (!raw) {
      return [];
    }
    return normalizeCaptureSavedViews(JSON.parse(raw) as unknown);
  } catch {
    return [];
  }
}

function persistCaptureSavedViews(savedViews: CaptureSavedView[]) {
  localStorage.setItem(
    CAPTURE_SAVED_VIEWS_KEY,
    JSON.stringify(normalizeCaptureSavedViews(savedViews)),
  );
}

function isEditableElement(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) {
    return false;
  }
  return (
    target.isContentEditable ||
    target instanceof HTMLInputElement ||
    target instanceof HTMLTextAreaElement ||
    target instanceof HTMLSelectElement
  );
}

export async function createQaLabApp(root: HTMLDivElement) {
  const state: UiState = {
    theme: detectTheme(),
    bootstrap: null,
    snapshot: null,
    latestReport: null,
    scenarioRun: null,
    captureSessions: [],
    captureEvents: [],
    captureQueryPreset: "none",
    captureQueryRows: [],
    captureKindFilter: [],
    captureProviderFilter: [],
    captureHostFilter: [],
    captureSearchText: "",
    captureHeaderMode: "key",
    captureViewMode: "list",
    captureGroupMode: "none",
    captureTimelineLaneMode: "domain",
    captureTimelineLaneSort: "most-events",
    captureTimelinePreviousLaneSort: null,
    captureTimelineLaneSearch: "",
    captureTimelineZoom: 100,
    captureTimelineSparklineMode: "session-relative",
    captureTimelineWindowStartPct: null,
    captureTimelineWindowEndPct: null,
    captureTimelineBrushAnchorPct: null,
    captureTimelineBrushCurrentPct: null,
    captureTimelineFocusSelectedFlow: false,
    captureTimelineFocusedLaneMode: "all",
    captureTimelineFocusedLaneThreshold: "any",
    captureDetailPlacement: "right",
    captureDetailSplitPct: 34,
    captureDetailSplitDragging: false,
    captureDetailView: "overview",
    capturePreferredDetailView: null,
    captureFlowDetailLayout: null,
    capturePayloadDetailLayout: null,
    capturePayloadExtent: "preview",
    capturePayloadEventSort: "stream",
    capturePayloadEventFilter: "",
    captureErrorsOnly: false,
    captureCoverage: null,
    captureStartupStatus: null,
    captureControlsExpanded: false,
    captureSummaryExpanded: false,
    captureSavedViews: loadCaptureSavedViews(),
    captureSelectedSessionsExpanded: false,
    sidebarCollapsed: detectSidebarCollapsed(),
    sidebarPanel: detectSidebarPanel(),
    captureCollapsedLaneIds: [],
    capturePinnedLaneIds: [],
    selectedCaptureSessionIds: [],
    selectedCaptureEventKey: null,
    selectedConversationId: null,
    selectedThreadId: null,
    selectedScenarioId: null,
    activeTab: "chat",
    runnerDraft: null,
    runnerDraftDirty: false,
    composer: {
      conversationKind: "direct",
      conversationId: "alice",
      senderId: "alice",
      senderName: "Alice",
      text: "",
    },
    busy: false,
    error: null,
  };

  /* Track whether user has scrolled up in the chat */
  let chatScrollLocked = true;
  let previousMessageCount = 0;

  /* ---------- Render guards (avoid DOM churn during polling) ---------- */

  let lastFingerprint = "";
  let renderDeferred = false;
  let previousRunnerStatus: string | null = null;
  let currentUiVersion: string | null = null;
  let syncingCaptureTimelineScroll = false;
  let sparklineSweepActive = false;
  let sparklineSweepAnchorStartPct: number | null = null;
  let sparklineSweepAnchorEndPct: number | null = null;
  let sparklineSweepCurrentStartPct: number | null = null;
  let sparklineSweepCurrentEndPct: number | null = null;
  let captureGlobalListenersBound = false;

  function stateFingerprint(): string {
    const msgs = state.snapshot?.messages;
    const ev = state.snapshot?.events;
    return JSON.stringify({
      mc: msgs?.length ?? 0,
      lm: msgs && msgs.length > 0 ? msgs[msgs.length - 1].id : null,
      cc: state.snapshot?.conversations.length ?? 0,
      tc: state.snapshot?.threads.length ?? 0,
      ec: ev?.length ?? 0,
      lc: ev && ev.length > 0 ? ev[ev.length - 1].cursor : -1,
      rs: state.bootstrap?.runner.status,
      ra: state.bootstrap?.runner.startedAt,
      rf: state.bootstrap?.runner.finishedAt,
      re: state.bootstrap?.runner.error,
      ss: state.scenarioRun?.status,
      sc: state.scenarioRun?.counts,
      so: state.scenarioRun?.scenarios.map((o) => o.status).join(","),
      rp: state.latestReport?.generatedAt,
      cs: state.bootstrap?.runnerCatalog.status,
      cl: state.bootstrap?.runnerCatalog.real.length ?? 0,
      cps: state.captureSessions.length,
      cse: state.captureSessions[0]?.eventCount ?? 0,
      cei: state.selectedCaptureSessionIds.join(","),
      cec: state.captureEvents.length,
      ceh: state.captureEvents[0]?.host ?? null,
      ccp: state.captureQueryPreset,
      ccq: state.captureQueryRows.length,
      ccv: state.captureCoverage?.totalEvents ?? 0,
      ccpv: state.captureCoverage?.providers[0]?.value ?? null,
      ccss: state.captureStartupStatus?.proxy.ok ?? null,
      ccsg: state.captureStartupStatus?.gateway.ok ?? null,
      ccce: state.captureControlsExpanded,
      ccse: state.captureSummaryExpanded,
      ccsx: state.captureSelectedSessionsExpanded,
      ccsv: state.captureSavedViews.map((view) => `${view.id}:${view.name}`).join("|"),
      scc: state.sidebarCollapsed,
      scp: state.sidebarPanel,
      cck: state.captureKindFilter.join(","),
      ccpf: state.captureProviderFilter.join(","),
      cchf: state.captureHostFilter.join(","),
      cchm: state.captureHeaderMode,
      ccgm: state.captureGroupMode,
      cctl: state.captureTimelineLaneMode,
      ccts: state.captureTimelineLaneSort,
      cctps: state.captureTimelinePreviousLaneSort,
      cctq: state.captureTimelineLaneSearch,
      cctz: state.captureTimelineZoom,
      cctsm: state.captureTimelineSparklineMode,
      cctws: state.captureTimelineWindowStartPct,
      cctwe: state.captureTimelineWindowEndPct,
      cctba: state.captureTimelineBrushAnchorPct,
      cctbc: state.captureTimelineBrushCurrentPct,
      cctff: state.captureTimelineFocusSelectedFlow,
      cctfm: state.captureTimelineFocusedLaneMode,
      cctft: state.captureTimelineFocusedLaneThreshold,
      ccdp: state.captureDetailPlacement,
      ccds: state.captureDetailSplitPct,
      ccdsd: state.captureDetailSplitDragging,
      ccdv: state.captureDetailView,
      ccpdv: state.capturePreferredDetailView,
      ccdfl: state.captureFlowDetailLayout,
      ccdpl: state.capturePayloadDetailLayout,
      ccdpe: state.capturePayloadExtent,
      ccpes: state.capturePayloadEventSort,
      ccpef: state.capturePayloadEventFilter,
      ccli: state.captureCollapsedLaneIds.join(","),
      ccpi: state.capturePinnedLaneIds.join(","),
      er: state.error,
    });
  }

  function isSelectOpen(): boolean {
    const active = document.activeElement;
    return !!active && root.contains(active) && active.tagName === "SELECT";
  }

  /* ---------- Data fetching ---------- */

  async function refresh() {
    try {
      const [bootstrap, snapshot, report, outcomes] = await Promise.all([
        getJson<Bootstrap>("/api/bootstrap"),
        getJson<Snapshot>("/api/state"),
        getJson<ReportEnvelope>("/api/report"),
        getJson<OutcomesEnvelope>("/api/outcomes"),
      ]);
      state.bootstrap = bootstrap;
      state.snapshot = snapshot;
      state.latestReport = report.report ?? bootstrap.latestReport;
      state.scenarioRun = outcomes.run;
      if (!state.runnerDraft || !state.runnerDraftDirty) {
        state.runnerDraft = {
          ...bootstrap.runner.selection,
          scenarioIds: [...bootstrap.runner.selection.scenarioIds],
        };
        state.runnerDraftDirty = false;
      }
      if (!state.selectedConversationId) {
        state.selectedConversationId = snapshot.conversations[0]?.id ?? null;
      }
      if (!state.selectedScenarioId) {
        state.selectedScenarioId = bootstrap.scenarios[0]?.id ?? null;
      }
      if (!state.composer.conversationId) {
        state.composer = {
          ...state.composer,
          conversationKind: bootstrap.defaults.conversationKind,
          conversationId: bootstrap.defaults.conversationId,
          senderId: bootstrap.defaults.senderId,
          senderName: bootstrap.defaults.senderName,
        };
      }
      state.error = null;
    } catch (error) {
      state.error = formatErrorMessage(error);
    }

    try {
      const sessions = await getJson<CaptureSessionsEnvelope>("/api/capture/sessions");
      const startupStatusPromise = getJson<CaptureStartupStatusEnvelope>(
        "/api/capture/startup-status",
      );
      state.captureSessions = sessions.sessions;
      const availableSessionIds = new Set(sessions.sessions.map((session) => session.id));
      state.selectedCaptureSessionIds = state.selectedCaptureSessionIds.filter((id) =>
        availableSessionIds.has(id),
      );
      if (state.selectedCaptureSessionIds.length === 0) {
        state.selectedCaptureSessionIds = sessions.sessions[0]?.id ? [sessions.sessions[0].id] : [];
      }
      const startupStatusResult = await Promise.allSettled([startupStatusPromise]);
      state.captureStartupStatus =
        startupStatusResult[0]?.status === "fulfilled" ? startupStatusResult[0].value.status : null;
      if (state.selectedCaptureSessionIds.length > 0) {
        const eventsPromises = state.selectedCaptureSessionIds.map((sessionId) =>
          getJson<CaptureEventsEnvelope>(
            `/api/capture/events?sessionId=${encodeURIComponent(sessionId)}`,
          ),
        );
        const singleSessionId =
          state.selectedCaptureSessionIds.length === 1 ? state.selectedCaptureSessionIds[0] : null;
        const coveragePromise = singleSessionId
          ? getJson<CaptureCoverageEnvelope>(
              `/api/capture/coverage?sessionId=${encodeURIComponent(singleSessionId)}`,
            )
          : Promise.resolve<CaptureCoverageEnvelope | null>(null);
        const queryPromise =
          state.captureQueryPreset === "none"
            ? Promise.resolve<CaptureQueryEnvelope>({ rows: [] })
            : singleSessionId
              ? getJson<CaptureQueryEnvelope>(
                  `/api/capture/query?sessionId=${encodeURIComponent(
                    singleSessionId,
                  )}&preset=${encodeURIComponent(state.captureQueryPreset)}`,
                )
              : Promise.resolve<CaptureQueryEnvelope>({ rows: [] });
        const [eventsResult, coverageResult, queryResult] = await Promise.allSettled([
          Promise.all(eventsPromises),
          coveragePromise,
          queryPromise,
        ]);
        if (eventsResult.status !== "fulfilled") {
          throw eventsResult.reason;
        }
        state.captureEvents = eventsResult.value
          .flatMap((envelope) => envelope.events)
          .toSorted(
            (left, right) =>
              right.ts - left.ts || String(right.id ?? "").localeCompare(String(left.id ?? "")),
          );
        state.captureCoverage =
          coverageResult.status === "fulfilled" && coverageResult.value
            ? coverageResult.value.coverage
            : summarizeCaptureCoverageFromEvents(
                state.selectedCaptureSessionIds,
                state.captureEvents,
              );
        state.captureQueryRows = queryResult.status === "fulfilled" ? queryResult.value.rows : [];
        if (
          !state.selectedCaptureEventKey ||
          !state.captureEvents.some(
            (event) =>
              `${event.id ?? "no-id"}:${event.flowId}:${event.ts}:${event.kind}` ===
              state.selectedCaptureEventKey,
          )
        ) {
          const first = state.captureEvents[0];
          state.selectedCaptureEventKey = first
            ? `${first.id ?? "no-id"}:${first.flowId}:${first.ts}:${first.kind}`
            : null;
        }
      } else {
        state.captureEvents = [];
        state.captureCoverage = null;
        state.captureQueryRows = [];
        state.selectedCaptureEventKey = null;
      }
    } catch (error) {
      state.error = formatErrorMessage(error);
    }

    /* Auto-switch to chat when a run starts so user can watch live */
    const currentRunnerStatus = state.bootstrap?.runner.status ?? null;
    if (currentRunnerStatus === "running" && previousRunnerStatus !== "running") {
      state.activeTab = "chat";
      chatScrollLocked = true;
    }
    previousRunnerStatus = currentRunnerStatus;

    /* Only re-render when data actually changed; defer if a <select> is open */
    const fp = stateFingerprint();
    if (fp !== lastFingerprint) {
      lastFingerprint = fp;
      renderDeferred = true;
    }
    if (renderDeferred && !isSelectOpen()) {
      renderDeferred = false;
      render();
    }
  }

  async function pollUiVersion() {
    if (document.visibilityState === "hidden") {
      return;
    }
    try {
      const payload = await getJsonNoStore<{ version: string | null }>("/api/ui-version");
      if (!currentUiVersion) {
        currentUiVersion = payload.version;
        return;
      }
      if (payload.version && payload.version !== currentUiVersion) {
        window.location.reload();
      }
    } catch {
      // Ignore transient rebuild windows while the dist dir is being rewritten.
    }
  }

  /* ---------- Draft mutations ---------- */

  function updateRunnerDraft(mutator: (draft: RunnerSelection) => RunnerSelection) {
    const fallback = state.bootstrap?.runner.selection;
    if (!state.runnerDraft && fallback) {
      state.runnerDraft = { ...fallback, scenarioIds: [...fallback.scenarioIds] };
    }
    if (!state.runnerDraft) {
      return;
    }
    state.runnerDraft = mutator(state.runnerDraft);
    state.runnerDraftDirty = true;
    render();
  }

  /* ---------- Actions ---------- */

  async function runSelfCheck() {
    state.busy = true;
    state.error = null;
    render();
    try {
      const result = await postJson<{ report: string; outputPath: string }>(
        "/api/scenario/self-check",
        {},
      );
      state.latestReport = {
        outputPath: result.outputPath,
        markdown: result.report,
        generatedAt: new Date().toISOString(),
      };
      state.activeTab = "report";
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function resetState() {
    state.busy = true;
    render();
    try {
      await postJson("/api/reset", {});
      state.latestReport = null;
      state.selectedThreadId = null;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function sendInbound() {
    const conversationId = state.composer.conversationId.trim();
    const text = state.composer.text.trim();
    if (!conversationId || !text) {
      state.error = "Conversation id and text are required.";
      render();
      return;
    }
    state.busy = true;
    state.error = null;
    render();
    try {
      await postJson("/api/inbound/message", {
        conversation: {
          id: conversationId,
          kind: state.composer.conversationKind,
          ...(state.composer.conversationKind === "channel" ? { title: conversationId } : {}),
        },
        senderId: state.composer.senderId.trim() || "alice",
        senderName: state.composer.senderName.trim() || undefined,
        text,
        ...(state.selectedThreadId ? { threadId: state.selectedThreadId } : {}),
      });
      state.selectedConversationId = conversationId;
      state.composer.text = "";
      chatScrollLocked = true;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function runSuite() {
    if (!state.runnerDraft) {
      state.error = "Runner selection not ready yet.";
      render();
      return;
    }
    state.busy = true;
    state.error = null;
    render();
    try {
      const result = await postJson<{ runner: { selection: RunnerSelection } }>(
        "/api/scenario/suite",
        {
          providerMode: state.runnerDraft.providerMode,
          primaryModel: state.runnerDraft.primaryModel,
          alternateModel: state.runnerDraft.alternateModel,
          scenarioIds: state.runnerDraft.scenarioIds,
        },
      );
      state.runnerDraft = {
        ...result.runner.selection,
        scenarioIds: [...result.runner.selection.scenarioIds],
      };
      state.runnerDraftDirty = false;
      state.activeTab = "chat";
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  async function sendKickoff() {
    state.busy = true;
    state.error = null;
    render();
    try {
      await postJson("/api/kickoff", {});
      state.activeTab = "chat";
      chatScrollLocked = true;
      await refresh();
    } catch (error) {
      state.error = formatErrorMessage(error);
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  function downloadReport() {
    if (!state.latestReport?.markdown) {
      return;
    }
    const blob = new Blob([state.latestReport.markdown], { type: "text/markdown;charset=utf-8" });
    const href = URL.createObjectURL(blob);
    const anchor = document.createElement("a");
    anchor.href = href;
    anchor.download = "qa-report.md";
    anchor.click();
    URL.revokeObjectURL(href);
  }

  function toggleTheme() {
    state.theme = state.theme === "dark" ? "light" : "dark";
    localStorage.setItem("qa-lab-theme", state.theme);
    render();
  }

  function toggleSidebar() {
    state.sidebarCollapsed = !state.sidebarCollapsed;
    localStorage.setItem("qa-lab-sidebar-collapsed", state.sidebarCollapsed ? "1" : "0");
    render();
  }

  function setSidebarPanel(panel: UiState["sidebarPanel"]) {
    state.sidebarPanel = panel;
    localStorage.setItem("qa-lab-sidebar-panel", panel);
    if (state.sidebarCollapsed) {
      state.sidebarCollapsed = false;
      localStorage.setItem("qa-lab-sidebar-collapsed", "0");
    }
    render();
  }

  function applyCaptureSavedView(view: CaptureSavedView) {
    const normalized = normalizeCaptureSavedView(view);
    if (!normalized) {
      return;
    }
    state.selectedCaptureSessionIds = [...normalized.sessionIds];
    state.captureKindFilter = [...normalized.kindFilter];
    state.captureProviderFilter = [...normalized.providerFilter];
    state.captureHostFilter = [...normalized.hostFilter];
    state.captureSearchText = normalized.searchText;
    state.captureHeaderMode = normalized.headerMode;
    state.captureViewMode = normalized.viewMode;
    state.captureGroupMode = normalized.groupMode;
    state.captureTimelineLaneMode = normalized.timelineLaneMode;
    state.captureTimelineLaneSort = normalized.timelineLaneSort;
    state.captureTimelineZoom = normalized.timelineZoom;
    state.captureTimelineSparklineMode = normalized.timelineSparklineMode;
    state.captureErrorsOnly = normalized.errorsOnly;
    state.captureDetailPlacement = normalized.detailPlacement;
    state.capturePayloadDetailLayout = normalized.payloadLayout;
    state.capturePayloadExtent = normalized.payloadExtent;
    state.selectedCaptureEventKey = null;
  }

  function buildCaptureSavedView(name: string): CaptureSavedView {
    return {
      id: crypto.randomUUID(),
      name,
      sessionIds: [...state.selectedCaptureSessionIds],
      kindFilter: [...state.captureKindFilter],
      providerFilter: [...state.captureProviderFilter],
      hostFilter: [...state.captureHostFilter],
      searchText: state.captureSearchText,
      headerMode: state.captureHeaderMode,
      viewMode: state.captureViewMode,
      groupMode: state.captureGroupMode,
      timelineLaneMode: state.captureTimelineLaneMode,
      timelineLaneSort: state.captureTimelineLaneSort,
      timelineZoom: state.captureTimelineZoom,
      timelineSparklineMode: state.captureTimelineSparklineMode,
      errorsOnly: state.captureErrorsOnly,
      detailPlacement: state.captureDetailPlacement,
      payloadLayout: state.capturePayloadDetailLayout,
      payloadExtent: state.capturePayloadExtent,
    };
  }

  /* ---------- Chat scroll tracking ---------- */

  function trackChatScroll() {
    const el = root.querySelector<HTMLElement>("#chat-messages");
    if (!el) {
      return;
    }
    el.addEventListener("scroll", () => {
      const threshold = 40;
      chatScrollLocked = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
    });
  }

  function scrollChatToBottom(force?: boolean) {
    const el = root.querySelector<HTMLElement>("#chat-messages");
    if (!el) {
      return;
    }
    const newCount = state.snapshot?.messages.length ?? 0;
    if (force || (chatScrollLocked && newCount !== previousMessageCount)) {
      el.scrollTop = el.scrollHeight;
    }
    previousMessageCount = newCount;
  }

  /* ---------- Event binding ---------- */

  function bindEvents() {
    /* Tabs */
    root.querySelectorAll<HTMLElement>("[data-tab]").forEach((node) => {
      node.addEventListener("click", () => {
        const nextTab = node.dataset.tab as TabId | undefined;
        if (nextTab) {
          state.activeTab = nextTab;
          render();
        }
      });
    });

    /* Conversation chips */
    root.querySelectorAll<HTMLElement>("[data-conversation-id]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedConversationId = node.dataset.conversationId ?? null;
        state.selectedThreadId = null;
        if (state.activeTab !== "chat") {
          state.activeTab = "chat";
        }
        render();
      });
    });

    /* Thread chips */
    root.querySelectorAll<HTMLElement>("[data-thread-select]").forEach((node) => {
      node.addEventListener("click", () => {
        const val = node.dataset.threadSelect;
        if (val === "root") {
          state.selectedThreadId = null;
        } else {
          state.selectedThreadId = val ?? null;
          const conv = node.dataset.threadConv;
          if (conv) {
            state.selectedConversationId = conv;
          }
        }
        render();
      });
    });

    /* Scenario selection (results tab + sidebar) */
    root.querySelectorAll<HTMLElement>("[data-scenario-id]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedScenarioId = node.dataset.scenarioId ?? null;
        if (state.activeTab !== "results") {
          state.activeTab = "results";
        }
        render();
      });
    });

    /* Header / sidebar buttons */
    root
      .querySelector<HTMLElement>("[data-action='refresh']")
      ?.addEventListener("click", () => void refresh());
    root
      .querySelector<HTMLElement>("[data-action='reset']")
      ?.addEventListener("click", () => void resetState());
    root
      .querySelector<HTMLElement>("[data-action='toggle-theme']")
      ?.addEventListener("click", toggleTheme);
    root
      .querySelector<HTMLElement>("[data-action='toggle-sidebar']")
      ?.addEventListener("click", toggleSidebar);
    root.querySelectorAll<HTMLElement>("[data-sidebar-panel]").forEach((node) => {
      node.addEventListener("click", () => {
        const panel = node.dataset.sidebarPanel;
        if (panel === "config" || panel === "run" || panel === "scenarios") {
          setSidebarPanel(panel);
        }
      });
    });
    root
      .querySelector<HTMLElement>("[data-action='self-check']")
      ?.addEventListener("click", () => void runSelfCheck());
    root
      .querySelector<HTMLElement>("[data-action='run-suite']")
      ?.addEventListener("click", () => void runSuite());
    root
      .querySelector<HTMLElement>("[data-action='kickoff']")
      ?.addEventListener("click", () => void sendKickoff());
    root
      .querySelector<HTMLElement>("[data-action='send']")
      ?.addEventListener("click", () => void sendInbound());
    root
      .querySelector<HTMLElement>("[data-action='download-report']")
      ?.addEventListener("click", downloadReport);

    /* Scenario All/None */
    root
      .querySelector<HTMLElement>("[data-action='select-all-scenarios']")
      ?.addEventListener("click", () => {
        updateRunnerDraft((d) => ({
          ...d,
          scenarioIds: state.bootstrap?.scenarios.map((s) => s.id) ?? d.scenarioIds,
        }));
      });
    root
      .querySelector<HTMLElement>("[data-action='clear-scenarios']")
      ?.addEventListener("click", () => {
        updateRunnerDraft((d) => ({ ...d, scenarioIds: [] }));
      });

    /* Scenario toggles */
    root.querySelectorAll<HTMLInputElement>("[data-scenario-toggle-id]").forEach((node) => {
      node.addEventListener("change", () => {
        const scenarioId = node.dataset.scenarioToggleId;
        if (!scenarioId) {
          return;
        }
        updateRunnerDraft((draft) => {
          const selected = new Set(draft.scenarioIds);
          if (node.checked) {
            selected.add(scenarioId);
          } else {
            selected.delete(scenarioId);
          }
          const orderedIds = state.bootstrap?.scenarios
            .map((s) => s.id)
            .filter((id) => selected.has(id)) ?? [...selected];
          return { ...draft, scenarioIds: orderedIds };
        });
      });
    });

    /* Config form */
    root.querySelector<HTMLSelectElement>("#provider-mode")?.addEventListener("change", (e) => {
      const mode =
        (e.currentTarget as HTMLSelectElement).value === "live-frontier"
          ? "live-frontier"
          : "mock-openai";
      updateRunnerDraft((d) => ({
        ...d,
        providerMode: mode,
        ...defaultModelsForProviderMode(mode, state.bootstrap),
      }));
    });
    root.querySelector<HTMLSelectElement>("#primary-model")?.addEventListener("change", (e) => {
      const primaryModel = (e.currentTarget as HTMLSelectElement).value;
      updateRunnerDraft((d) => ({
        ...d,
        primaryModel,
        fastMode: isQaFastModeEnabled({ primaryModel, alternateModel: d.alternateModel }),
      }));
    });
    root.querySelector<HTMLSelectElement>("#alternate-model")?.addEventListener("change", (e) => {
      const alternateModel = (e.currentTarget as HTMLSelectElement).value;
      updateRunnerDraft((d) => ({
        ...d,
        alternateModel,
        fastMode: isQaFastModeEnabled({ primaryModel: d.primaryModel, alternateModel }),
      }));
    });

    root.querySelector<HTMLSelectElement>("#capture-session")?.addEventListener("change", (e) => {
      state.selectedCaptureSessionIds = readMultiSelect(e.currentTarget as HTMLSelectElement);
      state.selectedCaptureEventKey = null;
      void refresh();
    });
    root.querySelector<HTMLButtonElement>("#capture-save-view")?.addEventListener("click", () => {
      const name = window.prompt("Saved view name");
      const trimmed = name?.trim();
      if (!trimmed) {
        return;
      }
      state.captureSavedViews = [buildCaptureSavedView(trimmed), ...state.captureSavedViews].slice(
        0,
        12,
      );
      persistCaptureSavedViews(state.captureSavedViews);
      render();
    });
    root
      .querySelector<HTMLSelectElement>("#capture-saved-view")
      ?.addEventListener("change", (e) => {
        const id = (e.currentTarget as HTMLSelectElement).value;
        const view = state.captureSavedViews.find((candidate) => candidate.id === id);
        if (!view) {
          return;
        }
        applyCaptureSavedView(view);
        void refresh();
      });
    root.querySelector<HTMLButtonElement>("#capture-delete-view")?.addEventListener("click", () => {
      const select = root.querySelector<HTMLSelectElement>("#capture-saved-view");
      const id = select?.value?.trim();
      if (!id) {
        return;
      }
      state.captureSavedViews = state.captureSavedViews.filter((view) => view.id !== id);
      persistCaptureSavedViews(state.captureSavedViews);
      render();
    });
    root.querySelectorAll<HTMLButtonElement>("[data-capture-session-remove]").forEach((node) => {
      node.addEventListener("click", () => {
        const sessionId = node.dataset.captureSessionRemove?.trim();
        if (!sessionId) {
          return;
        }
        state.selectedCaptureSessionIds = state.selectedCaptureSessionIds.filter(
          (id) => id !== sessionId,
        );
        state.selectedCaptureEventKey = null;
        void refresh();
      });
    });
    root
      .querySelector<HTMLButtonElement>("#capture-toggle-selected-sessions")
      ?.addEventListener("click", () => {
        state.captureSelectedSessionsExpanded = !state.captureSelectedSessionsExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-delete-selected-sessions")
      ?.addEventListener("click", async () => {
        if (state.selectedCaptureSessionIds.length === 0) {
          return;
        }
        const confirmed = window.confirm(
          `Delete ${state.selectedCaptureSessionIds.length} selected capture session${
            state.selectedCaptureSessionIds.length === 1 ? "" : "s"
          }?`,
        );
        if (!confirmed) {
          return;
        }
        await postJson("/api/capture/delete-sessions", {
          sessionIds: state.selectedCaptureSessionIds,
        });
        state.selectedCaptureSessionIds = [];
        state.selectedCaptureEventKey = null;
        await refresh();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-purge-all")
      ?.addEventListener("click", async () => {
        const confirmed = window.confirm("Purge all captured sessions, events, and blobs?");
        if (!confirmed) {
          return;
        }
        await postJson("/api/capture/purge", {});
        state.selectedCaptureSessionIds = [];
        state.selectedCaptureEventKey = null;
        await refresh();
      });
    root.querySelector<HTMLSelectElement>("#capture-preset")?.addEventListener("change", (e) => {
      state.captureQueryPreset = (e.currentTarget as HTMLSelectElement)
        .value as UiState["captureQueryPreset"];
      void refresh();
    });
    const readMultiSelect = (select: HTMLSelectElement) =>
      [...select.selectedOptions].map((option) => option.value).filter(Boolean);
    root
      .querySelector<HTMLSelectElement>("#capture-kind-filter")
      ?.addEventListener("change", (e) => {
        state.captureKindFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-provider-filter")
      ?.addEventListener("change", (e) => {
        state.captureProviderFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-host-filter")
      ?.addEventListener("change", (e) => {
        state.captureHostFilter = readMultiSelect(e.currentTarget as HTMLSelectElement);
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-header-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureHeaderMode = value === "all" || value === "hidden" ? value : "key";
        render();
      });
    root.querySelector<HTMLSelectElement>("#capture-view-mode")?.addEventListener("change", (e) => {
      state.captureViewMode =
        (e.currentTarget as HTMLSelectElement).value === "timeline" ? "timeline" : "list";
      state.captureCollapsedLaneIds = [];
      state.capturePinnedLaneIds = [];
      state.captureTimelineWindowStartPct = null;
      state.captureTimelineWindowEndPct = null;
      state.captureTimelineBrushAnchorPct = null;
      state.captureTimelineBrushCurrentPct = null;
      state.selectedCaptureEventKey = null;
      render();
    });
    root
      .querySelector<HTMLSelectElement>("#capture-group-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureGroupMode =
          value === "flow" || value === "host-path" || value === "burst" ? value : "none";
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-lane-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineLaneMode = value === "provider" || value === "flow" ? value : "domain";
        state.captureTimelinePreviousLaneSort = null;
        state.captureCollapsedLaneIds = [];
        state.capturePinnedLaneIds = [];
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-lane-sort")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        const nextSort =
          value === "most-errors" || value === "severity" || value === "alphabetical"
            ? value
            : "most-events";
        if (nextSort !== state.captureTimelineLaneSort) {
          state.captureTimelinePreviousLaneSort = state.captureTimelineLaneSort;
        }
        state.captureTimelineLaneSort = nextSort;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-timeline-lane-search")
      ?.addEventListener("input", (e) => {
        state.captureTimelineLaneSearch = (e.currentTarget as HTMLInputElement).value ?? "";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-zoom")
      ?.addEventListener("change", (e) => {
        const value = Number((e.currentTarget as HTMLSelectElement).value);
        state.captureTimelineZoom =
          value === 75 || value === 150 || value === 200 || value === 300 ? value : 100;
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-sparkline-mode")
      ?.addEventListener("change", (e) => {
        state.captureTimelineSparklineMode =
          (e.currentTarget as HTMLSelectElement).value === "lane-relative"
            ? "lane-relative"
            : "session-relative";
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-timeline-clear-window")
      ?.addEventListener("click", () => {
        state.captureTimelineWindowStartPct = null;
        state.captureTimelineWindowEndPct = null;
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-timeline-focus-flow")
      ?.addEventListener("change", (e) => {
        state.captureTimelineFocusSelectedFlow = (e.currentTarget as HTMLInputElement).checked;
        if (!state.captureTimelineFocusSelectedFlow) {
          state.captureTimelineFocusedLaneMode = "all";
          state.captureTimelineFocusedLaneThreshold = "any";
        }
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-focused-lane-mode")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineFocusedLaneMode =
          value === "only-matching" || value === "collapse-background" ? value : "all";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-timeline-focused-lane-threshold")
      ?.addEventListener("change", (e) => {
        const value = (e.currentTarget as HTMLSelectElement).value;
        state.captureTimelineFocusedLaneThreshold =
          value === "events-2" || value === "percent-10" || value === "percent-25" ? value : "any";
        render();
      });
    root
      .querySelector<HTMLSelectElement>("#capture-detail-placement")
      ?.addEventListener("change", (e) => {
        state.captureDetailPlacement =
          (e.currentTarget as HTMLSelectElement).value === "bottom" ? "bottom" : "right";
        render();
      });
    root
      .querySelector<HTMLElement>("[data-capture-detail-splitter]")
      ?.addEventListener("mousedown", (event) => {
        if (event.button !== 0 || state.captureDetailPlacement !== "right") {
          return;
        }
        const splitRoot = root.querySelector<HTMLElement>("[data-capture-detail-split-root]");
        if (!splitRoot) {
          return;
        }
        const rect = splitRoot.getBoundingClientRect();
        state.captureDetailSplitDragging = true;
        render();
        const handleMove = (moveEvent: MouseEvent) => {
          const localX = moveEvent.clientX - rect.left;
          const nextPct = ((rect.width - localX) / rect.width) * 100;
          state.captureDetailSplitPct = Math.max(22, Math.min(55, Number(nextPct.toFixed(2))));
          render();
        };
        const handleUp = () => {
          state.captureDetailSplitDragging = false;
          window.removeEventListener("mousemove", handleMove);
          window.removeEventListener("mouseup", handleUp);
          render();
        };
        window.addEventListener("mousemove", handleMove);
        window.addEventListener("mouseup", handleUp);
        event.preventDefault();
      });
    root
      .querySelector<HTMLElement>("[data-capture-detail-splitter]")
      ?.addEventListener("dblclick", () => {
        state.captureDetailSplitPct = 34;
        state.captureDetailSplitDragging = false;
        render();
      });
    root.querySelectorAll<HTMLInputElement>('input[name="capture-detail-view"]').forEach((node) => {
      node.addEventListener("change", () => {
        if (!node.checked) {
          return;
        }
        const value = node.value;
        state.captureDetailView =
          value === "flow" || value === "payload" || value === "headers" ? value : "overview";
        state.capturePreferredDetailView = state.captureDetailView;
        render();
      });
    });
    root.querySelectorAll<HTMLInputElement>('input[name="capture-flow-layout"]').forEach((node) => {
      node.addEventListener("change", () => {
        if (!node.checked) {
          return;
        }
        state.captureFlowDetailLayout = node.value === "pair-first" ? "pair-first" : "nav-first";
        render();
      });
    });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-layout"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadDetailLayout = node.value === "raw" ? "raw" : "formatted";
          render();
        });
      });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-extent"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadExtent = node.value === "full" ? "full" : "preview";
          render();
        });
      });
    root
      .querySelectorAll<HTMLInputElement>('input[name="capture-payload-event-sort"]')
      .forEach((node) => {
        node.addEventListener("change", () => {
          if (!node.checked) {
            return;
          }
          state.capturePayloadEventSort =
            node.value === "name" || node.value === "size" ? node.value : "stream";
          render();
        });
      });
    root
      .querySelector<HTMLInputElement>("#capture-payload-event-filter")
      ?.addEventListener("input", (e) => {
        state.capturePayloadEventFilter = (e.currentTarget as HTMLInputElement).value ?? "";
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-search-filter")
      ?.addEventListener("input", (e) => {
        state.captureSearchText = (e.currentTarget as HTMLInputElement).value ?? "";
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLInputElement>("#capture-errors-only")
      ?.addEventListener("change", (e) => {
        state.captureErrorsOnly = (e.currentTarget as HTMLInputElement).checked;
        state.selectedCaptureEventKey = null;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-summary-toggle")
      ?.addEventListener("click", () => {
        state.captureSummaryExpanded = !state.captureSummaryExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-controls-toggle")
      ?.addEventListener("click", () => {
        state.captureControlsExpanded = !state.captureControlsExpanded;
        render();
      });
    root
      .querySelector<HTMLButtonElement>("#capture-clear-filters")
      ?.addEventListener("click", () => {
        state.captureKindFilter = [];
        state.captureProviderFilter = [];
        state.captureHostFilter = [];
        state.captureSearchText = "";
        state.captureHeaderMode = "key";
        state.captureViewMode = "list";
        state.captureGroupMode = "none";
        state.captureTimelineLaneMode = "domain";
        state.captureTimelineLaneSort = "most-events";
        state.captureTimelinePreviousLaneSort = null;
        state.captureTimelineLaneSearch = "";
        state.captureTimelineZoom = 100;
        state.captureTimelineSparklineMode = "session-relative";
        state.captureTimelineWindowStartPct = null;
        state.captureTimelineWindowEndPct = null;
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.captureTimelineFocusSelectedFlow = false;
        state.captureTimelineFocusedLaneMode = "all";
        state.captureTimelineFocusedLaneThreshold = "any";
        state.captureErrorsOnly = false;
        state.captureCollapsedLaneIds = [];
        state.capturePinnedLaneIds = [];
        state.selectedCaptureEventKey = null;
        render();
      });
    root.querySelectorAll<HTMLElement>("[data-capture-lane-toggle]").forEach((node) => {
      node.addEventListener("click", () => {
        const laneId = node.dataset.captureLaneToggle;
        if (!laneId) {
          return;
        }
        const collapsed = new Set(state.captureCollapsedLaneIds);
        if (collapsed.has(laneId)) {
          collapsed.delete(laneId);
        } else {
          collapsed.add(laneId);
        }
        state.captureCollapsedLaneIds = [...collapsed];
        render();
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-lane-pin]").forEach((node) => {
      node.addEventListener("click", () => {
        const laneId = node.dataset.captureLanePin;
        if (!laneId) {
          return;
        }
        const pinned = new Set(state.capturePinnedLaneIds);
        if (pinned.has(laneId)) {
          pinned.delete(laneId);
        } else {
          pinned.add(laneId);
        }
        state.capturePinnedLaneIds = [...pinned];
        render();
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-event]").forEach((node) => {
      node.addEventListener("click", () => {
        state.selectedCaptureEventKey = node.dataset.captureEvent ?? null;
        render();
      });
    });
    root.querySelectorAll<HTMLButtonElement>("[data-copy-text]").forEach((node) => {
      node.addEventListener("click", async () => {
        const text = node.dataset.copyText ?? "";
        if (!text) {
          return;
        }
        await navigator.clipboard.writeText(text).catch(() => undefined);
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-sparkline-window]").forEach((node) => {
      const readWindow = () => {
        const start = Number(node.dataset.captureWindowStart ?? "NaN");
        const end = Number(node.dataset.captureWindowEnd ?? "NaN");
        return Number.isFinite(start) && Number.isFinite(end) ? { start, end } : null;
      };
      node.addEventListener("mousedown", (event) => {
        if (event.button !== 0) {
          return;
        }
        const windowRange = readWindow();
        if (!windowRange) {
          return;
        }
        sparklineSweepActive = true;
        sparklineSweepAnchorStartPct = windowRange.start;
        sparklineSweepAnchorEndPct = windowRange.end;
        sparklineSweepCurrentStartPct = windowRange.start;
        sparklineSweepCurrentEndPct = windowRange.end;
        state.captureTimelineBrushAnchorPct = windowRange.start;
        state.captureTimelineBrushCurrentPct = windowRange.end;
        render();
      });
      node.addEventListener("mouseenter", () => {
        if (!sparklineSweepActive) {
          return;
        }
        const windowRange = readWindow();
        if (!windowRange) {
          return;
        }
        sparklineSweepCurrentStartPct = windowRange.start;
        sparklineSweepCurrentEndPct = windowRange.end;
        const previewStart = Math.min(
          sparklineSweepAnchorStartPct ?? windowRange.start,
          windowRange.start,
        );
        const previewEnd = Math.max(sparklineSweepAnchorEndPct ?? windowRange.end, windowRange.end);
        state.captureTimelineBrushAnchorPct = previewStart;
        state.captureTimelineBrushCurrentPct = previewEnd;
        render();
      });
    });
    const timelineViewports = [...root.querySelectorAll<HTMLElement>(".capture-timeline-viewport")];
    timelineViewports.forEach((node) => {
      node.addEventListener("scroll", () => {
        if (syncingCaptureTimelineScroll) {
          return;
        }
        syncingCaptureTimelineScroll = true;
        const nextLeft = node.scrollLeft;
        for (const other of timelineViewports) {
          if (other !== node && other.scrollLeft !== nextLeft) {
            other.scrollLeft = nextLeft;
          }
        }
        syncingCaptureTimelineScroll = false;
      });
    });
    root.querySelectorAll<HTMLElement>("[data-capture-timeline-brush-surface]").forEach((node) => {
      node.addEventListener("mousedown", (event) => {
        if (event.button !== 0) {
          return;
        }
        const viewport = node;
        const trackWidth = Number(viewport.dataset.captureTimelineTrackWidth ?? "0");
        if (!Number.isFinite(trackWidth) || trackWidth <= 0) {
          return;
        }
        const percentFromEvent = (clientX: number) => {
          const rect = viewport.getBoundingClientRect();
          const localX = clientX - rect.left + viewport.scrollLeft;
          return Math.min(100, Math.max(0, (localX / trackWidth) * 100));
        };
        const anchorPct = percentFromEvent(event.clientX);
        state.captureTimelineBrushAnchorPct = anchorPct;
        state.captureTimelineBrushCurrentPct = anchorPct;
        render();
        const handleMove = (moveEvent: MouseEvent) => {
          state.captureTimelineBrushCurrentPct = percentFromEvent(moveEvent.clientX);
          render();
        };
        const handleUp = () => {
          const anchor = state.captureTimelineBrushAnchorPct;
          const current = state.captureTimelineBrushCurrentPct;
          if (anchor != null && current != null) {
            const start = Math.min(anchor, current);
            const end = Math.max(anchor, current);
            if (end - start >= 1) {
              state.captureTimelineWindowStartPct = start;
              state.captureTimelineWindowEndPct = end;
              state.selectedCaptureEventKey = null;
            }
          }
          state.captureTimelineBrushAnchorPct = null;
          state.captureTimelineBrushCurrentPct = null;
          window.removeEventListener("mousemove", handleMove);
          window.removeEventListener("mouseup", handleUp);
          render();
        };
        window.addEventListener("mousemove", handleMove);
        window.addEventListener("mouseup", handleUp);
      });
    });
    if (!captureGlobalListenersBound) {
      captureGlobalListenersBound = true;
      window.addEventListener("mouseup", (event) => {
        if (!sparklineSweepActive) {
          return;
        }
        const anchorStart = sparklineSweepAnchorStartPct;
        const anchorEnd = sparklineSweepAnchorEndPct;
        const currentStart = sparklineSweepCurrentStartPct;
        const currentEnd = sparklineSweepCurrentEndPct;
        sparklineSweepActive = false;
        sparklineSweepAnchorStartPct = null;
        sparklineSweepAnchorEndPct = null;
        sparklineSweepCurrentStartPct = null;
        sparklineSweepCurrentEndPct = null;
        if (
          anchorStart == null ||
          anchorEnd == null ||
          currentStart == null ||
          currentEnd == null
        ) {
          state.captureTimelineBrushAnchorPct = null;
          state.captureTimelineBrushCurrentPct = null;
          render();
          return;
        }
        const start = Math.min(anchorStart, currentStart);
        const end = Math.max(anchorEnd, currentEnd);
        const width = Math.max(0.01, end - start);
        const expand = event.shiftKey ? width : 0;
        state.captureTimelineWindowStartPct = Math.max(0, Math.min(100, start - expand));
        state.captureTimelineWindowEndPct = Math.max(0, Math.min(100, end + expand));
        state.captureTimelineBrushAnchorPct = null;
        state.captureTimelineBrushCurrentPct = null;
        state.selectedCaptureEventKey = null;
        render();
      });
      root.addEventListener("keydown", (event) => {
        if (state.activeTab !== "capture") {
          return;
        }
        if (isEditableElement(event.target)) {
          return;
        }
        if (event.key === "1" || event.key === "2" || event.key === "3" || event.key === "4") {
          const radios = [
            ...root.querySelectorAll<HTMLInputElement>('input[name="capture-detail-view"]'),
          ].filter((node) => !node.disabled);
          const index = Number(event.key) - 1;
          const target = radios[index];
          if (target) {
            event.preventDefault();
            target.checked = true;
            state.captureDetailView =
              target.value === "flow" || target.value === "payload" || target.value === "headers"
                ? target.value
                : "overview";
            state.capturePreferredDetailView = state.captureDetailView;
            render();
          }
          return;
        }
        if (state.captureViewMode !== "timeline") {
          return;
        }
        if (
          event.key !== "ArrowLeft" &&
          event.key !== "ArrowRight" &&
          event.key !== "Home" &&
          event.key !== "End" &&
          event.key !== "Escape"
        ) {
          return;
        }
        if (event.key === "Escape") {
          if (
            state.captureTimelineWindowStartPct != null ||
            state.captureTimelineBrushAnchorPct != null
          ) {
            event.preventDefault();
            state.captureTimelineWindowStartPct = null;
            state.captureTimelineWindowEndPct = null;
            state.captureTimelineBrushAnchorPct = null;
            state.captureTimelineBrushCurrentPct = null;
            state.selectedCaptureEventKey = null;
            render();
          }
          return;
        }
        const markers = [
          ...root.querySelectorAll<HTMLElement>(".capture-timeline [data-capture-event]"),
        ];
        if (markers.length === 0) {
          return;
        }
        const currentIndex = markers.findIndex(
          (node) => (node.dataset.captureEvent ?? null) === state.selectedCaptureEventKey,
        );
        let nextIndex = Math.max(currentIndex, 0);
        if (event.key === "Home") {
          nextIndex = 0;
        } else if (event.key === "End") {
          nextIndex = markers.length - 1;
        } else if (event.key === "ArrowLeft") {
          nextIndex = currentIndex <= 0 ? 0 : currentIndex - 1;
        } else if (event.key === "ArrowRight") {
          nextIndex = currentIndex < 0 ? 0 : Math.min(markers.length - 1, currentIndex + 1);
        }
        const next = markers[nextIndex];
        if (!next) {
          return;
        }
        event.preventDefault();
        state.selectedCaptureEventKey = next.dataset.captureEvent ?? null;
        render();
      });
    }

    /* Composer form */
    root.querySelector<HTMLSelectElement>("#conversation-kind")?.addEventListener("change", (e) => {
      state.composer.conversationKind =
        (e.currentTarget as HTMLSelectElement).value === "channel" ? "channel" : "direct";
    });
    root.querySelector<HTMLInputElement>("#conversation-id")?.addEventListener("input", (e) => {
      state.composer.conversationId = (e.currentTarget as HTMLInputElement).value;
    });
    root.querySelector<HTMLInputElement>("#sender-id")?.addEventListener("input", (e) => {
      state.composer.senderId = (e.currentTarget as HTMLInputElement).value;
    });
    root.querySelector<HTMLInputElement>("#sender-name")?.addEventListener("input", (e) => {
      state.composer.senderName = (e.currentTarget as HTMLInputElement).value;
    });

    /* Composer textarea: capture input + Enter-to-send */
    const textarea = root.querySelector<HTMLTextAreaElement>("#composer-text");
    if (textarea) {
      textarea.addEventListener("input", (e) => {
        state.composer.text = (e.currentTarget as HTMLTextAreaElement).value;
        /* Auto-grow */
        textarea.style.height = "auto";
        textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
      });
      textarea.addEventListener("keydown", (e) => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          void sendInbound();
        }
      });
    }

    /* Chat scroll tracking */
    trackChatScroll();
  }

  /* ---------- Render ---------- */

  function render() {
    /* Preserve focused element id so we can restore focus after re-render */
    const focusedId = (document.activeElement as HTMLElement)?.id || null;
    const composerText = state.composer.text;

    root.innerHTML = renderQaLabUi(state);
    bindEvents();

    /* Restore composer text (since we re-rendered) */
    const textEl = root.querySelector<HTMLTextAreaElement>("#composer-text");
    if (textEl && composerText) {
      textEl.value = composerText;
      textEl.style.height = "auto";
      textEl.style.height = `${Math.min(textEl.scrollHeight, 120)}px`;
    }

    /* Restore focus */
    if (focusedId) {
      const el = root.querySelector<HTMLElement>(`#${CSS.escape(focusedId)}`);
      if (el && "focus" in el) {
        el.focus();
      }
    }

    if (
      state.activeTab === "capture" &&
      state.captureViewMode === "timeline" &&
      state.selectedCaptureEventKey
    ) {
      const selectedTimelineMarker = root.querySelector<HTMLElement>(
        `.capture-timeline [data-capture-event="${CSS.escape(state.selectedCaptureEventKey)}"]`,
      );
      if (selectedTimelineMarker) {
        selectedTimelineMarker.scrollIntoView({ block: "nearest", inline: "center" });
      }
    }

    /* Auto-scroll chat */
    requestAnimationFrame(() => scrollChatToBottom());
  }

  /* ---------- Bootstrap ---------- */

  render();
  await refresh();
  void pollUiVersion();
  setInterval(() => void refresh(), 1_000);
  setInterval(() => void pollUiVersion(), 1_000);
}
