/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import React, { useState, useEffect, useRef, useMemo } from 'react';
import { useDevToolsData, type ConsoleLog, type NetworkLog } from './hooks';

type ThemeMode = 'light' | 'dark' | null; // null means follow system

interface ThemeColors {
  bg: string;
  bgSecondary: string;
  bgHover: string;
  border: string;
  text: string;
  textSecondary: string;
  accent: string;
  consoleBg: string;
  rowBorder: string;
  errorBg: string;
  errorText: string;
  warnBg: string;
  warnText: string;
}

export default function App() {
  const [activeTab, setActiveTab] = useState<'console' | 'network'>('console');
  const { networkLogs, consoleLogs, connectedSessions } = useDevToolsData();
  const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
    null,
  );
  const [importedLogs, setImportedLogs] = useState<{
    network: NetworkLog[];
    console: ConsoleLog[];
  } | null>(null);
  const [importedSessionId, setImportedSessionId] = useState<string | null>(
    null,
  );

  // --- Toast Logic ---
  const [toastMessage, setToastMessage] = useState<string | null>(null);
  const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const showToast = (msg: string) => {
    setToastMessage(msg);
    if (toastTimeoutRef.current) {
      clearTimeout(toastTimeoutRef.current);
    }
    toastTimeoutRef.current = setTimeout(() => {
      setToastMessage(null);
      toastTimeoutRef.current = null;
    }, 5000);
  };

  // --- Theme Logic ---
  const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
    const saved = localStorage.getItem('devtools-theme');
    if (!saved) return null; // Default: follow system
    return saved as ThemeMode;
  });

  const [systemIsDark, setSystemIsDark] = useState(
    window.matchMedia('(prefers-color-scheme: dark)').matches,
  );

  useEffect(() => {
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const listener = (e: MediaQueryListEvent) => setSystemIsDark(e.matches);
    media.addEventListener('change', listener);
    return () => media.removeEventListener('change', listener);
  }, []);

  const isDark = themeMode === null ? systemIsDark : themeMode === 'dark';

  const t = useMemo(
    () => ({
      bg: isDark ? '#202124' : '#ffffff',
      bgSecondary: isDark ? '#292a2d' : '#f3f3f3',
      bgHover: isDark ? '#35363a' : '#e8f0fe',
      border: isDark ? '#3c4043' : '#ccc',
      text: isDark ? '#e8eaed' : '#333',
      textSecondary: isDark ? '#9aa0a6' : '#666',
      accent: isDark ? '#8ab4f8' : '#1a73e8',
      consoleBg: isDark ? '#1e1e1e' : '#fff',
      rowBorder: isDark ? '#303134' : '#f0f0f0',
      errorBg: isDark ? '#3c1e1e' : '#fff0f0',
      errorText: isDark ? '#f28b82' : '#a80000',
      warnBg: isDark ? '#302a10' : '#fff3cd',
      warnText: isDark ? '#fdd663' : '#7a5d00',
    }),
    [isDark],
  );

  const toggleTheme = () => {
    const nextMode = isDark ? 'light' : 'dark';
    setThemeMode(nextMode);
    localStorage.setItem('devtools-theme', nextMode);
  };

  // --- Import Logic ---
  const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const content = event.target?.result as string;
      try {
        const networkMap = new Map<string, NetworkLog>();
        const consoleLogs: ConsoleLog[] = [];

        content
          .split('\n')
          .filter((l) => l.trim())
          .forEach((l) => {
            const parsed = JSON.parse(l);
            const payload = parsed.payload || {};
            const type = parsed.type;
            const timestamp = parsed.timestamp;

            if (type === 'console') {
              consoleLogs.push({
                ...payload,
                type,
                timestamp,
                id: payload.id || Math.random().toString(36).substring(2, 11),
              });
            } else if (type === 'network') {
              const id = payload.id;
              if (!id) return;

              if (!networkMap.has(id)) {
                networkMap.set(id, {
                  ...payload,
                  type,
                  timestamp,
                  id,
                } as NetworkLog);
              } else {
                // It's likely a response update
                const existing = networkMap.get(id)!;
                networkMap.set(id, {
                  ...existing,
                  ...payload,
                  // Ensure we don't overwrite the original timestamp or type
                  type: existing.type,
                  timestamp: existing.timestamp,
                } as NetworkLog);
              }
            }
          });

        const importId = `[Imported] ${file.name}`;
        const networkLogs = Array.from(networkMap.values()).sort(
          (a, b) => a.timestamp - b.timestamp,
        );

        setImportedLogs({ network: networkLogs, console: consoleLogs });
        setImportedSessionId(importId);
        setSelectedSessionId(importId);
      } catch (err) {
        console.error('Import error:', err);
        alert('Failed to parse session file. Ensure it is a valid JSONL file.');
      }
    };
    reader.readAsText(file);
    e.target.value = '';
  };

  // --- Export Logic ---
  const handleExport = () => {
    if (!selectedSessionId) return;

    // Collect entries with timestamps, then sort before serializing
    const entries: Array<{ timestamp: number; data: object }> = [];

    // Export console logs
    filteredConsoleLogs.forEach((log) => {
      entries.push({
        timestamp: log.timestamp,
        data: {
          type: 'console',
          payload: { type: log.type, content: log.content },
          sessionId: log.sessionId,
          timestamp: log.timestamp,
        },
      });
    });

    // Export network logs
    filteredNetworkLogs.forEach((log) => {
      entries.push({
        timestamp: log.timestamp,
        data: {
          type: 'network',
          payload: log,
          sessionId: log.sessionId,
          timestamp: log.timestamp,
        },
      });
    });

    // Sort by timestamp, then serialize
    entries.sort((a, b) => a.timestamp - b.timestamp);

    const content = entries.map((e) => JSON.stringify(e.data)).join('\n');
    const blob = new Blob([content], { type: 'application/jsonl' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = `session-${selectedSessionId}.jsonl`;
    a.click();

    URL.revokeObjectURL(url);
  };

  // --- Session Discovery ---
  const sessions = useMemo(() => {
    const sessionMap = new Map<string, number>();
    const updateMap = (l: { sessionId?: string; timestamp: number }) => {
      if (!l.sessionId) return;
      const currentMax = sessionMap.get(l.sessionId) || 0;
      if (l.timestamp > currentMax) sessionMap.set(l.sessionId, l.timestamp);
    };
    networkLogs.forEach(updateMap);
    consoleLogs.forEach(updateMap);

    const discovered = Array.from(sessionMap.entries())
      .sort((a, b) => b[1] - a[1])
      .map((entry) => entry[0]);

    if (importedSessionId) {
      return [importedSessionId, ...discovered];
    }
    return discovered;
  }, [networkLogs, consoleLogs, importedSessionId]);

  useEffect(() => {
    if (sessions.length > 0 && selectedSessionId === null) {
      setSelectedSessionId(sessions[0]);
    }
  }, [sessions, selectedSessionId]);

  const filteredConsoleLogs = useMemo(() => {
    if (!selectedSessionId) return [];
    if (selectedSessionId === importedSessionId && importedLogs) {
      return importedLogs.console;
    }
    return consoleLogs.filter((l) => l.sessionId === selectedSessionId);
  }, [consoleLogs, selectedSessionId, importedSessionId, importedLogs]);

  const filteredNetworkLogs = useMemo(() => {
    if (!selectedSessionId) return [];
    if (selectedSessionId === importedSessionId && importedLogs) {
      return importedLogs.network;
    }
    return networkLogs.filter((l) => l.sessionId === selectedSessionId);
  }, [networkLogs, selectedSessionId, importedSessionId, importedLogs]);

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        height: '100vh',
        background: t.bg,
        color: t.text,
        transition: 'background 0.2s, color 0.2s',
        fontFamily:
          '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
      }}
    >
      <style>{`
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: ${t.bgSecondary}; }
        ::-webkit-scrollbar-thumb { background: ${t.border}; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: ${t.textSecondary}; }
        [data-gutter]::selection, [data-gutter] *::selection { background: transparent; }
        [data-gutter] .fold-icon { opacity: 0; transition: opacity 0.15s; }
        [data-code-view]:has([data-gutter]:hover) .fold-icon { opacity: 1; }
      `}</style>

      {/* Toolbar */}
      <div
        style={{
          display: 'flex',
          background: t.bgSecondary,
          borderBottom: `1px solid ${t.border}`,
          height: '36px',
          alignItems: 'center',
          padding: '0 8px',
          gap: '12px',
        }}
      >
        <div style={{ display: 'flex', height: '100%' }}>
          <TabButton
            active={activeTab === 'console'}
            onClick={() => setActiveTab('console')}
            label="Console"
            t={t}
          />
          <TabButton
            active={activeTab === 'network'}
            onClick={() => setActiveTab('network')}
            label="Network"
            t={t}
          />
        </div>

        <div
          style={{
            marginLeft: 'auto',
            fontSize: '11px',
            display: 'flex',
            alignItems: 'center',
            gap: '12px',
          }}
        >
          {selectedSessionId &&
            connectedSessions.includes(selectedSessionId) && (
              <>
                <button
                  onClick={async () => {
                    try {
                      await fetch('/api/trigger-debugger', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ sessionId: selectedSessionId }),
                      });
                      showToast(
                        'Node debugger attached. Open chrome://inspect in Chrome to start debugging.',
                      );
                    } catch (e) {
                      console.error('Failed to trigger debugger:', e);
                    }
                  }}
                  style={{
                    fontSize: '11px',
                    padding: '4px 8px',
                    border: `1px solid ${t.border}`,
                    background: t.bg,
                    color: t.text,
                    borderRadius: '4px',
                    cursor: 'pointer',
                    fontWeight: 600,
                  }}
                  title="Attach Node Debugger and open chrome://inspect"
                >
                  🐞 Debug Node
                </button>
                <button
                  onClick={handleExport}
                  style={{
                    fontSize: '11px',
                    padding: '4px 8px',
                    border: `1px solid ${t.border}`,
                    background: t.bg,
                    color: t.text,
                    borderRadius: '4px',
                    cursor: 'pointer',
                    fontWeight: 600,
                  }}
                >
                  📤 Export
                </button>
              </>
            )}

          <label
            style={{
              padding: '2px 8px',
              borderRadius: '4px',
              border: `1px solid ${t.border}`,
              background: t.bg,
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              gap: '6px',
              fontWeight: 600,
              fontSize: '11px',
            }}
          >
            <span>📥 Import</span>
            <input
              type="file"
              accept=".jsonl"
              onChange={handleImport}
              style={{ display: 'none' }}
            />
          </label>

          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '6px',
            }}
          >
            <span style={{ fontSize: '11px', color: t.textSecondary }}>
              Session:
            </span>
            {sessions.length > 0 ? (
              <select
                value={selectedSessionId || ''}
                onChange={(e) => setSelectedSessionId(e.target.value)}
                style={{
                  fontSize: '11px',
                  padding: '2px 8px',
                  background: t.bg,
                  color: t.text,
                  border: `1px solid ${t.border}`,
                  borderRadius: '3px',
                  minWidth: '280px',
                  outline: 'none',
                }}
              >
                {sessions.map((id) => (
                  <option key={id} value={id}>
                    {id}{' '}
                    {id === sessions[0] && !id.startsWith('[Imported]')
                      ? '(Latest)'
                      : ''}
                  </option>
                ))}
              </select>
            ) : (
              <span
                style={{
                  fontSize: '11px',
                  color: t.textSecondary,
                  fontStyle: 'italic',
                }}
              >
                No Sessions
              </span>
            )}
            {selectedSessionId &&
              !selectedSessionId.startsWith('[Imported]') && (
                <span
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: '4px',
                    fontSize: '11px',
                    marginLeft: '8px',
                  }}
                >
                  <span
                    style={{
                      width: '6px',
                      height: '6px',
                      borderRadius: '50%',
                      background: connectedSessions.includes(selectedSessionId)
                        ? '#34a853'
                        : '#ea4335',
                    }}
                  />
                  <span style={{ color: t.textSecondary }}>
                    {connectedSessions.includes(selectedSessionId)
                      ? 'Connected'
                      : 'Disconnected'}
                  </span>
                </span>
              )}
          </div>

          <button
            onClick={toggleTheme}
            style={{
              fontSize: '14px',
              padding: '2px 8px',
              border: `1px solid ${t.border}`,
              background: t.bg,
              color: t.text,
              borderRadius: '4px',
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              height: '24px',
              width: '32px',
            }}
            title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
          >
            {isDark ? '🌙' : '☀️'}
          </button>
        </div>
      </div>

      {/* Content */}
      <div
        style={{
          flex: 1,
          overflow: 'hidden',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        {selectedSessionId ? (
          <>
            <div
              style={{
                display: activeTab === 'console' ? 'flex' : 'none',
                height: '100%',
              }}
            >
              <ConsoleView logs={filteredConsoleLogs} t={t} />
            </div>
            <div
              style={{
                display: activeTab === 'network' ? 'flex' : 'none',
                height: '100%',
              }}
            >
              <NetworkView logs={filteredNetworkLogs} t={t} isDark={isDark} />
            </div>
          </>
        ) : (
          <div
            style={{
              flex: 1,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              color: t.textSecondary,
              fontSize: '14px',
            }}
          >
            Please start Gemini CLI to begin debugging
          </div>
        )}
      </div>

      {/* Toast Notification */}
      {toastMessage && (
        <div
          style={{
            position: 'fixed',
            bottom: '24px',
            right: '24px',
            background: t.accent,
            color: '#fff',
            padding: '12px 24px',
            borderRadius: '8px',
            boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
            fontSize: '13px',
            fontWeight: 500,
            zIndex: 1000,
            animation: 'fadeInOut 5s ease forwards',
          }}
        >
          {toastMessage}
        </div>
      )}

      {/* CSS Animations */}
      <style>{`
        @keyframes fadeInOut {
          0% { opacity: 0; transform: translateY(10px); }
          5% { opacity: 1; transform: translateY(0); }
          95% { opacity: 1; transform: translateY(0); }
          100% { opacity: 0; transform: translateY(10px); }
        }
      `}</style>
    </div>
  );
}

function TabButton({
  active,
  onClick,
  label,
  t,
}: {
  active: boolean;
  onClick: () => void;
  label: string;
  t: ThemeColors;
}) {
  return (
    <div
      onClick={onClick}
      style={{
        padding: '4px 16px',
        cursor: 'pointer',
        color: active ? t.accent : t.textSecondary,
        fontWeight: 600,
        fontSize: '12px',
        userSelect: 'none',
        borderBottom: active
          ? `2px solid ${t.accent}`
          : '2px solid transparent',
        height: '100%',
        boxSizing: 'border-box',
        display: 'flex',
        alignItems: 'center',
        transition: 'all 0.2s',
      }}
    >
      {label}
    </div>
  );
}

// --- Console Components ---

function ConsoleLogEntry({ log, t }: { log: ConsoleLog; t: ThemeColors }) {
  const [isExpanded, setIsExpanded] = useState(false);
  const content = log.content || '';
  const lines = content.split('\n');
  const CHAR_LIMIT = 500;
  const LINE_LIMIT = 5;

  const isTooLong = content.length > CHAR_LIMIT;
  const isTooManyLines = lines.length > LINE_LIMIT;
  const needsCollapse = isTooLong || isTooManyLines;

  const isError = log.type === 'error';
  const isWarn = log.type === 'warn';
  const bg = isError ? t.errorBg : isWarn ? t.warnBg : 'transparent';
  const color = isError ? t.errorText : isWarn ? t.warnText : t.text;
  const icon = isError ? '❌' : isWarn ? '⚠️' : ' ';

  let displayContent = content;
  if (needsCollapse && !isExpanded) {
    if (isTooManyLines) {
      displayContent = lines.slice(0, LINE_LIMIT).join('\n') + '\n...';
    } else {
      displayContent = content.substring(0, CHAR_LIMIT) + '...';
    }
  }

  return (
    <div
      style={{
        display: 'flex',

        borderBottom: `1px solid ${t.rowBorder}`,

        padding: '4px 12px',

        backgroundColor: bg,

        alignItems: 'flex-start',

        gap: '8px',
      }}
    >
      <div
        style={{
          width: '16px',

          textAlign: 'center',

          flexShrink: 0,

          fontSize: '10px',

          marginTop: '2px',
        }}
      >
        {icon}
      </div>

      <div
        style={{
          flex: 1,

          display: 'flex',

          flexDirection: 'column',
        }}
      >
        <div
          style={{
            whiteSpace: 'pre-wrap',

            wordBreak: 'break-all',

            color: color,

            lineHeight: '1.5',

            fontSize: '11px',
          }}
        >
          {displayContent}
        </div>
      </div>

      <div
        style={{
          display: 'flex',

          alignItems: 'center',

          gap: '8px',

          flexShrink: 0,
        }}
      >
        {needsCollapse && (
          <div
            onClick={() => setIsExpanded(!isExpanded)}
            style={{
              fontSize: '12px',

              color: t.text,

              cursor: 'pointer',

              fontWeight: 'bold',

              userSelect: 'none',

              width: '20px',

              height: '20px',

              display: 'flex',

              alignItems: 'center',

              justifyContent: 'center',

              borderRadius: '4px',

              border: `1px solid ${t.border}`,

              background: t.bgSecondary,

              transition: 'all 0.1s',
            }}
            onMouseOver={(e) => {
              (e.currentTarget as HTMLDivElement).style.background = t.bgHover;
            }}
            onMouseOut={(e) => {
              (e.currentTarget as HTMLDivElement).style.background =
                t.bgSecondary;
            }}
            title={isExpanded ? 'Collapse' : 'Expand'}
          >
            {isExpanded ? '−' : '+'}
          </div>
        )}

        <div
          style={{
            color: t.textSecondary,

            fontSize: '10px',

            userSelect: 'none',

            textAlign: 'right',

            minWidth: '70px',
          }}
        >
          {new Date(log.timestamp).toLocaleTimeString([], {
            hour12: false,

            hour: '2-digit',

            minute: '2-digit',

            second: '2-digit',
          })}
        </div>
      </div>
    </div>
  );
}

function ConsoleView({ logs, t }: { logs: ConsoleLog[]; t: ThemeColors }) {
  const bottomRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [logs.length]);

  if (logs.length === 0) {
    return (
      <div
        style={{
          padding: '20px',

          color: t.textSecondary,

          fontSize: '11px',

          textAlign: 'center',

          flex: 1,
        }}
      >
        No console logs in this session
      </div>
    );
  }

  return (
    <div
      style={{
        flex: 1,

        overflowY: 'auto',

        fontFamily:
          'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace',

        background: t.consoleBg,

        fontSize: '12px',
      }}
    >
      {logs.map((log) => (
        <ConsoleLogEntry key={log.id} log={log} t={t} />
      ))}

      <div ref={bottomRef} />
    </div>
  );
}

// --- Network Components ---

function NetworkView({
  logs,
  t,
  isDark,
}: {
  logs: NetworkLog[];
  t: ThemeColors;
  isDark: boolean;
}) {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [filter, setFilter] = useState('');
  const [groupByDomain, setGroupByDomain] = useState(true);
  const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(
    {},
  );
  const [sidebarWidth, setSidebarWidth] = useState(400);
  const isResizing = useRef(false);

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (!isResizing.current) return;
      const newWidth = Math.max(
        200,
        Math.min(e.clientX, window.innerWidth - 200),
      );
      setSidebarWidth(newWidth);
    };
    const handleMouseUp = () => {
      isResizing.current = false;
      document.body.style.cursor = 'default';
      document.body.style.userSelect = 'auto';
    };
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, []);

  const startResizing = () => {
    isResizing.current = true;
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
  };

  const filteredLogs = useMemo(() => {
    let result = logs;
    if (filter) {
      const lower = filter.toLowerCase();
      result = logs.filter((l) => l.url.toLowerCase().includes(lower));
    }
    return result;
  }, [logs, filter]);

  const groupedLogs = useMemo(() => {
    if (!groupByDomain) return null;
    const groups: Record<string, NetworkLog[]> = {};
    filteredLogs.forEach((log) => {
      let groupKey = 'Other';
      try {
        const url = new URL(log.url);
        const lastSlashIndex = url.pathname.lastIndexOf('/');
        const basePath =
          lastSlashIndex !== -1
            ? url.pathname.substring(0, lastSlashIndex + 1)
            : '/';
        groupKey = url.hostname + basePath;
      } catch {
        /* ignore */
      }
      if (!groups[groupKey]) groups[groupKey] = [];
      groups[groupKey].push(log);
    });
    return groups;
  }, [filteredLogs, groupByDomain]);

  useEffect(() => {
    if (groupedLogs) {
      setExpandedGroups((prev) => {
        const next = { ...prev };
        Object.keys(groupedLogs).forEach((key) => {
          if (next[key] === undefined) {
            // Collapse play.googleapis.com by default
            next[key] = !key.includes('play.googleapis.com');
          }
        });
        return next;
      });
    }
  }, [groupedLogs]);

  const toggleGroup = (key: string) => {
    setExpandedGroups((prev) => ({ ...prev, [key]: !prev[key] }));
  };

  // --- Context Menu --- (reserved for future actions)

  const selectedLog = logs.find((l) => l.id === selectedId);

  const renderLogItem = (log: NetworkLog, nameOverride?: string) => {
    const isPending = log.pending;
    const status = log.response
      ? log.response.status
      : log.error
        ? 'ERR'
        : '...';
    const isError = log.error || (log.response && log.response.status >= 400);

    let name = nameOverride || log.url;
    if (!nameOverride) {
      try {
        const urlObj = new URL(log.url);
        name = urlObj.pathname + urlObj.search;
      } catch {
        /* ignore */
      }
    }

    const isSelected = log.id === selectedId;

    return (
      <div
        key={log.id}
        onClick={() => setSelectedId(log.id)}
        style={{
          padding: '8px 12px',
          cursor: 'pointer',
          borderBottom: `1px solid ${t.rowBorder}`,
          display: 'flex',
          flexDirection: 'column',
          fontSize: '12px',
          backgroundColor: isSelected ? t.bgHover : 'transparent',
          color: isError ? '#f28b82' : t.text,
          paddingLeft: nameOverride ? '24px' : '12px',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center' }}>
          <span
            style={{
              fontWeight: 'bold',
              width: '45px',
              flexShrink: 0,
              fontSize: '10px',
              color: isDark ? '#81c995' : '#188038', // Green for methods
            }}
          >
            {log.method}
          </span>
          <span
            style={{
              flex: 1,
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              margin: '0 8px',
              fontWeight: 500,
            }}
            title={log.url}
          >
            {name}
          </span>
          <span
            style={{
              width: '40px',
              textAlign: 'right',
              flexShrink: 0,
              fontSize: '11px',
              color: isPending ? t.accent : isError ? '#f28b82' : '#81c995',
            }}
          >
            {isPending ? '⏳' : status}
          </span>
        </div>
      </div>
    );
  };

  if (logs.length === 0) {
    return (
      <div
        style={{
          flex: 1,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: t.textSecondary,
          fontSize: '12px',
        }}
      >
        No network activity in this session
      </div>
    );
  }

  return (
    <div style={{ display: 'flex', width: '100%', height: '100%' }}>
      {/* List */}
      <div
        style={{
          width: `${sidebarWidth}px`,
          display: 'flex',
          flexDirection: 'column',
          borderRight: `1px solid ${t.border}`,
          background: t.bg,
        }}
      >
        <div
          style={{
            padding: '6px',
            background: t.bgSecondary,
            borderBottom: `1px solid ${t.border}`,
            display: 'flex',
            gap: '6px',
          }}
        >
          <input
            type="text"
            placeholder="Filter..."
            value={filter}
            onChange={(e) => setFilter(e.target.value)}
            style={{
              flex: 1,
              boxSizing: 'border-box',
              padding: '4px 10px',
              background: t.bg,
              color: t.text,
              border: `1px solid ${t.border}`,
              borderRadius: '4px',
              fontSize: '12px',
            }}
          />
          <button
            onClick={() => setGroupByDomain(!groupByDomain)}
            style={{
              background: groupByDomain ? t.accent : t.bg,
              color: groupByDomain ? '#fff' : t.text,
              border: `1px solid ${t.border}`,
              borderRadius: '4px',
              cursor: 'pointer',
              fontSize: '12px',
              padding: '0 8px',
            }}
            title="Group by Domain"
          >
            📂
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto' }}>
          {groupByDomain && groupedLogs
            ? Object.keys(groupedLogs).map((groupKey) => (
                <div key={groupKey}>
                  <div
                    onClick={() => toggleGroup(groupKey)}
                    style={{
                      padding: '6px 12px',
                      background: t.bgSecondary,
                      fontWeight: 'bold',
                      fontSize: '11px',
                      borderBottom: `1px solid ${t.rowBorder}`,
                      wordBreak: 'break-all',
                      cursor: 'pointer',
                      display: 'flex',
                      alignItems: 'center',
                      userSelect: 'none',
                    }}
                  >
                    <span
                      style={{
                        marginRight: '8px',
                        fontSize: '9px',
                        color: t.textSecondary,
                      }}
                    >
                      {expandedGroups[groupKey] ? '▼' : '▶'}
                    </span>
                    {groupKey}
                    <span
                      style={{
                        marginLeft: 'auto',
                        fontWeight: 'normal',
                        color: t.textSecondary,
                        fontSize: '10px',
                        background: t.bg,
                        padding: '0 6px',
                        borderRadius: '10px',
                      }}
                    >
                      {groupedLogs[groupKey].length}
                    </span>
                  </div>
                  {expandedGroups[groupKey] &&
                    groupedLogs[groupKey].map((log) => {
                      let displayName = log.url;
                      try {
                        const url = new URL(log.url);
                        const lastSlashIndex = url.pathname.lastIndexOf('/');
                        const suffix = url.pathname.substring(
                          lastSlashIndex + 1,
                        );
                        displayName = (suffix || '/') + url.search;
                      } catch {
                        /* ignore */
                      }
                      return renderLogItem(log, displayName);
                    })}
                </div>
              ))
            : filteredLogs.map((log) => renderLogItem(log))}
        </div>
      </div>

      {/* Resizer */}
      <div
        onMouseDown={startResizing}
        style={{
          width: '2px',
          cursor: 'col-resize',
          background: t.border,
          flexShrink: 0,
          zIndex: 10,
        }}
      />

      {/* Detail */}
      <div
        style={{
          flex: 1,
          display: 'flex',
          flexDirection: 'column',
          overflow: 'hidden',
          background: t.bg,
        }}
      >
        {selectedLog ? (
          <NetworkDetail log={selectedLog} t={t} />
        ) : (
          <div
            style={{
              padding: '40px',
              textAlign: 'center',
              color: t.textSecondary,
              fontSize: '14px',
            }}
          >
            Select a request to view details
          </div>
        )}
      </div>
    </div>
  );
}

type Tab = 'headers' | 'payload' | 'response';

function NetworkDetail({ log, t }: { log: NetworkLog; t: ThemeColors }) {
  const [activeTab, setActiveTab] = useState<Tab>('headers');
  const status = log.response
    ? log.pending
      ? '⏳'
      : log.response.status
    : log.error
      ? 'Error'
      : '⏳';

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        height: '100%',
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          padding: '12px 16px',
          borderBottom: `1px solid ${t.border}`,
          background: t.bgSecondary,
        }}
      >
        <div
          style={{
            fontWeight: 'bold',
            fontSize: '13px',
            marginBottom: '6px',
            wordBreak: 'break-all',
            color: t.text,
          }}
        >
          {log.url}
        </div>
        <div
          style={{
            fontSize: '11px',
            color: t.textSecondary,
            display: 'flex',
            gap: '8px',
          }}
        >
          <span
            style={{
              background: t.bg,
              padding: '1px 6px',
              borderRadius: '3px',
              fontWeight: 'bold',
            }}
          >
            {log.method}
          </span>
          <span>•</span>
          <span style={{ color: log.error ? '#f28b82' : '#81c995' }}>
            {status}
          </span>
          <span>•</span>
          <span>{new Date(log.timestamp).toLocaleTimeString()}</span>
          {log.response && (
            <>
              <span>•</span>
              <span style={{ color: t.accent }}>
                {log.response.durationMs}ms
              </span>
            </>
          )}
        </div>
      </div>
      <div
        style={{
          display: 'flex',
          borderBottom: `1px solid ${t.border}`,
          background: t.bgSecondary,
          paddingLeft: '8px',
        }}
      >
        {(['headers', 'payload', 'response'] as const).map((tab) => (
          <div
            key={tab}
            onClick={() => setActiveTab(tab)}
            style={{
              padding: '8px 16px',
              cursor: 'pointer',
              fontWeight: 600,
              fontSize: '12px',
              textTransform: 'capitalize',
              borderBottom:
                activeTab === tab
                  ? `2px solid ${t.accent}`
                  : '2px solid transparent',
              color: activeTab === tab ? t.accent : t.textSecondary,
              transition: 'all 0.2s',
            }}
          >
            {tab}
          </div>
        ))}
      </div>
      <div style={{ flex: 1, overflowY: 'auto', background: t.bg }}>
        {activeTab === 'headers' && (
          <div style={{ padding: '16px' }}>
            <Section title="General" t={t}>
              <Pair k="Request URL" v={log.url} t={t} />
              <Pair k="Request Method" v={log.method} t={t} />
              <Pair
                k="Status Code"
                v={String(log.response ? log.response.status : 'Pending')}
                t={t}
                color={log.error ? '#f28b82' : '#81c995'}
              />
              {log.error && (
                <Pair k="Error" v={log.error} t={t} color="#f28b82" />
              )}
            </Section>
            <Section title="Response Headers" t={t}>
              {log.response ? (
                <HeadersMap headers={log.response.headers} t={t} />
              ) : (
                <span style={{ fontStyle: 'italic', color: t.textSecondary }}>
                  (no response yet)
                </span>
              )}
            </Section>
            <Section title="Request Headers" t={t}>
              <HeadersMap headers={log.headers} t={t} />
            </Section>
          </div>
        )}
        {activeTab === 'payload' && <BodyView content={log.body} t={t} />}
        {activeTab === 'response' && (
          <BodyView content={log.response?.body} chunks={log.chunks} t={t} />
        )}
      </div>
    </div>
  );
}

function Section({
  title,
  children,
  t,
}: {
  title: string;
  children: React.ReactNode;
  t: ThemeColors;
}) {
  const [collapsed, setCollapsed] = useState(false);
  return (
    <div
      style={{
        marginBottom: '16px',
        border: `1px solid ${t.border}`,
        borderRadius: '6px',
        overflow: 'hidden',
      }}
    >
      <div
        onClick={() => setCollapsed(!collapsed)}
        style={{
          padding: '8px 12px',
          background: t.bgSecondary,
          fontWeight: 'bold',
          fontSize: '11px',
          cursor: 'pointer',
          userSelect: 'none',
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
        }}
      >
        <span style={{ fontSize: '9px', color: t.textSecondary }}>
          {collapsed ? '▶' : '▼'}
        </span>
        {title}
      </div>
      {!collapsed && (
        <div style={{ padding: '12px', background: t.bg }}>{children}</div>
      )}
    </div>
  );
}

function Pair({
  k,
  v,
  color,
  t,
}: {
  k: string;
  v: string;
  color?: string;
  t: ThemeColors;
}) {
  return (
    <div
      style={{
        display: 'flex',
        fontSize: '12px',
        fontFamily: 'monospace',
        marginBottom: '4px',
        lineHeight: '1.4',
      }}
    >
      <div
        style={{
          fontWeight: 'bold',
          color: t.textSecondary,
          width: '160px',
          flexShrink: 0,
        }}
      >
        {k}:
      </div>
      <div style={{ flex: 1, wordBreak: 'break-all', color: color || t.text }}>
        {v}
      </div>
    </div>
  );
}

function HeadersMap({
  headers,
  t,
}: {
  headers: Record<string, unknown> | undefined;
  t: ThemeColors;
}) {
  if (!headers) return <div style={{ color: t.textSecondary }}>(none)</div>;
  return (
    <>
      {Object.entries(headers).map(([k, v]) => (
        <Pair key={k} k={k} v={String(v)} t={t} />
      ))}
    </>
  );
}

function BodyView({
  content,
  chunks,
  t,
}: {
  content?: string;
  chunks?: Array<{ index: number; data: string; timestamp: number }>;
  t: ThemeColors;
}) {
  const [mode, setMode] = useState<'json' | 'raw'>('json');
  const hasChunks = chunks && chunks.length > 0;
  const safeContent = hasChunks
    ? chunks.map((c) => c.data).join('')
    : content || '';
  const getFormattedJson = () => {
    try {
      return JSON.stringify(JSON.parse(safeContent), null, 2);
    } catch {
      return safeContent;
    }
  };

  const copyJson = () => {
    navigator.clipboard.writeText(getFormattedJson()).catch(() => {
      // Clipboard API unavailable — silently ignore
    });
  };

  const downloadJson = () => {
    const blob = new Blob([getFormattedJson()], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'body.json';
    a.click();
    URL.revokeObjectURL(url);
  };

  if (!safeContent && !hasChunks)
    return (
      <div
        style={{ padding: '40px', color: t.textSecondary, textAlign: 'center' }}
      >
        (No content)
      </div>
    );

  const iconBtn = {
    background: 'none',
    border: 'none',
    cursor: 'pointer',
    color: t.textSecondary,
    padding: '2px',
    borderRadius: '4px',
    display: 'flex',
    alignItems: 'center',
  } as const;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <div
        style={{
          padding: '6px 12px',
          background: t.bgSecondary,
          borderBottom: `1px solid ${t.border}`,
          display: 'flex',
          gap: '8px',
          alignItems: 'center',
        }}
      >
        {(['json', 'raw'] as const).map((m) => (
          <button
            key={m}
            onClick={() => setMode(m)}
            style={{
              fontSize: '11px',
              padding: '2px 8px',
              borderRadius: '4px',
              border: `1px solid ${t.border}`,
              background: mode === m ? t.accent : t.bg,
              color: mode === m ? '#fff' : t.text,
              cursor: 'pointer',
              textTransform: 'uppercase',
              fontWeight: 'bold',
            }}
          >
            {m}
          </button>
        ))}
        <div style={{ flex: 1 }} />
        <button onClick={copyJson} style={iconBtn} title="Copy JSON">
          <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
            <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z" />
            <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z" />
          </svg>
        </button>
        <button onClick={downloadJson} style={iconBtn} title="Download JSON">
          <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
            <path d="M2.75 14A1.75 1.75 0 011 12.25v-2.5a.75.75 0 011.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-2.5a.75.75 0 011.5 0v2.5A1.75 1.75 0 0113.25 14z" />
            <path d="M7.25 7.689V2a.75.75 0 011.5 0v5.689l1.97-1.969a.749.749 0 111.06 1.06l-3.25 3.25a.749.749 0 01-1.06 0L4.22 6.78a.749.749 0 111.06-1.06z" />
          </svg>
        </button>
      </div>
      <div
        style={{
          flex: 1,
          overflow: 'auto',
          padding: mode === 'raw' ? '16px' : 0,
        }}
      >
        {hasChunks && mode === 'raw' ? (
          <div>
            {chunks.map((chunk, i) => (
              <div key={i} style={{ marginBottom: '12px' }}>
                <div
                  style={{
                    fontSize: '10px',
                    color: t.textSecondary,
                    marginBottom: '4px',
                  }}
                >
                  [
                  {new Date(chunk.timestamp).toLocaleTimeString('en-US', {
                    hour12: false,
                  })}
                  .{String(chunk.timestamp % 1000).padStart(3, '0')}]
                </div>
                <pre
                  style={{
                    margin: 0,
                    fontSize: '12px',
                    fontFamily: 'SFMono-Regular, Consolas, monospace',
                    whiteSpace: 'pre-wrap',
                    wordBreak: 'break-all',
                    color: t.text,
                    lineHeight: '1.5',
                  }}
                >
                  {chunk.data}
                </pre>
              </div>
            ))}
          </div>
        ) : mode === 'raw' ? (
          <pre
            style={{
              margin: 0,
              fontSize: '12px',
              fontFamily: 'SFMono-Regular, Consolas, monospace',
              whiteSpace: 'pre-wrap',
              wordBreak: 'break-all',
              color: t.text,
              lineHeight: '1.5',
            }}
          >
            {safeContent}
          </pre>
        ) : (
          <JsonViewer content={safeContent} t={t} />
        )}
      </div>
    </div>
  );
}

function JsonViewer({ content, t }: { content: string; t: ThemeColors }) {
  const safeContent = content || '';
  if (safeContent.includes('data:')) {
    const chunks = safeContent
      .split(/\n\s*\n/)
      .map((eventBlock, i) => ({
        index: i + 1,
        jsonStr: eventBlock
          .split('\n')
          .filter((line) => line.trim().startsWith('data:'))
          .map((line) => line.trim().substring(5).trim())
          .join(''),
      }))
      .filter((c) => c.jsonStr);
    if (chunks.length > 0) {
      return (
        <div>
          {chunks.map((chunk) => (
            <div
              key={chunk.index}
              style={{
                marginBottom: '12px',
                borderLeft: `2px solid ${t.accent}`,
                paddingLeft: '12px',
                background: t.bgSecondary,
                borderRadius: '0 4px 4px 0',
                padding: '8px 12px',
              }}
            >
              <div
                style={{
                  fontWeight: 'bold',
                  color: t.textSecondary,
                  fontSize: '10px',
                  marginBottom: '4px',
                }}
              >
                CHUNK {chunk.index}
              </div>
              <CodeView data={tryParse(chunk.jsonStr)} t={t} />
            </div>
          ))}
        </div>
      );
    }
  }
  return <CodeView data={tryParse(safeContent)} t={t} />;
}

function tryParse(str: string) {
  try {
    return JSON.parse(str);
  } catch {
    return str;
  }
}

interface JsonLine {
  text: string;
  foldStart: boolean;
  foldEnd: number; // -1 if not a fold start
  closingBracket: string; // '}' or ']' for fold starts
}

function jsonToLines(data: unknown): JsonLine[] {
  const str =
    typeof data === 'string' && !data.startsWith('{') && !data.startsWith('[')
      ? data
      : JSON.stringify(data, null, 2);
  if (str == null)
    return [
      { text: 'undefined', foldStart: false, foldEnd: -1, closingBracket: '' },
    ];
  const raw = str.split('\n');
  const lines: JsonLine[] = raw.map((text) => ({
    text,
    foldStart: false,
    foldEnd: -1,
    closingBracket: '',
  }));
  // Match opening brackets to closing brackets using a stack
  const stack: Array<{ index: number; bracket: string }> = [];
  for (let i = 0; i < raw.length; i++) {
    const trimmed = raw[i].trimEnd();
    const last = trimmed[trimmed.length - 1];
    if (last === '{' || last === '[') {
      stack.push({ index: i, bracket: last === '{' ? '}' : ']' });
    }
    // Check for closing bracket (with or without trailing comma)
    const stripped = trimmed.replace(/,\s*$/, '');
    const closeChar = stripped[stripped.length - 1];
    if (
      (closeChar === '}' || closeChar === ']') &&
      stack.length > 0 &&
      stack[stack.length - 1].bracket === closeChar
    ) {
      const open = stack.pop()!;
      lines[open.index].foldStart = true;
      lines[open.index].foldEnd = i;
      lines[open.index].closingBracket = open.bracket;
    }
  }
  return lines;
}

// Tokenize a JSON line for syntax highlighting
const TOKEN_RE =
  /("(?:[^"\\]|\\.)*")\s*(?=:)|("(?:[^"\\]|\\.)*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)|([{}[\]:,])/g;

function highlightLine(text: string, t: ThemeColors): React.ReactNode {
  const parts: React.ReactNode[] = [];
  let lastIndex = 0;
  let match: RegExpExecArray | null;
  TOKEN_RE.lastIndex = 0;

  while ((match = TOKEN_RE.exec(text)) !== null) {
    // Push any unmatched text before this token (whitespace/other)
    if (match.index > lastIndex) {
      parts.push(text.slice(lastIndex, match.index));
    }
    const [full, key, str, num, bool, nul] = match;
    if (key) {
      parts.push(
        <span key={lastIndex} style={{ color: t.accent, fontWeight: 'bold' }}>
          {full}
        </span>,
      );
    } else if (str) {
      // Unescape JSON string escapes so \n renders as actual newlines.
      // Use JSON.parse to handle all escape sequences correctly in one pass
      // (manual chained .replace() can double-unescape e.g. \\n → \<newline>).
      let unescaped: string;
      try {
        unescaped = JSON.parse(full) as string;
      } catch {
        unescaped = full.slice(1, -1);
      }
      const strLines = unescaped.split('\n');
      if (strLines.length <= 1) {
        parts.push(
          <span key={lastIndex} style={{ color: '#81c995' }}>
            {full}
          </span>,
        );
      } else {
        const indent = ' '.repeat(match.index + 1);
        parts.push(
          <CollapsibleString
            key={lastIndex}
            lines={strLines}
            indent={indent}
            t={t}
          />,
        );
      }
    } else if (num) {
      parts.push(
        <span key={lastIndex} style={{ color: '#ad7fa8' }}>
          {full}
        </span>,
      );
    } else if (bool) {
      parts.push(
        <span key={lastIndex} style={{ color: '#fdd663', fontWeight: 'bold' }}>
          {full}
        </span>,
      );
    } else if (nul) {
      parts.push(
        <span key={lastIndex} style={{ color: '#babdb6', fontWeight: 'bold' }}>
          {full}
        </span>,
      );
    } else {
      // punctuation
      parts.push(<span key={lastIndex}>{full}</span>);
    }
    lastIndex = match.index + full.length;
  }
  // Remaining text
  if (lastIndex < text.length) {
    parts.push(text.slice(lastIndex));
  }
  return parts;
}

const STRING_LINE_THRESHOLD = 20;

function CollapsibleString({
  lines,
  indent,
  t,
}: {
  lines: string[];
  indent: string;
  t: ThemeColors;
}) {
  const [expanded, setExpanded] = useState(false);
  const needsTruncation = lines.length > STRING_LINE_THRESHOLD;
  const displayLines =
    needsTruncation && !expanded
      ? lines.slice(0, STRING_LINE_THRESHOLD)
      : lines;

  return (
    <span style={{ color: '#81c995' }}>
      &quot;{displayLines[0]}
      {displayLines.slice(1).map((sl, si) => (
        <React.Fragment key={si}>
          {'\n'}
          {indent}
          {sl}
        </React.Fragment>
      ))}
      {needsTruncation && (
        <>
          {'\n'}
          {indent}
          <span
            onClick={() => setExpanded(!expanded)}
            style={{
              color: t.accent,
              cursor: 'pointer',
              fontStyle: 'italic',
              userSelect: 'none',
            }}
          >
            {expanded
              ? '▲ collapse'
              : `... ${lines.length - STRING_LINE_THRESHOLD} more lines`}
          </span>
        </>
      )}
      &quot;
    </span>
  );
}

function CodeView({ data, t }: { data: unknown; t: ThemeColors }) {
  const lines = useMemo(() => jsonToLines(data), [data]);
  const [collapsed, setCollapsed] = useState<Set<number>>(() => new Set());
  const contentRef = useRef<HTMLDivElement>(null);

  const toggleFold = (lineIndex: number) => {
    setCollapsed((prev) => {
      const next = new Set(prev);
      if (next.has(lineIndex)) {
        next.delete(lineIndex);
      } else {
        next.add(lineIndex);
      }
      return next;
    });
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
      e.preventDefault();
      const sel = window.getSelection();
      if (sel && contentRef.current) {
        sel.removeAllRanges();
        const range = document.createRange();
        range.selectNodeContents(contentRef.current);
        sel.addRange(range);
      }
    }
  };

  // Build visible lines, skipping folded regions
  const visibleLines: Array<{
    index: number;
    content: React.ReactNode;
    foldable: boolean;
    isCollapsed: boolean;
  }> = [];

  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    const isCollapsed = collapsed.has(i);
    if (line.foldStart && isCollapsed) {
      // Show the opening line with collapsed indicator
      const indent = line.text.match(/^(\s*)/)?.[1] || '';
      const trimmed = line.text.trimStart();
      visibleLines.push({
        index: i,
        content: (
          <>
            {indent.length > 0 && <span>{indent}</span>}
            {highlightLine(trimmed, t)}
            <span style={{ color: t.textSecondary, fontStyle: 'italic' }}>
              {' '}
              ... {line.closingBracket}
            </span>
          </>
        ),
        foldable: true,
        isCollapsed: true,
      });
      // Skip to the line after foldEnd
      i = line.foldEnd + 1;
    } else {
      visibleLines.push({
        index: i,
        content: highlightLine(line.text, t),
        foldable: line.foldStart,
        isCollapsed: false,
      });
      i++;
    }
  }

  return (
    <div
      tabIndex={0}
      onKeyDown={handleKeyDown}
      ref={contentRef}
      data-code-view
      style={{
        display: 'grid',
        gridTemplateColumns: '20px 1fr',
        fontFamily:
          'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace',
        fontSize: '12px',
        lineHeight: '1.5',
        outline: 'none',
      }}
    >
      {visibleLines.map((vl) => (
        <React.Fragment key={vl.index}>
          {/* Gutter cell */}
          <div
            data-gutter
            style={{
              userSelect: 'none',
              textAlign: 'center',
              color: t.textSecondary,
              borderRight: `1px solid ${t.border}`,
              paddingRight: '2px',
              cursor: vl.foldable ? 'pointer' : 'default',
              fontSize: '9px',
              paddingTop: '3px',
            }}
            onClick={vl.foldable ? () => toggleFold(vl.index) : undefined}
          >
            {vl.foldable ? (
              <span className="fold-icon">{vl.isCollapsed ? '▶' : '▼'}</span>
            ) : (
              ''
            )}
          </div>
          {/* Content cell */}
          <div
            style={{
              whiteSpace: 'pre',
              color: t.text,
              paddingLeft: '8px',
              minHeight: '18px',
            }}
          >
            {vl.content}
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
