/**
 * UnifiedConsoleApp — single TUI replacing app.tsx, inspect-app.tsx, and lab-app.tsx.
 *
 * Features a unified timeline showing all events (interactive, observed, lab)
 * with actions on timeline entries: detail, provenance, lab replay.
 */

import { useState, useEffect, useCallback, useRef } from 'react';
import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
import { McpSession } from './mcp-session.js';
import type { LogEntry, McpTool } from './mcp-session.js';
import type {
  TimelineEvent,
  UnifiedConsoleState,
  ActionState,
  ActiveSession,
  ProxyModelDetails,
} from './unified-types.js';
import { MAX_TIMELINE_EVENTS } from './unified-types.js';
import { Timeline } from './components/timeline.js';
import { ActionArea } from './components/action-area.js';
import { BeginSessionView } from './components/begin-session.js';
import { SessionSidebar } from './components/session-sidebar.js';
import { Toolbar } from './components/toolbar.js';
import { getContentText } from './components/provenance-view.js';
import { formatBodyDetail } from './format-event.js';
import type { IncomingMessage } from 'node:http';
import { request as httpRequest } from 'node:http';
import http from 'node:http';

// ── SSE Client (adapted from inspect-app.tsx) ──

interface SSETrafficEvent {
  timestamp: string;
  projectName: string;
  sessionId: string;
  eventType: string;
  method?: string;
  upstreamName?: string;
  body: unknown;
  durationMs?: number;
  correlationId?: string;
}

function connectSSE(
  url: string,
  opts: {
    onSessions: (sessions: ActiveSession[]) => void;
    onEvent: (event: SSETrafficEvent) => void;
    onLive: () => void;
    onError: (err: string) => void;
  },
): () => void {
  let aborted = false;
  const parsed = new URL(url);

  const req = httpRequest(
    {
      hostname: parsed.hostname,
      port: parsed.port,
      path: parsed.pathname + parsed.search,
      headers: { Accept: 'text/event-stream' },
    },
    (res: IncomingMessage) => {
      let buffer = '';
      let currentEventType = 'message';

      res.setEncoding('utf-8');
      res.on('data', (chunk: string) => {
        buffer += chunk;
        const lines = buffer.split('\n');
        buffer = lines.pop()!;

        for (const line of lines) {
          if (line.startsWith('event: ')) {
            currentEventType = line.slice(7).trim();
          } else if (line.startsWith('data: ')) {
            const data = line.slice(6);
            try {
              const parsed = JSON.parse(data);
              if (currentEventType === 'sessions') {
                opts.onSessions(parsed as ActiveSession[]);
              } else if (currentEventType === 'live') {
                opts.onLive();
              } else {
                opts.onEvent(parsed as SSETrafficEvent);
              }
            } catch {
              // Ignore unparseable
            }
            currentEventType = 'message';
          }
        }
      });

      res.on('end', () => {
        if (!aborted) opts.onError('SSE connection closed');
      });

      res.on('error', (err) => {
        if (!aborted) opts.onError(err.message);
      });
    },
  );

  req.on('error', (err) => {
    if (!aborted) opts.onError(err.message);
  });

  req.end();

  return () => {
    aborted = true;
    req.destroy();
  };
}

// ── HTTP helpers ──

function httpGet<T>(url: string, timeout = 5000): Promise<T> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const req = http.get(
      { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, timeout },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (c: Buffer) => chunks.push(c));
        res.on('end', () => {
          try { resolve(JSON.parse(Buffer.concat(chunks).toString()) as T); }
          catch { reject(new Error('Invalid JSON')); }
        });
      },
    );
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
  });
}

function httpPost<T>(url: string, body: unknown, timeout = 30000): Promise<T> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const data = JSON.stringify(body);
    const req = http.request(
      {
        hostname: parsed.hostname,
        port: parsed.port,
        path: parsed.pathname,
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
        timeout,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (c: Buffer) => chunks.push(c));
        res.on('end', () => {
          try { resolve(JSON.parse(Buffer.concat(chunks).toString()) as T); }
          catch { reject(new Error('Invalid JSON response')); }
        });
      },
    );
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    req.write(data);
    req.end();
  });
}

function httpPut<T>(url: string, body: unknown, timeout = 5000): Promise<T> {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const data = JSON.stringify(body);
    const req = http.request(
      {
        hostname: parsed.hostname,
        port: parsed.port,
        path: parsed.pathname,
        method: 'PUT',
        headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
        timeout,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (c: Buffer) => chunks.push(c));
        res.on('end', () => {
          try { resolve(JSON.parse(Buffer.concat(chunks).toString()) as T); }
          catch { reject(new Error('Invalid JSON response')); }
        });
      },
    );
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    req.write(data);
    req.end();
  });
}

// ── Main App ──

interface UnifiedAppProps {
  projectName?: string | undefined;
  endpointUrl?: string | undefined;
  mcplocalUrl: string;
  token?: string;
}

function UnifiedApp({ projectName, endpointUrl, mcplocalUrl, token }: UnifiedAppProps) {
  const { exit } = useApp();
  const { stdout } = useStdout();
  const termHeight = stdout?.rows ?? 40;

  // Interactive session — only created on demand (via "New Session")
  const [session, setSession] = useState<McpSession | null>(null);
  const [projectPickerIdx, setProjectPickerIdx] = useState(0);
  const [state, setState] = useState<UnifiedConsoleState>({
    phase: endpointUrl ? 'ready' : 'ready', // Start ready — observe-only by default
    error: null,
    session: null,
    gated: false,
    initResult: null,
    tools: [],
    resources: [],
    prompts: [],
    sseConnected: false,
    observedSessions: [],
    showSidebar: !projectName, // Show sidebar by default when no project
    selectedSessionIdx: -1,
    sidebarMode: 'sessions',
    availableProjects: [],
    activeProjectName: projectName ?? null,
    toolbarFocusIdx: -1,
    events: [],
    focusedEventIdx: -1,
    nextEventId: 1,
    laneFilter: 'all',
    action: { type: 'none' },
    availableModels: [],
    availableProviders: [],
    availableLlms: [],
  });

  const stateRef = useRef(state);
  stateRef.current = state;

  // ── Add timeline event ──
  const addEvent = useCallback((partial: Omit<TimelineEvent, 'id'>) => {
    setState((s) => {
      const event: TimelineEvent = { ...partial, id: s.nextEventId };
      let events = [...s.events, event];
      if (events.length > MAX_TIMELINE_EVENTS) {
        events = events.slice(events.length - MAX_TIMELINE_EVENTS);
      }
      return { ...s, events, nextEventId: s.nextEventId + 1 };
    });
  }, []);

  // ── Log callback (interactive session → timeline) ──
  const handleLog = useCallback((entry: LogEntry) => {
    const eventType = entry.direction === 'request'
      ? 'client_request' as const
      : entry.direction === 'error'
        ? 'client_response' as const
        : 'client_response' as const;

    addEvent({
      timestamp: entry.timestamp,
      lane: 'interactive',
      eventType,
      method: entry.method,
      projectName: stateRef.current.activeProjectName ?? 'unknown',
      sessionId: session?.getSessionId() ?? 'pending',
      body: entry.body,
    });
  }, [addEvent, session]);

  useEffect(() => {
    if (session) session.onLog = handleLog;
  }, [session, handleLog]);

  // ── Connect interactive session (on demand via `n` key) ──
  const connect = useCallback(async (sess: McpSession) => {
    try {
      const initResult = await sess.initialize();
      const tools = await sess.listTools();
      const gated = tools.length === 1 && tools[0]?.name === 'begin_session';

      setState((s) => ({
        ...s,
        phase: 'ready',
        session: sess,
        initResult,
        tools,
        gated,
      }));

      if (!gated) {
        try {
          const [resources, prompts] = await Promise.all([
            sess.listResources(),
            sess.listPrompts(),
          ]);
          setState((s) => ({ ...s, resources, prompts }));
        } catch {
          // Non-fatal
        }
      }
    } catch (err) {
      setState((s) => ({
        ...s,
        phase: 'error',
        error: `Connection failed: ${err instanceof Error ? err.message : String(err)}`,
      }));
    }
  }, []);

  // No auto-connect — user launches interactive session explicitly

  // ── SSE for observed traffic ──
  useEffect(() => {
    const base = mcplocalUrl.replace(/\/$/, '');
    const inspectUrl = projectName
      ? `${base}/inspect?project=${encodeURIComponent(projectName)}`
      : `${base}/inspect`;

    const disconnect = connectSSE(inspectUrl, {
      onSessions: (sessions) => {
        setState((s) => ({ ...s, observedSessions: sessions }));
      },
      onEvent: (e) => {
        // Skip events from our own interactive session (already captured via onLog)
        const currentSid = session?.getSessionId();
        if (currentSid && e.sessionId === currentSid) return;

        // Update observed sessions list when sessions are created/closed
        if (e.eventType === 'session_created') {
          setState((s) => {
            const exists = s.observedSessions.some((os) => os.sessionId === e.sessionId);
            if (exists) return s;
            return {
              ...s,
              observedSessions: [...s.observedSessions, {
                sessionId: e.sessionId,
                projectName: e.projectName,
                startedAt: e.timestamp,
              }],
            };
          });
        } else if (e.eventType === 'session_closed') {
          setState((s) => ({
            ...s,
            observedSessions: s.observedSessions.filter((os) => os.sessionId !== e.sessionId),
          }));
        }

        addEvent({
          timestamp: new Date(e.timestamp),
          lane: 'observed',
          eventType: e.eventType as TimelineEvent['eventType'],
          method: e.method,
          projectName: e.projectName,
          sessionId: e.sessionId,
          upstreamName: e.upstreamName,
          body: e.body,
          durationMs: e.durationMs,
          correlationId: e.correlationId,
        });
      },
      onLive: () => {
        setState((s) => ({ ...s, sseConnected: true }));
      },
      onError: () => {
        setState((s) => ({ ...s, sseConnected: false }));
      },
    });

    return disconnect;
  }, [mcplocalUrl, projectName, session, addEvent]);

  // ── Fetch available ProxyModels, Providers, and LLMs ──
  useEffect(() => {
    const baseUrl = mcplocalUrl.replace(/\/$/, '');
    Promise.all([
      httpGet<Array<{ name: string }>>(`${baseUrl}/proxymodels`).catch(() => []),
      httpGet<{ providers: string[] }>(`${baseUrl}/llm/providers`).catch(() => ({ providers: [] })),
      httpGet<{ models: string[] }>(`${baseUrl}/llm/models`).catch(() => ({ models: [] })),
    ]).then(([models, providerData, llmData]) => {
      setState((s) => ({
        ...s,
        availableModels: models.map((m) => m.name),
        availableProviders: providerData.providers ?? [],
        availableLlms: llmData.models ?? [],
      }));
    });
  }, [mcplocalUrl]);

  // ── Fetch available projects on startup ──
  useEffect(() => {
    const baseUrl = mcplocalUrl.replace(/\/$/, '');
    httpGet<Array<{ name: string }>>(`${baseUrl}/api/v1/projects`)
      .then((projects) => {
        setState((s) => ({ ...s, availableProjects: projects.map((p) => p.name) }));
      })
      .catch(() => {
        // Non-fatal — project picker will show empty list
      });
  }, [mcplocalUrl]);

  // ── Start / reconnect interactive session ──
  const startInteractiveSession = useCallback(async (forProject?: string) => {
    const targetProject = forProject ?? projectName;
    if (!targetProject) {
      setState((s) => ({ ...s, error: 'No project selected — cannot start interactive session' }));
      return;
    }
    const baseUrl = mcplocalUrl.replace(/\/$/, '');
    const url = `${baseUrl}/projects/${encodeURIComponent(targetProject)}/mcp`;
    setState((s) => ({
      ...s,
      phase: 'connecting',
      error: null,
      action: { type: 'none' },
      activeProjectName: targetProject,
      sidebarMode: 'sessions',
    }));
    if (session) await session.close().catch(() => {});
    const newSession = new McpSession(url, token);
    newSession.onLog = handleLog;
    setSession(newSession);
    await connect(newSession);
  }, [session, projectName, mcplocalUrl, token, handleLog, connect]);

  // ── After begin_session ──
  const onSessionBegan = useCallback(async () => {
    if (!session) return;
    setState((s) => ({ ...s, gated: false, action: { type: 'none' } }));
    try {
      const [tools, resources, prompts] = await Promise.all([
        session.listTools(),
        session.listResources(),
        session.listPrompts(),
      ]);
      setState((s) => ({ ...s, tools, resources, prompts }));
    } catch {
      // Non-fatal
    }
  }, [session]);

  // ── Helper: find upstream event(s) for provenance ──
  // For discovery methods (tools/list, resources/list, prompts/list) that fan out
  // to multiple servers, merge all upstream responses into one synthetic event.
  const findUpstreamEvent = useCallback((clientEvent: TimelineEvent): TimelineEvent | null => {
    if (!clientEvent.correlationId) return null;
    const matches = state.events.filter(
      (e) => e.correlationId === clientEvent.correlationId && e.eventType === 'upstream_response',
    );
    if (matches.length === 0) return null;
    if (matches.length === 1) return matches[0]!;

    // Multiple upstream responses — merge results for discovery methods.
    // Each upstream returns { result: { tools: [...] } } (or prompts/resources).
    const method = clientEvent.method ?? '';
    const listKey = method === 'tools/list' ? 'tools'
      : method === 'prompts/list' ? 'prompts'
      : method === 'resources/list' ? 'resources'
      : null;

    if (!listKey) {
      // Non-discovery method with multiple upstream events — return first
      return matches[0]!;
    }

    // Merge: collect items from all successful upstream responses
    const merged: unknown[] = [];
    const serverSummary: string[] = [];
    for (const m of matches) {
      const body = m.body as Record<string, unknown> | null;
      const result = body?.['result'] as Record<string, unknown> | undefined;
      const items = result?.[listKey];
      if (Array.isArray(items)) {
        merged.push(...items);
        serverSummary.push(`${m.upstreamName ?? '?'}: ${String(items.length)}`);
      } else if (body?.['error']) {
        const err = body['error'] as { message?: string };
        serverSummary.push(`${m.upstreamName ?? '?'}: error (${err.message ?? 'unknown'})`);
      }
    }

    // Build synthetic merged event
    return {
      ...matches[0]!,
      upstreamName: serverSummary.join(', '),
      body: { result: { [listKey]: merged } },
    };
  }, [state.events]);

  // ── Preview replay (provenance) ──
  const runPreviewReplay = useCallback(async () => {
    const s = stateRef.current;
    if (s.action.type !== 'provenance') return;

    const content = s.action.editedContent;
    if (!content) {
      setState((prev) => ({ ...prev, error: 'No content to replay' }));
      return;
    }

    // Resolve client event from the filtered events list via state
    const filtered = s.events.filter((e) => {
      if (s.laneFilter !== 'all' && e.lane !== s.laneFilter) return false;
      return true;
    });
    const clientEvent = filtered[s.action.clientEventIdx];
    const sourceName = clientEvent?.upstreamName
      ? `${clientEvent.upstreamName}/${clientEvent.method ?? ''}`
      : clientEvent?.method ?? 'unknown';

    setState((prev) => ({
      ...prev,
      action: { ...prev.action, replayRunning: true } as ActionState,
    }));

    const baseUrl = mcplocalUrl.replace(/\/$/, '');

    try {
      const replayBody: Record<string, unknown> = {
        content,
        sourceName,
        proxyModel: s.action.replayConfig.proxyModel,
      };
      if (s.action.replayConfig.provider) replayBody['provider'] = s.action.replayConfig.provider;
      if (s.action.replayConfig.llmModel) replayBody['llmModel'] = s.action.replayConfig.llmModel;

      const result = await httpPost<{ content: string; durationMs: number }>(`${baseUrl}/proxymodel/replay`, replayBody)
        .catch((err) => ({ content: '', durationMs: 0, error: err instanceof Error ? err.message : String(err) }));

      setState((prev) => {
        if (prev.action.type !== 'provenance') return prev;
        return {
          ...prev,
          action: {
            ...prev.action,
            replayResult: {
              content: result.content,
              durationMs: result.durationMs,
              error: 'error' in result ? (result as { error: string }).error : undefined,
            },
            replayRunning: false,
          },
        };
      });
    } catch (err) {
      setState((prev) => ({
        ...prev,
        error: `Preview replay failed: ${err instanceof Error ? err.message : String(err)}`,
        action: prev.action.type === 'provenance' ? { ...prev.action, replayRunning: false } : prev.action,
      }));
    }
  }, [mcplocalUrl]);

  // ── Build session list for sidebar/keybindings ──
  const allSessions = buildAllSessions(session?.getSessionId(), state.observedSessions, state.events, projectName ?? 'all');

  // ── Filtered events (lane + session) ──
  const filteredEvents = state.events.filter((e) => {
    if (state.laneFilter !== 'all' && e.lane !== state.laneFilter) return false;
    if (state.selectedSessionIdx >= 0 && state.selectedSessionIdx < allSessions.length) {
      const selectedSid = allSessions[state.selectedSessionIdx]!.sessionId;
      if (e.sessionId !== selectedSid) return false;
    }
    return true;
  });

  // ── Action area helpers ──
  const setAction = useCallback((action: ActionState) => {
    setState((s) => ({ ...s, action }));
  }, []);

  const setError = useCallback((msg: string) => {
    setState((s) => ({ ...s, error: msg }));
  }, []);

  // ── Keyboard ──
  useInput((input, key) => {
    const s = stateRef.current;

    // Quit always works
    if (input === 'q' && !key.ctrl) {
      session?.close().catch(() => {});
      exit();
      return;
    }

    // ── Action area keybindings ──

    // Begin-session view: let it handle input (only when user explicitly started a session)
    if (s.gated && s.action.type === 'none' && s.phase === 'ready' && session) {
      // Don't capture shortcuts — begin-session text input needs them
      if (key.escape) {
        // Esc cancels the gating prompt — drop back to observe-only
        setState((prev) => ({ ...prev, gated: false }));
        return;
      }
      return;
    }

    // Tool input / raw jsonrpc: text input mode
    if (s.action.type === 'tool-input' || s.action.type === 'raw-jsonrpc') {
      if (key.escape) setAction({ type: 'none' });
      return;
    }

    // Tool/resource/prompt browser: Select component handles input
    if (s.action.type === 'tool-browser' || s.action.type === 'resource-browser' || s.action.type === 'prompt-browser') {
      if (key.escape) setAction({ type: 'none' });
      return;
    }

    // Detail view
    if (s.action.type === 'detail') {
      // Search mode: capture input for search query
      if (s.action.searchMode) {
        if (key.escape) {
          // Cancel search mode, clear query
          setState((prev) => {
            if (prev.action.type !== 'detail') return prev;
            return { ...prev, action: { ...prev.action, searchMode: false, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
          });
          return;
        }
        if (key.return) {
          // Submit search: compute matches and jump to first
          const event = filteredEvents[s.action.eventIdx];
          if (event && s.action.searchQuery.length > 0) {
            const allLines = formatBodyDetail(event.eventType, event.method ?? '', event.body);
            const query = s.action.searchQuery.toLowerCase();
            const matches = allLines.reduce<number[]>((acc, line, i) => {
              if (line.toLowerCase().includes(query)) acc.push(i);
              return acc;
            }, []);
            const firstMatchOffset = matches.length > 0 ? matches[0]! : s.action.scrollOffset;
            setState((prev) => {
              if (prev.action.type !== 'detail') return prev;
              return { ...prev, action: { ...prev.action, searchMode: false, searchMatches: matches, searchMatchIdx: matches.length > 0 ? 0 : -1, scrollOffset: firstMatchOffset } };
            });
          } else {
            setState((prev) => {
              if (prev.action.type !== 'detail') return prev;
              return { ...prev, action: { ...prev.action, searchMode: false } };
            });
          }
          return;
        }
        if (key.backspace || key.delete) {
          setState((prev) => {
            if (prev.action.type !== 'detail') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: prev.action.searchQuery.slice(0, -1) } };
          });
          return;
        }
        if (input && !key.ctrl && !key.meta) {
          setState((prev) => {
            if (prev.action.type !== 'detail') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: prev.action.searchQuery + input } };
          });
        }
        return;
      }

      if (key.escape) {
        // If search query active, clear it first; otherwise close detail view
        if (s.action.searchQuery.length > 0) {
          setState((prev) => {
            if (prev.action.type !== 'detail') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
          });
          return;
        }
        setAction({ type: 'none' });
        return;
      }
      // "/" enters search mode
      if (input === '/') {
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          return { ...prev, action: { ...prev.action, searchMode: true, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
        });
        return;
      }
      // n = next search match
      if (input === 'n' && s.action.searchMatches.length > 0) {
        const nextMatchIdx = (s.action.searchMatchIdx + 1) % s.action.searchMatches.length;
        const offset = s.action.searchMatches[nextMatchIdx]!;
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          return { ...prev, action: { ...prev.action, searchMatchIdx: nextMatchIdx, scrollOffset: offset } };
        });
        return;
      }
      // N = previous search match
      if (input === 'N' && s.action.searchMatches.length > 0) {
        const prevMatchIdx = (s.action.searchMatchIdx - 1 + s.action.searchMatches.length) % s.action.searchMatches.length;
        const offset = s.action.searchMatches[prevMatchIdx]!;
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          return { ...prev, action: { ...prev.action, searchMatchIdx: prevMatchIdx, scrollOffset: offset } };
        });
        return;
      }
      if (key.downArrow) {
        setState((prev) => ({ ...prev, action: { ...prev.action, scrollOffset: (prev.action as { scrollOffset: number }).scrollOffset + 1 } as ActionState }));
        return;
      }
      if (key.upArrow) {
        setState((prev) => ({ ...prev, action: { ...prev.action, scrollOffset: Math.max(0, (prev.action as { scrollOffset: number }).scrollOffset - 1) } as ActionState }));
        return;
      }
      if (key.rightArrow) {
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          return { ...prev, action: { ...prev.action, horizontalOffset: prev.action.horizontalOffset + 20 } };
        });
        return;
      }
      if (key.leftArrow) {
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          return { ...prev, action: { ...prev.action, horizontalOffset: Math.max(0, prev.action.horizontalOffset - 20) } };
        });
        return;
      }
      if (key.pageDown) {
        // Navigate to next event in timeline
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          const nextIdx = Math.min(prev.action.eventIdx + 1, filteredEvents.length - 1);
          if (nextIdx === prev.action.eventIdx) return prev;
          return { ...prev, focusedEventIdx: nextIdx, action: { ...prev.action, eventIdx: nextIdx, scrollOffset: 0, horizontalOffset: 0 } };
        });
        return;
      }
      if (key.pageUp) {
        // Navigate to previous event in timeline
        setState((prev) => {
          if (prev.action.type !== 'detail') return prev;
          const prevIdx = Math.max(prev.action.eventIdx - 1, 0);
          if (prevIdx === prev.action.eventIdx) return prev;
          return { ...prev, focusedEventIdx: prevIdx, action: { ...prev.action, eventIdx: prevIdx, scrollOffset: 0, horizontalOffset: 0 } };
        });
        return;
      }
      if (input === 'p') {
        const event = filteredEvents[s.action.eventIdx];
        if (event?.correlationId && event.eventType === 'client_response') {
          const upstream = findUpstreamEvent(event);
          const upstreamText = upstream ? getContentText(upstream) : '';
          const defaultModel = s.availableModels[0] ?? 'default';
          const defaultProvider = s.availableProviders[0] ?? null;
          const defaultLlm = s.availableLlms[0] ?? null;
          setAction({
            type: 'provenance',
            clientEventIdx: s.action.eventIdx,
            upstreamEvent: upstream,
            scrollOffset: 0,
            horizontalOffset: 0,
            focusedPanel: 'upstream',
            parameterIdx: 0,
            replayConfig: { proxyModel: defaultModel, provider: defaultProvider, llmModel: defaultLlm },
            replayResult: null,
            replayRunning: false,
            editingUpstream: false,
            editedContent: upstreamText,
            proxyModelDetails: null,
            liveOverride: false,
            serverList: [],
            serverOverrides: {},
            selectedServerIdx: -1,
            serverPickerOpen: false,
            modelPickerOpen: false,
            modelPickerIdx: 0,
            searchMode: false,
            searchQuery: '',
            searchMatches: [],
            searchMatchIdx: -1,
          });
          // Fetch proxymodel details and override info
          const baseUrl = mcplocalUrl.replace(/\/$/, '');
          httpGet<{ name: string; source: string; controller: string; controllerConfig?: Record<string, unknown>; stages: Array<{ type: string; config?: Record<string, unknown> }>; appliesTo: string[]; cacheable: boolean }>(`${baseUrl}/proxymodels/${encodeURIComponent(defaultModel)}`).then((details) => {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, proxyModelDetails: details as ProxyModelDetails } };
            });
          }).catch(() => { /* ignore */ });
          const effectiveProject = projectName ?? s.activeProjectName ?? event.projectName;
          if (effectiveProject) {
            httpGet<{ proxyModel: string | null; serverOverrides: Record<string, string>; servers: string[] }>(`${baseUrl}/projects/${encodeURIComponent(effectiveProject)}/override`).then((info) => {
              setState((prev) => {
                if (prev.action.type !== 'provenance') return prev;
                return { ...prev, action: { ...prev.action, serverList: info.servers ?? [], serverOverrides: info.serverOverrides ?? {} } };
              });
            }).catch(() => { /* ignore */ });
          }
        }
        return;
      }
      return;
    }

    // Provenance view
    if (s.action.type === 'provenance') {
      // Editing mode: let TextInput handle all input except Esc
      if (s.action.editingUpstream) {
        if (key.escape) {
          // Cancel editing, revert to original upstream text
          const upstream = s.action.upstreamEvent;
          const originalText = upstream ? getContentText(upstream) : '';
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, editingUpstream: false, editedContent: originalText } };
          });
        }
        return;
      }

      // Search mode: capture input for search query
      if (s.action.searchMode) {
        if (key.escape) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, searchMode: false, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
          });
          return;
        }
        if (key.return) {
          // Submit search: compute matches on the focused panel's content
          if (s.action.searchQuery.length > 0) {
            let searchLines: string[];
            if (s.action.focusedPanel === 'upstream') {
              const upText = s.action.editedContent || (s.action.upstreamEvent ? getContentText(s.action.upstreamEvent) : '');
              searchLines = upText.split('\n');
            } else if (s.action.focusedPanel === 'preview' && s.action.replayResult && !s.action.replayResult.error) {
              searchLines = s.action.replayResult.content.split('\n');
            } else {
              const clientEvent = filteredEvents[s.action.clientEventIdx];
              const clientText = clientEvent ? getContentText(clientEvent) : '';
              searchLines = clientText.split('\n');
            }
            const query = s.action.searchQuery.toLowerCase();
            const matches = searchLines.reduce<number[]>((acc, line, i) => {
              if (line.toLowerCase().includes(query)) acc.push(i);
              return acc;
            }, []);
            const firstMatchOffset = matches.length > 0 ? matches[0]! : s.action.scrollOffset;
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, searchMode: false, searchMatches: matches, searchMatchIdx: matches.length > 0 ? 0 : -1, scrollOffset: firstMatchOffset } };
            });
          } else {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, searchMode: false } };
            });
          }
          return;
        }
        if (key.backspace || key.delete) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: prev.action.searchQuery.slice(0, -1) } };
          });
          return;
        }
        if (input && !key.ctrl && !key.meta) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: prev.action.searchQuery + input } };
          });
        }
        return;
      }

      if (key.escape) {
        // If search query active, clear it first; otherwise close provenance view
        if (s.action.searchQuery.length > 0) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
          });
          return;
        }
        setAction({ type: 'none' });
        return;
      }
      if (s.action.replayRunning) return; // Ignore input while running

      // Tab cycles: parameters → preview → upstream → client
      if (key.tab) {
        const panels: Array<'parameters' | 'preview' | 'upstream' | 'client'> = ['parameters', 'preview', 'upstream', 'client'];
        const currentIdx = panels.indexOf(s.action.focusedPanel);
        const nextPanel = panels[(currentIdx + 1) % panels.length]!;
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, focusedPanel: nextPanel, scrollOffset: 0, horizontalOffset: 0 } };
        });
        return;
      }

      // Parameters panel: arrows navigate rows, Enter opens picker/cycles
      if (s.action.focusedPanel === 'parameters') {
        // Server picker mode: up/down navigate server list, Enter selects, Escape closes
        if (s.action.serverPickerOpen) {
          if (key.escape) {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, serverPickerOpen: false } };
            });
            return;
          }
          if (key.downArrow) {
            const maxIdx = s.action.serverList.length - 1;
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              const next = prev.action.selectedServerIdx >= maxIdx ? -1 : prev.action.selectedServerIdx + 1;
              return { ...prev, action: { ...prev.action, selectedServerIdx: next } };
            });
            return;
          }
          if (key.upArrow) {
            const maxIdx = s.action.serverList.length - 1;
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              const next = prev.action.selectedServerIdx <= -1 ? maxIdx : prev.action.selectedServerIdx - 1;
              return { ...prev, action: { ...prev.action, selectedServerIdx: next } };
            });
            return;
          }
          if (key.return) {
            // Select server and close picker; if a specific server is selected, open model picker
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              if (prev.action.selectedServerIdx >= 0) {
                // Specific server → open model picker to choose per-server ProxyModel
                const serverName = prev.action.serverList[prev.action.selectedServerIdx];
                const currentOverride = serverName ? prev.action.serverOverrides[serverName] : undefined;
                const modelIdx = currentOverride ? Math.max(0, s.availableModels.indexOf(currentOverride)) : 0;
                return { ...prev, action: { ...prev.action, serverPickerOpen: false, modelPickerOpen: true, modelPickerIdx: modelIdx } };
              }
              return { ...prev, action: { ...prev.action, serverPickerOpen: false } };
            });
            return;
          }
          return; // Absorb all other input while picker is open
        }

        // Model picker mode: select ProxyModel for the chosen server
        if (s.action.modelPickerOpen) {
          if (key.escape) {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, modelPickerOpen: false } };
            });
            return;
          }
          if (key.downArrow) {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              const next = Math.min(s.availableModels.length - 1, prev.action.modelPickerIdx + 1);
              return { ...prev, action: { ...prev.action, modelPickerIdx: next } };
            });
            return;
          }
          if (key.upArrow) {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              const next = Math.max(0, prev.action.modelPickerIdx - 1);
              return { ...prev, action: { ...prev.action, modelPickerIdx: next } };
            });
            return;
          }
          if (key.return) {
            // Apply per-server ProxyModel override
            const modelName = s.availableModels[s.action.modelPickerIdx];
            const serverName = s.action.selectedServerIdx >= 0 ? s.action.serverList[s.action.selectedServerIdx] : undefined;
            if (modelName && serverName) {
              setState((prev) => {
                if (prev.action.type !== 'provenance') return prev;
                const newOverrides = { ...prev.action.serverOverrides, [serverName]: modelName };
                return { ...prev, action: { ...prev.action, modelPickerOpen: false, serverOverrides: newOverrides } };
              });
              // If live, push the override
              if (s.action.liveOverride) {
                const effectiveProj = projectName ?? s.activeProjectName ?? filteredEvents[s.action.clientEventIdx]?.projectName;
                if (effectiveProj) {
                  const baseUrl = mcplocalUrl.replace(/\/$/, '');
                  httpPut<{ proxyModel: string | null; serverOverrides: Record<string, string> }>(`${baseUrl}/projects/${encodeURIComponent(effectiveProj)}/override`, { serverName, serverProxyModel: modelName }).then((info) => {
                    setState((prev) => {
                      if (prev.action.type !== 'provenance') return prev;
                      return { ...prev, action: { ...prev.action, serverOverrides: info.serverOverrides ?? {} } };
                    });
                  }).catch(() => { /* ignore */ });
                }
              }
            } else {
              setState((prev) => {
                if (prev.action.type !== 'provenance') return prev;
                return { ...prev, action: { ...prev.action, modelPickerOpen: false } };
              });
            }
            return;
          }
          return; // Absorb all other input while model picker is open
        }

        if (key.downArrow) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, parameterIdx: Math.min(4, prev.action.parameterIdx + 1) } };
          });
          return;
        }
        if (key.upArrow) {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, parameterIdx: Math.max(0, prev.action.parameterIdx - 1) } };
          });
          return;
        }

        // Helper: apply live override to the router
        const effectiveProjectForOverride = projectName ?? s.activeProjectName ?? filteredEvents[s.action.clientEventIdx]?.projectName;
        const applyLiveOverride = (proxyModelName: string, serverName?: string) => {
          if (!effectiveProjectForOverride) return;
          const baseUrl = mcplocalUrl.replace(/\/$/, '');
          const body: Record<string, string> = serverName
            ? { serverName, serverProxyModel: proxyModelName }
            : { proxyModel: proxyModelName };
          httpPut<{ proxyModel: string | null; serverOverrides: Record<string, string> }>(`${baseUrl}/projects/${encodeURIComponent(effectiveProjectForOverride)}/override`, body).then((info) => {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, serverOverrides: info.serverOverrides ?? {} } };
            });
          }).catch(() => { /* ignore */ });
        };

        // Helper: fetch proxymodel details
        const fetchDetails = (name: string) => {
          const baseUrl = mcplocalUrl.replace(/\/$/, '');
          httpGet<{ name: string; source: string; controller: string; controllerConfig?: Record<string, unknown>; stages: Array<{ type: string; config?: Record<string, unknown> }>; appliesTo: string[]; cacheable: boolean }>(`${baseUrl}/proxymodels/${encodeURIComponent(name)}`).then((details) => {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, proxyModelDetails: details as ProxyModelDetails } };
            });
          }).catch(() => { /* ignore */ });
        };

        // Left/Right or Enter cycles value for the focused parameter row
        if (key.return || key.rightArrow) {
          if (s.action.parameterIdx === 1) {
            // Provider cycling — also refresh available models for the new provider
            const cfg = s.action.replayConfig;
            if (s.availableProviders.length > 0) {
              const cur = s.availableProviders.indexOf(cfg.provider ?? '');
              const next = s.availableProviders[(cur + 1) % s.availableProviders.length]!;
              setState((prev) => {
                if (prev.action.type !== 'provenance') return prev;
                return { ...prev, action: { ...prev.action, replayConfig: { ...prev.action.replayConfig, provider: next, llmModel: null } } };
              });
              // Fetch models for the new provider
              const baseUrl = mcplocalUrl.replace(/\/$/, '');
              httpGet<{ models: string[] }>(`${baseUrl}/llm/models?provider=${encodeURIComponent(next)}`).then((data) => {
                setState((prev) => ({ ...prev, availableLlms: data.models ?? [] }));
              }).catch(() => { /* ignore */ });
            }
            return;
          }
          if (s.action.parameterIdx === 3) {
            // Live toggle
            const newLive = !s.action.liveOverride;
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, liveOverride: newLive } };
            });
            if (newLive) {
              const serverName = s.action.selectedServerIdx >= 0 ? s.action.serverList[s.action.selectedServerIdx] : undefined;
              applyLiveOverride(s.action.replayConfig.proxyModel, serverName);
            }
            return;
          }
          if (s.action.parameterIdx === 4) {
            // Open server picker and refresh server list
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, serverPickerOpen: true } };
            });
            const evtProject = projectName ?? s.activeProjectName ?? filteredEvents[s.action.clientEventIdx]?.projectName;
            if (evtProject) {
              const baseUrl = mcplocalUrl.replace(/\/$/, '');
              httpGet<{ proxyModel: string | null; serverOverrides: Record<string, string>; servers: string[] }>(`${baseUrl}/projects/${encodeURIComponent(evtProject)}/override`).then((info) => {
                setState((prev) => {
                  if (prev.action.type !== 'provenance') return prev;
                  return { ...prev, action: { ...prev.action, serverList: info.servers ?? [], serverOverrides: info.serverOverrides ?? {} } };
                });
              }).catch(() => { /* ignore */ });
            }
            return;
          }
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            const cfg = prev.action.replayConfig;
            const idx = prev.action.parameterIdx;
            if (idx === 0 && s.availableModels.length > 0) {
              const cur = s.availableModels.indexOf(cfg.proxyModel);
              const next = s.availableModels[(cur + 1) % s.availableModels.length]!;
              // Fetch details for new model and auto-apply if live
              fetchDetails(next);
              if (prev.action.liveOverride) {
                const serverName = prev.action.selectedServerIdx >= 0 ? prev.action.serverList[prev.action.selectedServerIdx] : undefined;
                applyLiveOverride(next, serverName);
              }
              return { ...prev, action: { ...prev.action, replayConfig: { ...cfg, proxyModel: next } } };
            }
            if (idx === 2 && s.availableLlms.length > 0) {
              const cur = s.availableLlms.indexOf(cfg.llmModel ?? '');
              const next = s.availableLlms[(cur + 1) % s.availableLlms.length]!;
              return { ...prev, action: { ...prev.action, replayConfig: { ...cfg, llmModel: next } } };
            }
            return prev;
          });
          return;
        }
        if (key.leftArrow) {
          if (s.action.parameterIdx === 1) {
            // Provider cycling backward
            const cfg = s.action.replayConfig;
            if (s.availableProviders.length > 0) {
              const cur = s.availableProviders.indexOf(cfg.provider ?? '');
              const next = s.availableProviders[(cur - 1 + s.availableProviders.length) % s.availableProviders.length]!;
              setState((prev) => {
                if (prev.action.type !== 'provenance') return prev;
                return { ...prev, action: { ...prev.action, replayConfig: { ...prev.action.replayConfig, provider: next, llmModel: null } } };
              });
              const baseUrl = mcplocalUrl.replace(/\/$/, '');
              httpGet<{ models: string[] }>(`${baseUrl}/llm/models?provider=${encodeURIComponent(next)}`).then((data) => {
                setState((prev) => ({ ...prev, availableLlms: data.models ?? [] }));
              }).catch(() => { /* ignore */ });
            }
            return;
          }
          if (s.action.parameterIdx === 3) {
            // Live toggle (same as Enter/right for toggle)
            const newLive = !s.action.liveOverride;
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, liveOverride: newLive } };
            });
            if (newLive) {
              const serverName = s.action.selectedServerIdx >= 0 ? s.action.serverList[s.action.selectedServerIdx] : undefined;
              applyLiveOverride(s.action.replayConfig.proxyModel, serverName);
            }
            return;
          }
          if (s.action.parameterIdx === 4) {
            // Open server picker (same as Enter)
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, serverPickerOpen: true } };
            });
            const evtProject2 = projectName ?? s.activeProjectName ?? filteredEvents[s.action.clientEventIdx]?.projectName;
            if (evtProject2) {
              const baseUrl = mcplocalUrl.replace(/\/$/, '');
              httpGet<{ proxyModel: string | null; serverOverrides: Record<string, string>; servers: string[] }>(`${baseUrl}/projects/${encodeURIComponent(evtProject2)}/override`).then((info) => {
                setState((prev) => {
                  if (prev.action.type !== 'provenance') return prev;
                  return { ...prev, action: { ...prev.action, serverList: info.servers ?? [], serverOverrides: info.serverOverrides ?? {} } };
                });
              }).catch(() => { /* ignore */ });
            }
            return;
          }
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            const cfg = prev.action.replayConfig;
            const idx = prev.action.parameterIdx;
            if (idx === 0 && s.availableModels.length > 0) {
              const cur = s.availableModels.indexOf(cfg.proxyModel);
              const next = s.availableModels[(cur - 1 + s.availableModels.length) % s.availableModels.length]!;
              fetchDetails(next);
              if (prev.action.liveOverride) {
                const serverName = prev.action.selectedServerIdx >= 0 ? prev.action.serverList[prev.action.selectedServerIdx] : undefined;
                applyLiveOverride(next, serverName);
              }
              return { ...prev, action: { ...prev.action, replayConfig: { ...cfg, proxyModel: next } } };
            }
            if (idx === 2 && s.availableLlms.length > 0) {
              const cur = s.availableLlms.indexOf(cfg.llmModel ?? '');
              const next = s.availableLlms[(cur - 1 + s.availableLlms.length) % s.availableLlms.length]!;
              return { ...prev, action: { ...prev.action, replayConfig: { ...cfg, llmModel: next } } };
            }
            return prev;
          });
          return;
        }
        return;
      }

      // Enter runs replay (when not in parameters panel)
      if (key.return) {
        void runPreviewReplay();
        return;
      }

      // Toggle edit mode on upstream
      if (input === 'e') {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, editingUpstream: true, focusedPanel: 'upstream' as const } };
        });
        return;
      }

      // "/" enters search mode (in scrollable panels: preview, upstream, client)
      if (input === '/') {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, searchMode: true, searchQuery: '', searchMatches: [], searchMatchIdx: -1 } };
        });
        return;
      }
      // n = next search match
      if (input === 'n' && s.action.searchMatches.length > 0) {
        const nextMatchIdx = (s.action.searchMatchIdx + 1) % s.action.searchMatches.length;
        const offset = s.action.searchMatches[nextMatchIdx]!;
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, searchMatchIdx: nextMatchIdx, scrollOffset: offset } };
        });
        return;
      }
      // N = previous search match
      if (input === 'N' && s.action.searchMatches.length > 0) {
        const prevMatchIdx = (s.action.searchMatchIdx - 1 + s.action.searchMatches.length) % s.action.searchMatches.length;
        const offset = s.action.searchMatches[prevMatchIdx]!;
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, searchMatchIdx: prevMatchIdx, scrollOffset: offset } };
        });
        return;
      }

      // Scroll (for non-parameters panels)
      if (key.downArrow) {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, scrollOffset: prev.action.scrollOffset + 1 } };
        });
        return;
      }
      if (key.upArrow) {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, scrollOffset: Math.max(0, prev.action.scrollOffset - 1) } };
        });
        return;
      }
      if (key.rightArrow) {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, horizontalOffset: prev.action.horizontalOffset + 20 } };
        });
        return;
      }
      if (key.leftArrow) {
        setState((prev) => {
          if (prev.action.type !== 'provenance') return prev;
          return { ...prev, action: { ...prev.action, horizontalOffset: Math.max(0, prev.action.horizontalOffset - 20) } };
        });
        return;
      }

      // PageDown/PageUp: navigate to next/prev provenance-eligible event
      if (key.pageDown || key.pageUp) {
        const currentIdx = s.action.clientEventIdx;
        const step = key.pageDown ? 1 : -1;
        let nextIdx = currentIdx + step;
        while (nextIdx >= 0 && nextIdx < filteredEvents.length) {
          const evt = filteredEvents[nextIdx];
          if (evt?.correlationId && evt.eventType === 'client_response') break;
          nextIdx += step;
        }
        if (nextIdx >= 0 && nextIdx < filteredEvents.length) {
          const evt = filteredEvents[nextIdx]!;
          const upstream = findUpstreamEvent(evt);
          const upstreamText = upstream ? getContentText(upstream) : '';
          const defaultModel = s.availableModels[0] ?? 'default';
          const defaultProvider = s.availableProviders[0] ?? null;
          const defaultLlm = s.availableLlms[0] ?? null;
          setState((prev) => ({
            ...prev,
            focusedEventIdx: nextIdx,
            action: {
              type: 'provenance',
              clientEventIdx: nextIdx,
              upstreamEvent: upstream,
              scrollOffset: 0,
              horizontalOffset: 0,
              focusedPanel: prev.action.type === 'provenance' ? prev.action.focusedPanel : 'upstream',
              parameterIdx: prev.action.type === 'provenance' ? prev.action.parameterIdx : 0,
              replayConfig: { proxyModel: defaultModel, provider: defaultProvider, llmModel: defaultLlm },
              replayResult: null,
              replayRunning: false,
              editingUpstream: false,
              editedContent: upstreamText,
              proxyModelDetails: prev.action.type === 'provenance' ? prev.action.proxyModelDetails : null,
              liveOverride: prev.action.type === 'provenance' ? prev.action.liveOverride : false,
              serverList: prev.action.type === 'provenance' ? prev.action.serverList : [],
              serverOverrides: prev.action.type === 'provenance' ? prev.action.serverOverrides : {},
              selectedServerIdx: prev.action.type === 'provenance' ? prev.action.selectedServerIdx : -1,
              serverPickerOpen: false,
              modelPickerOpen: false,
              modelPickerIdx: 0,
              searchMode: false,
              searchQuery: '',
              searchMatches: [],
              searchMatchIdx: -1,
            },
          }));
        }
        return;
      }

      return;
    }

    // ── Timeline navigation (action.type === 'none') ──

    // Project picker mode (sidebar visible, mode = 'project-picker')
    if (s.showSidebar && s.sidebarMode === 'project-picker') {
      if (key.escape) {
        setState((prev) => ({ ...prev, sidebarMode: 'sessions' }));
        return;
      }
      if (key.downArrow) {
        setProjectPickerIdx((prev) => Math.min(s.availableProjects.length - 1, prev + 1));
        return;
      }
      if (key.upArrow) {
        setProjectPickerIdx((prev) => Math.max(0, prev - 1));
        return;
      }
      if (key.return && s.availableProjects.length > 0) {
        const selected = s.availableProjects[projectPickerIdx];
        if (selected) {
          void startInteractiveSession(selected);
        }
        return;
      }
      return; // Absorb all other input in project-picker mode
    }

    // Session sidebar navigation (when sidebar visible, normal mode)
    if (s.showSidebar) {
      if (input === 'a') {
        setState((prev) => ({ ...prev, selectedSessionIdx: -1, focusedEventIdx: -1 }));
        return;
      }
      // Enter on "New Session" row
      if (key.return && s.selectedSessionIdx === -2) {
        if (projectName) {
          // Project specified on CLI → start session directly
          void startInteractiveSession(projectName);
        } else {
          // No project → switch to project picker
          setProjectPickerIdx(0);
          setState((prev) => ({ ...prev, sidebarMode: 'project-picker' }));
        }
        return;
      }
      // Enter selects current session and closes sidebar
      if (key.return && s.selectedSessionIdx >= 0) {
        const selectedSession = allSessions[s.selectedSessionIdx];
        setState((prev) => ({
          ...prev,
          showSidebar: false,
          focusedEventIdx: -1,
          activeProjectName: selectedSession?.projectName ?? prev.activeProjectName,
        }));
        return;
      }
      // Arrows control session selection when sidebar is visible
      // Range: -2 (New Session), -1 (all), 0+ (sessions)
      if (key.downArrow) {
        setState((prev) => ({
          ...prev,
          selectedSessionIdx: Math.min(allSessions.length - 1, prev.selectedSessionIdx + 1),
          focusedEventIdx: -1,
        }));
        return;
      }
      if (key.upArrow) {
        setState((prev) => ({
          ...prev,
          selectedSessionIdx: Math.max(-2, prev.selectedSessionIdx - 1),
          focusedEventIdx: -1,
        }));
        return;
      }
      // Escape closes sidebar
      if (key.escape) {
        setState((prev) => ({ ...prev, showSidebar: false }));
        return;
      }
    }

    // Toolbar navigation (when focused)
    if (s.toolbarFocusIdx >= 0) {
      if (key.escape || key.upArrow) {
        setState((prev) => ({ ...prev, toolbarFocusIdx: -1 }));
        return;
      }
      if (key.leftArrow) {
        setState((prev) => ({ ...prev, toolbarFocusIdx: Math.max(0, prev.toolbarFocusIdx - 1) }));
        return;
      }
      if (key.rightArrow) {
        setState((prev) => ({ ...prev, toolbarFocusIdx: Math.min(3, prev.toolbarFocusIdx + 1) }));
        return;
      }
      if (key.downArrow) {
        // Move focus to timeline
        setState((prev) => ({ ...prev, toolbarFocusIdx: -1 }));
        return;
      }
      if (key.return) {
        const actions: ActionState[] = [
          { type: 'tool-browser' },
          { type: 'resource-browser' },
          { type: 'prompt-browser' },
          { type: 'raw-jsonrpc' },
        ];
        const action = actions[s.toolbarFocusIdx];
        if (action) {
          setState((prev) => ({ ...prev, toolbarFocusIdx: -1 }));
          setAction(action);
        }
        return;
      }
      if (key.tab) {
        setState((prev) => ({ ...prev, toolbarFocusIdx: -1 }));
        return;
      }
      return; // Absorb other input when toolbar is focused
    }

    // Tab focuses toolbar (when session is open, ungated, no sidebar)
    if (key.tab && session && !s.gated && !s.showSidebar) {
      setState((prev) => ({ ...prev, toolbarFocusIdx: 0 }));
      return;
    }

    // Escape reopens sidebar (when no action is active)
    if (key.escape && !s.showSidebar) {
      setState((prev) => ({ ...prev, showSidebar: true, selectedSessionIdx: -2, focusedEventIdx: -1 }));
      return;
    }

    // "a" resumes auto-scroll (follow newest events)
    if (input === 'a') {
      setState((prev) => ({ ...prev, focusedEventIdx: -1 }));
      return;
    }

    // Arrows control event navigation when sidebar is hidden
    if (key.downArrow) {
      setState((prev) => {
        if (prev.focusedEventIdx === -1) return prev; // already at bottom (auto-scroll)
        return { ...prev, focusedEventIdx: Math.min(filteredEvents.length - 1, prev.focusedEventIdx + 1) };
      });
      return;
    }
    if (key.upArrow) {
      setState((prev) => {
        if (prev.focusedEventIdx === -1) {
          // Leave auto-scroll, focus last event
          return filteredEvents.length > 0 ? { ...prev, focusedEventIdx: filteredEvents.length - 1 } : prev;
        }
        return { ...prev, focusedEventIdx: prev.focusedEventIdx <= 0 ? -1 : prev.focusedEventIdx - 1 };
      });
      return;
    }
    // PageDown/PageUp: jump by a page (vim-style)
    if (key.pageDown) {
      const pageSize = Math.max(1, stdout.rows - 8);
      setState((prev) => {
        if (prev.focusedEventIdx === -1) return prev; // already at bottom
        const next = Math.min(filteredEvents.length - 1, prev.focusedEventIdx + pageSize);
        return { ...prev, focusedEventIdx: next };
      });
      return;
    }
    if (key.pageUp) {
      const pageSize = Math.max(1, stdout.rows - 8);
      setState((prev) => {
        const current = prev.focusedEventIdx === -1 ? filteredEvents.length - 1 : prev.focusedEventIdx;
        const next = Math.max(0, current - pageSize);
        return { ...prev, focusedEventIdx: next };
      });
      return;
    }
    if (input === 'G') {
      setState((prev) => ({ ...prev, focusedEventIdx: -1 }));
      return;
    }
    if (input === 'g') {
      setState((prev) => ({ ...prev, focusedEventIdx: filteredEvents.length > 0 ? 0 : -1 }));
      return;
    }

    if (key.return && s.focusedEventIdx >= 0 && s.focusedEventIdx < filteredEvents.length) {
      setAction({ type: 'detail', eventIdx: s.focusedEventIdx, scrollOffset: 0, horizontalOffset: 0, searchMode: false, searchQuery: '', searchMatches: [], searchMatchIdx: -1 });
      return;
    }

    if (input === 'p' && s.focusedEventIdx >= 0) {
      const event = filteredEvents[s.focusedEventIdx];
      if (event?.correlationId && event.eventType === 'client_response') {
        const upstream = findUpstreamEvent(event);
        const upstreamText = upstream ? getContentText(upstream) : '';
        const defaultModel = s.availableModels[0] ?? 'default';
        const defaultProvider = s.availableProviders[0] ?? null;
        const defaultLlm = s.availableLlms[0] ?? null;
        setAction({
          type: 'provenance',
          clientEventIdx: s.focusedEventIdx,
          upstreamEvent: upstream,
          scrollOffset: 0,
          horizontalOffset: 0,
          focusedPanel: 'upstream',
          parameterIdx: 0,
          replayConfig: { proxyModel: defaultModel, provider: defaultProvider, llmModel: defaultLlm },
          replayResult: null,
          replayRunning: false,
          editingUpstream: false,
          editedContent: upstreamText,
          proxyModelDetails: null,
          liveOverride: false,
          serverList: [],
          serverOverrides: {},
          selectedServerIdx: -1,
          serverPickerOpen: false,
          modelPickerOpen: false,
          modelPickerIdx: 0,
          searchMode: false,
          searchQuery: '',
          searchMatches: [],
          searchMatchIdx: -1,
        });
        // Fetch proxymodel details and override info
        const baseUrl = mcplocalUrl.replace(/\/$/, '');
        httpGet<{ name: string; source: string; controller: string; controllerConfig?: Record<string, unknown>; stages: Array<{ type: string; config?: Record<string, unknown> }>; appliesTo: string[]; cacheable: boolean }>(`${baseUrl}/proxymodels/${encodeURIComponent(defaultModel)}`).then((details) => {
          setState((prev) => {
            if (prev.action.type !== 'provenance') return prev;
            return { ...prev, action: { ...prev.action, proxyModelDetails: details as ProxyModelDetails } };
          });
        }).catch(() => { /* ignore */ });
        const effectiveProject3 = projectName ?? s.activeProjectName ?? event.projectName;
        if (effectiveProject3) {
          httpGet<{ proxyModel: string | null; serverOverrides: Record<string, string>; servers: string[] }>(`${baseUrl}/projects/${encodeURIComponent(effectiveProject3)}/override`).then((info) => {
            setState((prev) => {
              if (prev.action.type !== 'provenance') return prev;
              return { ...prev, action: { ...prev.action, serverList: info.servers ?? [], serverOverrides: info.serverOverrides ?? {} } };
            });
          }).catch(() => { /* ignore */ });
        }
      }
      return;
    }

    if (input === 'x') {
      setState((prev) => ({ ...prev, events: [], focusedEventIdx: -1, nextEventId: 1 }));
      return;
    }
  });

  // ── Cleanup on unmount ──
  useEffect(() => {
    return () => {
      session?.close().catch(() => {});
    };
  }, [session]);

  // ── Layout ──
  const headerHeight = 2;
  const footerHeight = 1;
  const beginSessionHeight = (state.gated && session) ? 6 : 0;
  const showToolbar = session !== null && !state.gated;
  const toolbarHeight = showToolbar ? 1 : 0;
  // Provenance takes more vertical space (4-quadrant layout)
  const isProvenance = state.action.type === 'provenance';
  const actionHeight = state.action.type !== 'none'
    ? isProvenance
      ? Math.max(16, Math.floor(termHeight * 0.65))
      : Math.max(8, Math.floor(termHeight * 0.4))
    : 0;
  const timelineHeight = Math.max(4, termHeight - headerHeight - footerHeight - actionHeight - beginSessionHeight - toolbarHeight);
  // Show project name in timeline when: no project filter, or viewing all sessions with multiple projects
  const showProject = !projectName || (state.selectedSessionIdx === -1 && state.observedSessions.length > 0);
  const selectedSessionLabel = state.selectedSessionIdx >= 0 && state.selectedSessionIdx < allSessions.length
    ? allSessions[state.selectedSessionIdx]!.sessionId.slice(0, 8)
    : null;
  const hasInteractiveSession = session !== null;

  return (
    <Box flexDirection="column" height={termHeight}>
      {/* Header */}
      <Box flexDirection="column" borderStyle="single" borderBottom={true} borderTop={false} borderLeft={false} borderRight={false} paddingX={1}>
        <Box gap={2}>
          <Text bold color="white" backgroundColor="blue"> mcpctl console </Text>
          {state.activeProjectName ? (
            <Text bold>{state.activeProjectName}</Text>
          ) : projectName ? (
            <Text bold>{projectName}</Text>
          ) : (
            <Text bold dimColor>all projects</Text>
          )}
          {hasInteractiveSession && session.getSessionId() && <Text dimColor>session: {session.getSessionId()!.slice(0, 8)}</Text>}
          {hasInteractiveSession ? (
            state.gated ? (
              <Text color="yellow" bold>[GATED]</Text>
            ) : (
              <Text color="green" bold>[OPEN]</Text>
            )
          ) : (
            <Text dimColor>[OBSERVE]</Text>
          )}
          {state.sseConnected && <Text color="green">SSE</Text>}
          {selectedSessionLabel && <Text color="cyan">session:{selectedSessionLabel}</Text>}
          {state.phase === 'connecting' && <Text color="cyan">connecting...</Text>}
        </Box>
      </Box>

      {state.error && (
        <Box paddingX={1}>
          <Text color="red">{state.error}</Text>
        </Box>
      )}

      {/* Gated: show begin_session form (only when user explicitly started session) */}
      {state.gated && state.phase === 'ready' && session && (
        <Box paddingX={1} height={beginSessionHeight}>
          <BeginSessionLoader
            tools={state.tools}
            session={session}
            onToolsRefresh={(tools) => setState((s) => ({ ...s, tools, gated: tools.length === 1 && tools[0]?.name === 'begin_session' }))}
            onDone={onSessionBegan}
            onError={setError}
          />
        </Box>
      )}

      {/* Toolbar (shown when interactive session is open and ungated) */}
      {showToolbar && (
        <Toolbar
          toolCount={state.tools.length}
          resourceCount={state.resources.length}
          promptCount={state.prompts.length}
          focusedItem={state.toolbarFocusIdx}
        />
      )}

      {/* Main content: optional sidebar + timeline */}
      <Box flexDirection="row" flexGrow={1}>
        {state.showSidebar && (
          <SessionSidebar
            interactiveSessionId={session?.getSessionId()}
            observedSessions={state.observedSessions}
            events={state.events}
            selectedIdx={state.selectedSessionIdx}
            height={timelineHeight}
            projectName={projectName ?? 'all'}
            mode={state.sidebarMode}
            availableProjects={state.availableProjects}
            projectPickerIdx={projectPickerIdx}
          />
        )}
        <Timeline
          events={filteredEvents}
          height={timelineHeight}
          focusedIdx={state.focusedEventIdx}
          showProject={showProject}
        />
      </Box>

      {/* Action Area */}
      {session ? (
        <ActionArea
          action={state.action}
          events={filteredEvents}
          session={session}
          tools={state.tools}
          resources={state.resources}
          prompts={state.prompts}
          availableModels={state.availableModels}
          height={actionHeight}
          onSetAction={setAction}
          onError={setError}
        />
      ) : (
        // Observe-only: still render detail/provenance but not tool/resource/prompt actions
        state.action.type === 'detail' || state.action.type === 'provenance' ? (
          <ActionArea
            action={state.action}
            events={filteredEvents}
            session={null as unknown as McpSession}
            tools={[]}
            resources={[]}
            prompts={[]}
            availableModels={state.availableModels}
            height={actionHeight}
            onSetAction={setAction}
            onError={setError}
          />
        ) : null
      )}

      {/* Footer */}
      <Box paddingX={1}>
        <Text dimColor>
          {state.action.type === 'none'
            ? hasInteractiveSession
              ? state.focusedEventIdx === -1
                ? `[\u2191] nav  [PgUp] page  [Enter] detail  [p] prov  [Tab] toolbar  [Esc] sidebar  [q] quit`
                : `[\u2191\u2193] nav  [PgUp/Dn] page  [a] follow  [Enter] detail  [p] prov  [Tab] toolbar  [Esc] sidebar  [q] quit`
              : state.focusedEventIdx === -1
                ? `[\u2191] nav  [PgUp] page  [Enter] detail  [p] prov  [Esc] sidebar  [q] quit`
                : `[\u2191\u2193] nav  [PgUp/Dn] page  [a] follow  [Enter] detail  [p] prov  [Esc] sidebar  [q] quit`
            : '[Esc] back'}
        </Text>
      </Box>

    </Box>
  );
}

// ── BeginSession loader (same as app.tsx) ──

function BeginSessionLoader({
  tools,
  session,
  onToolsRefresh,
  onDone,
  onError,
}: {
  tools: McpTool[];
  session: McpSession;
  onToolsRefresh: (tools: McpTool[]) => void;
  onDone: () => void;
  onError: (msg: string) => void;
}) {
  const fetchedRef = useRef(false);
  const bsTool = tools.find((t) => t.name === 'begin_session');

  useEffect(() => {
    if (!bsTool && !fetchedRef.current) {
      fetchedRef.current = true;
      session.listTools().then(onToolsRefresh).catch(() => {});
    }
  }, [bsTool, session, onToolsRefresh]);

  if (!bsTool) {
    return <Text dimColor>Loading begin_session tool...</Text>;
  }

  return (
    <BeginSessionView
      tool={bsTool}
      session={session}
      onDone={() => onDone()}
      onError={onError}
      onBack={() => {}}
    />
  );
}

// ── Session list builder ──

function buildAllSessions(
  interactiveSessionId: string | undefined,
  observedSessions: ActiveSession[],
  events: TimelineEvent[],
  projectName: string,
): Array<{ sessionId: string; projectName: string }> {
  const result: Array<{ sessionId: string; projectName: string }> = [];
  const seen = new Set<string>();

  if (interactiveSessionId) {
    result.push({ sessionId: interactiveSessionId, projectName });
    seen.add(interactiveSessionId);
  }

  for (const s of observedSessions) {
    if (!seen.has(s.sessionId)) {
      result.push({ sessionId: s.sessionId, projectName: s.projectName });
      seen.add(s.sessionId);
    }
  }

  // Also discover sessions from traffic events
  for (const e of events) {
    if (!seen.has(e.sessionId)) {
      result.push({ sessionId: e.sessionId, projectName: e.projectName });
      seen.add(e.sessionId);
    }
  }

  return result;
}

// ── Render entrypoint ──

export interface UnifiedRenderOptions {
  projectName?: string | undefined;
  endpointUrl?: string | undefined;
  mcplocalUrl: string;
  token?: string;
}

export async function renderUnifiedConsole(opts: UnifiedRenderOptions): Promise<void> {
  const instance = render(
    <UnifiedApp
      projectName={opts.projectName}
      endpointUrl={opts.endpointUrl}
      mcplocalUrl={opts.mcplocalUrl}
      token={opts.token}
    />,
  );
  await instance.waitUntilExit();
}
