import { useEffect, useRef, useCallback, useState } from 'react';
import {
  X,
  SplitSquareHorizontal,
  SplitSquareVertical,
  GripHorizontal,
  Terminal,
  ZoomIn,
  ZoomOut,
  Copy,
  ClipboardPaste,
  CheckSquare,
  Trash2,
  ImageIcon,
  Loader2,
  Settings,
  RotateCcw,
  Search,
  ChevronUp,
  ChevronDown,
  Maximize2,
  Minimize2,
  ArrowDown,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS, type KeyboardShortcuts } from '@/store/app-store';
import { useShallow } from 'zustand/react/shallow';
import { matchesShortcutWithCode } from '@/hooks/use-keyboard-shortcuts';
import {
  getTerminalTheme,
  TERMINAL_FONT_OPTIONS,
  DEFAULT_TERMINAL_FONT,
} from '@/config/terminal-themes';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';

// Font size constraints
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 32;
const DEFAULT_FONT_SIZE = 14;

// Resize constraints
const RESIZE_DEBOUNCE_MS = 100; // Short debounce for responsive feel

// Image drag-drop constants
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB

// Large paste handling constants
const LARGE_PASTE_WARNING_THRESHOLD = 1024 * 1024; // 1MB - show warning for pastes this size or larger
const PASTE_CHUNK_SIZE = 8 * 1024; // 8KB chunks for large pastes
const PASTE_CHUNK_DELAY_MS = 10; // Small delay between chunks to prevent overwhelming WebSocket

interface TerminalPanelProps {
  sessionId: string;
  authToken: string | null;
  isActive: boolean;
  onFocus: () => void;
  onClose: () => void;
  onSplitHorizontal: () => void;
  onSplitVertical: () => void;
  onNewTab?: () => void;
  onNavigateUp?: () => void; // Navigate to terminal pane above
  onNavigateDown?: () => void; // Navigate to terminal pane below
  onNavigateLeft?: () => void; // Navigate to terminal pane on the left
  onNavigateRight?: () => void; // Navigate to terminal pane on the right
  onSessionInvalid?: () => void; // Called when session is no longer valid on server (e.g., server restarted)
  isDragging?: boolean;
  isDropTarget?: boolean;
  fontSize: number;
  onFontSizeChange: (size: number) => void;
  runCommandOnConnect?: string; // Command to run when terminal first connects (for new terminals)
  onCommandRan?: () => void; // Callback when the initial command has been sent
  isMaximized?: boolean;
  onToggleMaximize?: () => void;
}

// Type for xterm Terminal - we'll use any since we're dynamically importing
type XTerminal = InstanceType<typeof import('@xterm/xterm').Terminal>;
type XFitAddon = InstanceType<typeof import('@xterm/addon-fit').FitAddon>;
type XSearchAddon = InstanceType<typeof import('@xterm/addon-search').SearchAddon>;
type XWebLinksAddon = InstanceType<typeof import('@xterm/addon-web-links').WebLinksAddon>;

export function TerminalPanel({
  sessionId,
  authToken,
  isActive,
  onFocus,
  onClose,
  onSplitHorizontal,
  onSplitVertical,
  onNewTab,
  onNavigateUp,
  onNavigateDown,
  onNavigateLeft,
  onNavigateRight,
  onSessionInvalid,
  isDragging = false,
  isDropTarget = false,
  fontSize,
  onFontSizeChange,
  runCommandOnConnect,
  onCommandRan,
  isMaximized = false,
  onToggleMaximize,
}: TerminalPanelProps) {
  const terminalRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const xtermRef = useRef<XTerminal | null>(null);
  const fitAddonRef = useRef<XFitAddon | null>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const heartbeatIntervalRef = useRef<NodeJS.Timeout | null>(null);
  const lastShortcutTimeRef = useRef<number>(0);
  const resizeDebounceRef = useRef<NodeJS.Timeout | null>(null);
  const focusHandlerRef = useRef<{ dispose: () => void } | null>(null);
  const linkProviderRef = useRef<{ dispose: () => void } | null>(null);
  const [isTerminalReady, setIsTerminalReady] = useState(false);
  const [shellName, setShellName] = useState('shell');
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
  const [isMac, setIsMac] = useState(false);
  const isMacRef = useRef(false);
  const contextMenuRef = useRef<HTMLDivElement>(null);
  const [focusedMenuIndex, setFocusedMenuIndex] = useState(0);
  const focusedMenuIndexRef = useRef(0);
  const [isImageDragOver, setIsImageDragOver] = useState(false);
  const [isProcessingImage, setIsProcessingImage] = useState(false);
  const hasRunInitialCommandRef = useRef(false);
  const searchAddonRef = useRef<XSearchAddon | null>(null);
  const searchInputRef = useRef<HTMLInputElement>(null);
  const [showSearch, setShowSearch] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const showSearchRef = useRef(false);
  const [isAtBottom, setIsAtBottom] = useState(true);

  const [connectionStatus, setConnectionStatus] = useState<
    'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'auth_failed'
  >('connecting');
  const reconnectAttemptsRef = useRef(0);
  const MAX_RECONNECT_ATTEMPTS = 5;
  const INITIAL_RECONNECT_DELAY = 1000;
  const [processExitCode, setProcessExitCode] = useState<number | null>(null);

  // Get current project for image saving
  const currentProject = useAppStore((state) => state.currentProject);

  // Get terminal settings from store - grouped with shallow comparison to reduce re-renders
  const { defaultRunScript, screenReaderMode, fontFamily, scrollbackLines, lineHeight } =
    useAppStore(
      useShallow((state) => ({
        defaultRunScript: state.terminalState.defaultRunScript,
        screenReaderMode: state.terminalState.screenReaderMode,
        fontFamily: state.terminalState.fontFamily,
        scrollbackLines: state.terminalState.scrollbackLines,
        lineHeight: state.terminalState.lineHeight,
      }))
    );

  // Action setters are stable references, can use individual selectors
  const setTerminalDefaultRunScript = useAppStore((state) => state.setTerminalDefaultRunScript);
  const setTerminalScreenReaderMode = useAppStore((state) => state.setTerminalScreenReaderMode);
  const setTerminalFontFamily = useAppStore((state) => state.setTerminalFontFamily);
  const setTerminalScrollbackLines = useAppStore((state) => state.setTerminalScrollbackLines);
  const setTerminalLineHeight = useAppStore((state) => state.setTerminalLineHeight);

  // Detect platform on mount
  useEffect(() => {
    // Use modern userAgentData API with fallback to navigator.platform
    const nav = navigator as Navigator & { userAgentData?: { platform: string } };
    let detected = false;
    if (nav.userAgentData?.platform) {
      detected = nav.userAgentData.platform.toLowerCase().includes('mac');
    } else if (typeof navigator !== 'undefined') {
      // Fallback for browsers without userAgentData (intentionally using deprecated API)
      detected = /mac/i.test(navigator.platform);
    }
    setIsMac(detected);
    isMacRef.current = detected;
  }, []);

  // Get effective theme from store
  const getEffectiveTheme = useAppStore((state) => state.getEffectiveTheme);
  const effectiveTheme = getEffectiveTheme();

  // Get keyboard shortcuts from store - merged with defaults
  const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts);
  const mergedShortcuts: KeyboardShortcuts = {
    ...DEFAULT_KEYBOARD_SHORTCUTS,
    ...keyboardShortcuts,
  };
  const shortcutsRef = useRef(mergedShortcuts);
  shortcutsRef.current = mergedShortcuts;

  // Track system dark mode preference for "system" theme
  const [systemIsDark, setSystemIsDark] = useState(() => {
    if (typeof window !== 'undefined') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
    return false;
  });

  // Listen for system theme changes
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handleChange = (e: MediaQueryListEvent) => {
      setSystemIsDark(e.matches);
    };

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  // Resolve "system" theme to actual light/dark
  const resolvedTheme =
    effectiveTheme === 'system' ? (systemIsDark ? 'dark' : 'light') : effectiveTheme;

  // Use refs for callbacks and values to avoid effect re-runs
  const onFocusRef = useRef(onFocus);
  onFocusRef.current = onFocus;
  const onCloseRef = useRef(onClose);
  onCloseRef.current = onClose;
  const onSplitHorizontalRef = useRef(onSplitHorizontal);
  onSplitHorizontalRef.current = onSplitHorizontal;
  const onSplitVerticalRef = useRef(onSplitVertical);
  onSplitVerticalRef.current = onSplitVertical;
  const onNewTabRef = useRef(onNewTab);
  onNewTabRef.current = onNewTab;
  const onNavigateUpRef = useRef(onNavigateUp);
  onNavigateUpRef.current = onNavigateUp;
  const onNavigateDownRef = useRef(onNavigateDown);
  onNavigateDownRef.current = onNavigateDown;
  const onNavigateLeftRef = useRef(onNavigateLeft);
  onNavigateLeftRef.current = onNavigateLeft;
  const onNavigateRightRef = useRef(onNavigateRight);
  onNavigateRightRef.current = onNavigateRight;
  const onSessionInvalidRef = useRef(onSessionInvalid);
  onSessionInvalidRef.current = onSessionInvalid;
  const fontSizeRef = useRef(fontSize);
  fontSizeRef.current = fontSize;
  const themeRef = useRef(resolvedTheme);
  themeRef.current = resolvedTheme;
  const copySelectionRef = useRef<() => Promise<boolean>>(() => Promise.resolve(false));
  const pasteFromClipboardRef = useRef<() => Promise<void>>(() => Promise.resolve());

  // Zoom functions - use the prop callback
  const zoomIn = useCallback(() => {
    onFontSizeChange(Math.min(fontSize + 1, MAX_FONT_SIZE));
  }, [fontSize, onFontSizeChange]);

  const zoomOut = useCallback(() => {
    onFontSizeChange(Math.max(fontSize - 1, MIN_FONT_SIZE));
  }, [fontSize, onFontSizeChange]);

  const resetZoom = useCallback(() => {
    onFontSizeChange(DEFAULT_FONT_SIZE);
  }, [onFontSizeChange]);

  // Strip ANSI escape codes from text
  const stripAnsi = (text: string): string => {
    // Match ANSI escape sequences:
    // - CSI sequences: \x1b[...letter
    // - OSC sequences: \x1b]...ST
    // - Other escape sequences: \x1b followed by various characters
    // eslint-disable-next-line no-control-regex
    return text.replace(
      /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\x1b[>=<]|\x1b[78HM]|\x1b#[0-9]|\x1b./g,
      ''
    );
  };

  // Copy selected text to clipboard
  const copySelection = useCallback(async (): Promise<boolean> => {
    const terminal = xtermRef.current;
    if (!terminal) return false;

    const selection = terminal.getSelection();
    if (!selection) {
      toast.error('Nothing to copy', {
        description: 'Select some text first',
      });
      return false;
    }

    try {
      // Strip any ANSI escape codes that might be in the selection
      const cleanText = stripAnsi(selection);
      await navigator.clipboard.writeText(cleanText);
      toast.success('Copied to clipboard');
      return true;
    } catch (err) {
      console.error('[Terminal] Copy failed:', err);
      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
      toast.error('Copy failed', {
        description: errorMessage.includes('permission')
          ? 'Clipboard permission denied'
          : 'Could not access clipboard',
      });
      return false;
    }
  }, []);
  copySelectionRef.current = copySelection;

  // Helper function to send text in chunks with delay
  const sendTextInChunks = useCallback(async (text: string) => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;

    // For small pastes, send all at once
    if (text.length <= PASTE_CHUNK_SIZE) {
      ws.send(JSON.stringify({ type: 'input', data: text }));
      return;
    }

    // For large pastes, chunk it
    for (let i = 0; i < text.length; i += PASTE_CHUNK_SIZE) {
      if (ws.readyState !== WebSocket.OPEN) break;
      const chunk = text.slice(i, i + PASTE_CHUNK_SIZE);
      ws.send(JSON.stringify({ type: 'input', data: chunk }));
      // Small delay between chunks to prevent overwhelming the WebSocket
      if (i + PASTE_CHUNK_SIZE < text.length) {
        await new Promise((resolve) => setTimeout(resolve, PASTE_CHUNK_DELAY_MS));
      }
    }
  }, []);

  // Paste from clipboard
  const pasteFromClipboard = useCallback(async () => {
    const terminal = xtermRef.current;
    if (!terminal || !wsRef.current) return;

    try {
      const text = await navigator.clipboard.readText();
      if (!text) {
        toast.error('Nothing to paste', {
          description: 'Clipboard is empty',
        });
        return;
      }

      if (wsRef.current.readyState !== WebSocket.OPEN) {
        toast.error('Terminal not connected');
        return;
      }

      // Warn for large pastes
      if (text.length >= LARGE_PASTE_WARNING_THRESHOLD) {
        const sizeMB = (text.length / (1024 * 1024)).toFixed(1);
        toast.warning(`Large paste (${sizeMB}MB)`, {
          description: 'Sending in chunks, this may take a moment...',
          duration: 3000,
        });
      }

      await sendTextInChunks(text);
    } catch (err) {
      console.error('[Terminal] Paste failed:', err);
      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
      toast.error('Paste failed', {
        description: errorMessage.includes('permission')
          ? 'Clipboard permission denied'
          : 'Could not read from clipboard',
      });
    }
  }, [sendTextInChunks]);
  pasteFromClipboardRef.current = pasteFromClipboard;

  // Select all terminal content
  const selectAll = useCallback(() => {
    xtermRef.current?.selectAll();
  }, []);

  // Clear terminal
  const clearTerminal = useCallback(() => {
    xtermRef.current?.clear();
  }, []);

  // Get theme colors for search highlighting
  const terminalTheme = getTerminalTheme(effectiveTheme);
  const searchOptions = {
    caseSensitive: false,
    regex: false,
    decorations: {
      matchBackground: terminalTheme.searchMatchBackground,
      matchBorder: terminalTheme.searchMatchBorder,
      matchOverviewRuler: terminalTheme.searchMatchBorder,
      activeMatchBackground: terminalTheme.searchActiveMatchBackground,
      activeMatchBorder: terminalTheme.searchActiveMatchBorder,
      activeMatchColorOverviewRuler: terminalTheme.searchActiveMatchBorder,
    },
  };

  // Search functions
  const searchNext = useCallback(() => {
    if (searchAddonRef.current && searchQuery) {
      searchAddonRef.current.findNext(searchQuery, searchOptions);
    }
  }, [searchQuery, searchOptions]);

  const searchPrevious = useCallback(() => {
    if (searchAddonRef.current && searchQuery) {
      searchAddonRef.current.findPrevious(searchQuery, searchOptions);
    }
  }, [searchQuery, searchOptions]);

  const closeSearch = useCallback(() => {
    setShowSearch(false);
    showSearchRef.current = false;
    setSearchQuery('');
    searchAddonRef.current?.clearDecorations();
    xtermRef.current?.focus();
  }, []);

  // Handle pane navigation keyboard shortcuts at container level (capture phase)
  // This ensures we intercept before xterm can process the event
  const handleContainerKeyDownCapture = useCallback(
    (event: React.KeyboardEvent) => {
      // Ctrl+Alt+Arrow / Cmd+Alt+Arrow - Navigate between panes directionally
      if ((event.ctrlKey || event.metaKey) && event.altKey && !event.shiftKey) {
        const code = event.nativeEvent.code;
        if (code === 'ArrowRight') {
          event.preventDefault();
          event.stopPropagation();
          onNavigateRight?.();
        } else if (code === 'ArrowLeft') {
          event.preventDefault();
          event.stopPropagation();
          onNavigateLeft?.();
        } else if (code === 'ArrowDown') {
          event.preventDefault();
          event.stopPropagation();
          onNavigateDown?.();
        } else if (code === 'ArrowUp') {
          event.preventDefault();
          event.stopPropagation();
          onNavigateUp?.();
        }
      }
    },
    [onNavigateUp, onNavigateDown, onNavigateLeft, onNavigateRight]
  );

  // Scroll to bottom of terminal
  const scrollToBottom = useCallback(() => {
    if (xtermRef.current) {
      xtermRef.current.scrollToBottom();
      setIsAtBottom(true);
    }
  }, []);

  // Close context menu
  const closeContextMenu = useCallback(() => {
    setContextMenu(null);
  }, []);

  // Handle context menu action
  const handleContextMenuAction = useCallback(
    async (action: 'copy' | 'paste' | 'selectAll' | 'clear') => {
      closeContextMenu();
      switch (action) {
        case 'copy':
          await copySelection();
          break;
        case 'paste':
          await pasteFromClipboard();
          break;
        case 'selectAll':
          selectAll();
          break;
        case 'clear':
          clearTerminal();
          break;
      }
      xtermRef.current?.focus();
    },
    [closeContextMenu, copySelection, pasteFromClipboard, selectAll, clearTerminal]
  );

  const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:3008';
  const wsUrl = serverUrl.replace(/^http/, 'ws');

  // Draggable - only the drag handle triggers drag
  const {
    attributes: dragAttributes,
    listeners: dragListeners,
    setNodeRef: setDragRef,
  } = useDraggable({
    id: sessionId,
  });

  // Droppable - the entire panel is a drop target
  const { setNodeRef: setDropRef, isOver } = useDroppable({
    id: sessionId,
  });

  // Initialize terminal - dynamically import xterm to avoid SSR issues
  useEffect(() => {
    if (!terminalRef.current) return;

    let mounted = true;

    const initTerminal = async () => {
      // Dynamically import xterm modules
      const [{ Terminal }, { FitAddon }, { WebglAddon }, { SearchAddon }, { WebLinksAddon }] =
        await Promise.all([
          import('@xterm/xterm'),
          import('@xterm/addon-fit'),
          import('@xterm/addon-webgl'),
          import('@xterm/addon-search'),
          import('@xterm/addon-web-links'),
        ]);

      // Also import CSS
      await import('@xterm/xterm/css/xterm.css');

      if (!mounted || !terminalRef.current) return;

      // Get terminal theme matching the app theme
      const terminalTheme = getTerminalTheme(themeRef.current);

      // Get settings from store (read at initialization time)
      const terminalSettings = useAppStore.getState().terminalState;
      const screenReaderEnabled = terminalSettings.screenReaderMode;
      const terminalFontFamily = terminalSettings.fontFamily || DEFAULT_TERMINAL_FONT;
      const terminalScrollback = terminalSettings.scrollbackLines || 5000;
      const terminalLineHeight = terminalSettings.lineHeight || 1.0;

      // Create terminal instance with the current global font size and theme
      const terminal = new Terminal({
        cursorBlink: true,
        cursorStyle: 'block',
        fontSize: fontSizeRef.current,
        fontFamily: terminalFontFamily,
        lineHeight: terminalLineHeight,
        letterSpacing: 0,
        theme: terminalTheme,
        allowProposedApi: true,
        screenReaderMode: screenReaderEnabled,
        scrollback: terminalScrollback,
      });

      // Create fit addon
      const fitAddon = new FitAddon();
      terminal.loadAddon(fitAddon);

      // Create search addon
      const searchAddon = new SearchAddon();
      terminal.loadAddon(searchAddon);
      searchAddonRef.current = searchAddon;

      // Create web links addon for clickable URLs with custom handler for Electron
      const webLinksAddon = new WebLinksAddon((_event: MouseEvent, uri: string) => {
        // Use Electron API to open external links in system browser
        const api = getElectronAPI();
        if (api?.openExternalLink) {
          api.openExternalLink(uri).catch((error) => {
            console.error('[Terminal] Failed to open URL:', error);
            // Fallback to window.open if Electron API fails
            window.open(uri, '_blank', 'noopener,noreferrer');
          });
        } else {
          // Web fallback
          window.open(uri, '_blank', 'noopener,noreferrer');
        }
      });
      terminal.loadAddon(webLinksAddon);

      // Open terminal
      terminal.open(terminalRef.current);

      // Register custom link provider for file paths
      // Detects patterns like /path/to/file.ts:123:45 or ./src/file.js:10
      const filePathLinkProvider = {
        provideLinks: (
          lineNumber: number,
          callback: (
            links:
              | {
                  range: { start: { x: number; y: number }; end: { x: number; y: number } };
                  text: string;
                  activate: (event: MouseEvent, text: string) => void;
                }[]
              | undefined
          ) => void
        ) => {
          const line = terminal.buffer.active.getLine(lineNumber - 1);
          if (!line) {
            callback(undefined);
            return;
          }

          const lineText = line.translateToString(true);
          const links: {
            range: { start: { x: number; y: number }; end: { x: number; y: number } };
            text: string;
            activate: (event: MouseEvent, text: string) => void;
          }[] = [];

          // File path patterns:
          // 1. Absolute Unix: /path/to/file.ext:line:col or /path/to/file.ext:line
          // 2. Home directory: ~/path/to/file.ext:line:col
          // 3. Absolute Windows: C:\path\to\file.ext:line:col (less common in terminal output)
          // 4. Relative: ./path/file.ext:line or src/file.ext:line
          // Common formats from compilers/linters:
          // - ESLint: /path/file.ts:10:5
          // - TypeScript: src/file.ts(10,5)
          // - Go: /path/file.go:10:5
          const filePathRegex =
            /(?:^|[\s'"(])(((?:\/|\.\/|\.\.\/|~\/)[^\s:'"()]+|[a-zA-Z]:\\[^\s:'"()]+|[a-zA-Z0-9_-]+\/[^\s:'"()]+)(?:[:(\s](\d+)(?:[:,)](\d+))?)?)/g;

          let match;
          while ((match = filePathRegex.exec(lineText)) !== null) {
            const fullMatch = match[1];
            const filePath = match[2];
            const lineNum = match[3] ? parseInt(match[3], 10) : undefined;
            const colNum = match[4] ? parseInt(match[4], 10) : undefined;

            // Skip common false positives (URLs, etc.)
            if (
              filePath.startsWith('http://') ||
              filePath.startsWith('https://') ||
              filePath.startsWith('ws://')
            ) {
              continue;
            }

            // Calculate the start position (1-indexed for xterm)
            const startX = match.index + (match[0].length - match[1].length) + 1;
            const endX = startX + fullMatch.length;

            links.push({
              range: {
                start: { x: startX, y: lineNumber },
                end: { x: endX, y: lineNumber },
              },
              text: fullMatch,
              activate: async (event: MouseEvent, text: string) => {
                // Parse the path and line/column from the matched text
                const pathMatch = text.match(/^([^\s:()]+)(?:[:(\s](\d+)(?:[:,)](\d+))?)?/);
                if (!pathMatch) return;

                const clickedPath = pathMatch[1];
                const clickedLine = pathMatch[2] ? parseInt(pathMatch[2], 10) : undefined;
                const clickedCol = pathMatch[3] ? parseInt(pathMatch[3], 10) : undefined;

                // Resolve paths to absolute paths
                let absolutePath = clickedPath;
                const api = getElectronAPI();

                if (clickedPath.startsWith('~/')) {
                  // Home directory path - expand ~ to user's home directory
                  try {
                    const homePath = await api.getPath?.('home');
                    if (homePath) {
                      absolutePath = homePath + clickedPath.slice(1); // Replace ~ with home path
                    }
                  } catch {
                    // If we can't get home path, just use the path as-is
                    console.warn('[Terminal] Could not resolve home directory path');
                  }
                } else if (!clickedPath.startsWith('/') && !clickedPath.match(/^[a-zA-Z]:\\/)) {
                  // Relative path - resolve against project path
                  const projectPath = useAppStore.getState().currentProject?.path;
                  if (projectPath) {
                    absolutePath = `${projectPath}/${clickedPath}`.replace(/\/+/g, '/');
                  } else {
                    toast.warning('Cannot open relative path', {
                      description:
                        'No project selected. Open a project to click relative file paths.',
                    });
                    return;
                  }
                }

                // Open in editor using VS Code URL scheme
                // Works in both web (via anchor click) and Electron (via shell.openExternal)
                try {
                  const result = await api.openInEditor?.(absolutePath, clickedLine, clickedCol);
                  if (result && !result.success) {
                    toast.error('Failed to open in editor', { description: result.error });
                  }
                } catch (error) {
                  console.error('[Terminal] Failed to open file:', error);
                  toast.error('Failed to open file', {
                    description: error instanceof Error ? error.message : 'Unknown error',
                  });
                }
              },
            });
          }

          callback(links.length > 0 ? links : undefined);
        },
      };

      linkProviderRef.current = terminal.registerLinkProvider(filePathLinkProvider);

      // Try to load WebGL addon for better performance
      try {
        const webglAddon = new WebglAddon();
        webglAddon.onContextLoss(() => {
          webglAddon.dispose();
        });
        terminal.loadAddon(webglAddon);
      } catch {
        console.warn('[Terminal] WebGL addon not available, falling back to canvas');
      }

      // Fit terminal to container - wait for stable dimensions
      // Use initial delay then multiple RAFs to let react-resizable-panels finish layout
      let fitAttempts = 0;
      const MAX_FIT_ATTEMPTS = 10;
      let lastWidth = 0;
      let lastHeight = 0;

      const attemptFit = () => {
        if (!fitAddon || !terminalRef.current || fitAttempts >= MAX_FIT_ATTEMPTS) return;

        const rect = terminalRef.current.getBoundingClientRect();
        fitAttempts++;

        // Check if dimensions are stable (same as last attempt) and valid
        if (
          rect.width === lastWidth &&
          rect.height === lastHeight &&
          rect.width > 0 &&
          rect.height > 0
        ) {
          try {
            fitAddon.fit();
          } catch (err) {
            console.error('[Terminal] Initial fit error:', err);
          }
          return;
        }

        // Dimensions still changing or too small, try again
        lastWidth = rect.width;
        lastHeight = rect.height;
        requestAnimationFrame(attemptFit);
      };

      // Initial delay allows complex layouts to settle before attempting fit
      setTimeout(() => requestAnimationFrame(attemptFit), 50);

      xtermRef.current = terminal;
      fitAddonRef.current = fitAddon;
      setIsTerminalReady(true);

      // Handle focus - use ref to avoid re-running effect
      // Store disposer to prevent memory leak
      focusHandlerRef.current = terminal.onData(() => {
        onFocusRef.current();
      });

      // Custom key handler to intercept terminal shortcuts
      // Return false to prevent xterm from handling the key
      const SHORTCUT_COOLDOWN_MS = 300; // Prevent rapid firing

      terminal.attachCustomKeyEventHandler((event) => {
        // Only intercept keydown events
        if (event.type !== 'keydown') return true;

        // Use event.code for keyboard-layout-independent key detection
        const code = event.code;

        // Ctrl+Alt+Arrow / Cmd+Alt+Arrow - Navigate between panes directionally
        // Handle this FIRST before any other checks to prevent xterm from capturing it
        // Use explicit check for both Ctrl and Meta to work on all platforms
        if ((event.ctrlKey || event.metaKey) && event.altKey && !event.shiftKey) {
          if (code === 'ArrowRight') {
            event.preventDefault();
            event.stopPropagation();
            onNavigateRightRef.current?.();
            return false;
          } else if (code === 'ArrowLeft') {
            event.preventDefault();
            event.stopPropagation();
            onNavigateLeftRef.current?.();
            return false;
          } else if (code === 'ArrowDown') {
            event.preventDefault();
            event.stopPropagation();
            onNavigateDownRef.current?.();
            return false;
          } else if (code === 'ArrowUp') {
            event.preventDefault();
            event.stopPropagation();
            onNavigateUpRef.current?.();
            return false;
          }
        }

        // Check cooldown to prevent rapid terminal creation
        const now = Date.now();
        const canTrigger = now - lastShortcutTimeRef.current > SHORTCUT_COOLDOWN_MS;

        // Get current shortcuts from ref (allows customization)
        const shortcuts = shortcutsRef.current;

        // Split right (default: Alt+D)
        if (matchesShortcutWithCode(event, shortcuts.splitTerminalRight)) {
          event.preventDefault();
          if (canTrigger) {
            lastShortcutTimeRef.current = now;
            onSplitHorizontalRef.current();
          }
          return false;
        }

        // Split down (default: Alt+S)
        if (matchesShortcutWithCode(event, shortcuts.splitTerminalDown)) {
          event.preventDefault();
          if (canTrigger) {
            lastShortcutTimeRef.current = now;
            onSplitVerticalRef.current();
          }
          return false;
        }

        // Close terminal (default: Alt+W)
        if (matchesShortcutWithCode(event, shortcuts.closeTerminal)) {
          event.preventDefault();
          if (canTrigger) {
            lastShortcutTimeRef.current = now;
            onCloseRef.current();
          }
          return false;
        }

        // New terminal tab (default: Alt+T)
        if (matchesShortcutWithCode(event, shortcuts.newTerminalTab)) {
          event.preventDefault();
          if (canTrigger && onNewTabRef.current) {
            lastShortcutTimeRef.current = now;
            onNewTabRef.current();
          }
          return false;
        }

        const modKey = isMacRef.current ? event.metaKey : event.ctrlKey;
        const otherModKey = isMacRef.current ? event.ctrlKey : event.metaKey;

        // Ctrl+Shift+C / Cmd+Shift+C - Always copy (Linux terminal convention)
        if (modKey && !otherModKey && event.shiftKey && !event.altKey && code === 'KeyC') {
          event.preventDefault();
          copySelectionRef.current();
          return false;
        }

        // Ctrl+C / Cmd+C - Copy if text is selected, otherwise send SIGINT
        if (modKey && !otherModKey && !event.shiftKey && !event.altKey && code === 'KeyC') {
          const hasSelection = terminal.hasSelection();
          if (hasSelection) {
            event.preventDefault();
            copySelectionRef.current();
            terminal.clearSelection();
            return false;
          }
          // No selection - let xterm handle it (sends SIGINT)
          return true;
        }

        // Ctrl+V / Cmd+V or Ctrl+Shift+V / Cmd+Shift+V - Paste
        if (modKey && !otherModKey && !event.altKey && code === 'KeyV') {
          event.preventDefault();
          pasteFromClipboardRef.current();
          return false;
        }

        // Ctrl+A / Cmd+A - Select all
        if (modKey && !otherModKey && !event.shiftKey && !event.altKey && code === 'KeyA') {
          event.preventDefault();
          terminal.selectAll();
          return false;
        }

        // Ctrl+Shift+F / Cmd+Shift+F - Toggle search
        if (modKey && !otherModKey && event.shiftKey && !event.altKey && code === 'KeyF') {
          event.preventDefault();
          showSearchRef.current = !showSearchRef.current;
          setShowSearch(showSearchRef.current);
          return false;
        }

        // Let xterm handle all other keys
        return true;
      });
    };

    initTerminal();

    // Cleanup
    return () => {
      mounted = false;

      // Dispose focus handler to prevent memory leak
      if (focusHandlerRef.current) {
        focusHandlerRef.current.dispose();
        focusHandlerRef.current = null;
      }

      // Dispose link provider to prevent memory leak
      if (linkProviderRef.current) {
        linkProviderRef.current.dispose();
        linkProviderRef.current = null;
      }

      // Clear resize debounce timer
      if (resizeDebounceRef.current) {
        clearTimeout(resizeDebounceRef.current);
        resizeDebounceRef.current = null;
      }

      // Clear search decorations before disposing to prevent visual artifacts
      if (searchAddonRef.current) {
        searchAddonRef.current.clearDecorations();
        searchAddonRef.current = null;
      }

      if (xtermRef.current) {
        xtermRef.current.dispose();
        xtermRef.current = null;
      }
      fitAddonRef.current = null;
      setIsTerminalReady(false);
    };
  }, []); // No dependencies - only run once on mount

  // Connect WebSocket - wait for terminal to be ready
  useEffect(() => {
    if (!isTerminalReady || !sessionId) return;
    const terminal = xtermRef.current;
    if (!terminal) return;

    const connect = () => {
      // Build WebSocket URL with token
      let url = `${wsUrl}/api/terminal/ws?sessionId=${sessionId}`;
      if (authToken) {
        url += `&token=${encodeURIComponent(authToken)}`;
      }

      const ws = new WebSocket(url);
      wsRef.current = ws;

      ws.onopen = () => {
        console.log(`[Terminal] WebSocket connected for session ${sessionId}`);

        setConnectionStatus('connected');
        reconnectAttemptsRef.current = 0;

        // Start heartbeat to keep connection alive (prevents proxy/load balancer timeouts)
        if (heartbeatIntervalRef.current) {
          clearInterval(heartbeatIntervalRef.current);
        }
        heartbeatIntervalRef.current = setInterval(() => {
          if (ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: 'ping' }));
          }
        }, 30000); // Ping every 30 seconds
      };

      ws.onmessage = (event) => {
        try {
          const msg = JSON.parse(event.data);
          switch (msg.type) {
            case 'data':
              terminal.write(msg.data);
              break;
            case 'scrollback':
              // Only process scrollback if there's actual data
              // Don't clear if empty - prevents blank terminal issue
              if (msg.data && msg.data.length > 0) {
                // Clear any stale search decorations before restoring content
                searchAddonRef.current?.clearDecorations();
                // Use reset() which is more reliable than clear() or escape sequences
                terminal.reset();
                terminal.write(msg.data);
                // Mark as already initialized - don't run initial command for restored sessions
                hasRunInitialCommandRef.current = true;
              }
              break;
            case 'connected': {
              console.log(`[Terminal] Session connected: ${msg.shell} in ${msg.cwd}`);
              // Detect shell type from path
              const shellPath = (msg.shell || '').toLowerCase();
              // Windows shells use backslash paths and include powershell/pwsh/cmd
              const isWindowsShell =
                shellPath.includes('\\') ||
                shellPath.includes('powershell') ||
                shellPath.includes('pwsh') ||
                shellPath.includes('cmd.exe');
              const isPowerShell = shellPath.includes('powershell') || shellPath.includes('pwsh');

              if (msg.shell) {
                // Extract shell name from path (e.g., "/bin/bash" -> "bash", "C:\...\powershell.exe" -> "powershell.exe")
                const name = msg.shell.split(/[/\\]/).pop() || msg.shell;
                setShellName(name);
              }
              // Run initial command if specified and not already run
              // Only run for new terminals (no scrollback received)
              if (
                runCommandOnConnect &&
                !hasRunInitialCommandRef.current &&
                ws.readyState === WebSocket.OPEN
              ) {
                hasRunInitialCommandRef.current = true;
                // Use appropriate line ending for the shell type
                // Windows shells (PowerShell, cmd) expect \r\n, Unix shells expect \n
                const lineEnding = isWindowsShell ? '\r\n' : '\n';
                // PowerShell takes longer to initialize (profile loading, etc.)
                // Use 500ms for PowerShell, 100ms for other shells
                const delay = isPowerShell ? 500 : 100;

                setTimeout(() => {
                  if (ws.readyState === WebSocket.OPEN) {
                    ws.send(
                      JSON.stringify({ type: 'input', data: runCommandOnConnect + lineEnding })
                    );
                    onCommandRan?.();
                  }
                }, delay);
              }
              break;
            }
            case 'exit':
              terminal.write(`\r\n\x1b[33m[Process exited with code ${msg.exitCode}]\x1b[0m\r\n`);
              setProcessExitCode(msg.exitCode);
              break;
            case 'pong':
              // Heartbeat response
              break;
          }
        } catch (err) {
          console.error('[Terminal] Message parse error:', err);
        }
      };

      ws.onclose = (event) => {
        console.log(
          `[Terminal] WebSocket closed for session ${sessionId}:`,
          event.code,
          event.reason
        );
        wsRef.current = null;

        // Clear heartbeat interval
        if (heartbeatIntervalRef.current) {
          clearInterval(heartbeatIntervalRef.current);
          heartbeatIntervalRef.current = null;
        }

        if (event.code === 4001) {
          setConnectionStatus('auth_failed');
          toast.error('Terminal authentication expired', {
            description: 'Please unlock the terminal again to reconnect.',
            duration: 5000,
          });
          return;
        }

        // Don't reconnect if closed normally
        if (event.code === 1000 || event.code === 4003) {
          setConnectionStatus('disconnected');
          return;
        }

        if (event.code === 4004) {
          setConnectionStatus('disconnected');
          // Notify parent that this session is no longer valid on the server
          // This allows automatic cleanup of stale sessions (e.g., after server restart)
          if (onSessionInvalidRef.current) {
            onSessionInvalidRef.current();
            toast.info('Terminal session expired', {
              description:
                'The session was automatically removed. Create a new terminal to continue.',
              duration: 5000,
            });
          } else {
            toast.error('Terminal session not found', {
              description: 'The session may have expired. Please create a new terminal.',
              duration: 5000,
            });
          }
          return;
        }

        reconnectAttemptsRef.current++;

        if (reconnectAttemptsRef.current > MAX_RECONNECT_ATTEMPTS) {
          setConnectionStatus('disconnected');
          toast.error('Terminal disconnected', {
            description: 'Maximum reconnection attempts reached. Click to retry.',
            action: {
              label: 'Retry',
              onClick: () => {
                reconnectAttemptsRef.current = 0;
                setConnectionStatus('reconnecting');
                connect();
              },
            },
            duration: 10000,
          });
          return;
        }

        // Exponential backoff: 1s, 2s, 4s, 8s, 16s
        const delay = INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttemptsRef.current - 1);
        setConnectionStatus('reconnecting');

        // Attempt reconnect after exponential delay
        reconnectTimeoutRef.current = setTimeout(() => {
          if (xtermRef.current) {
            console.log(
              `[Terminal] Attempting reconnect for session ${sessionId} (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
            );
            connect();
          }
        }, delay);
      };

      ws.onerror = (error) => {
        console.error(`[Terminal] WebSocket error for session ${sessionId}:`, error);
      };
    };

    connect();

    // Handle terminal input
    const dataHandler = terminal.onData((data) => {
      if (wsRef.current?.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify({ type: 'input', data }));
      }
    });

    // Cleanup
    return () => {
      dataHandler.dispose();
      if (heartbeatIntervalRef.current) {
        clearInterval(heartbeatIntervalRef.current);
        heartbeatIntervalRef.current = null;
      }
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      if (wsRef.current) {
        wsRef.current.close();
        wsRef.current = null;
      }
    };
  }, [sessionId, authToken, wsUrl, isTerminalReady]);

  // Handle resize with debouncing
  const handleResize = useCallback(() => {
    // Clear any pending resize
    if (resizeDebounceRef.current) {
      clearTimeout(resizeDebounceRef.current);
    }

    // Debounce resize operations to prevent race conditions
    resizeDebounceRef.current = setTimeout(() => {
      if (!fitAddonRef.current || !xtermRef.current || !terminalRef.current) return;

      const container = terminalRef.current;
      const rect = container.getBoundingClientRect();

      // Only skip if container has no size at all
      if (rect.width <= 0 || rect.height <= 0) {
        return;
      }

      try {
        fitAddonRef.current.fit();
        const { cols, rows } = xtermRef.current;

        // Send resize to server
        if (wsRef.current?.readyState === WebSocket.OPEN) {
          wsRef.current.send(JSON.stringify({ type: 'resize', cols, rows }));
        }
      } catch (err) {
        console.error('[Terminal] Resize error:', err);
      }
    }, RESIZE_DEBOUNCE_MS);
  }, []);

  // Resize observer
  useEffect(() => {
    const container = terminalRef.current;
    if (!container) return;

    const resizeObserver = new ResizeObserver(() => {
      handleResize();
    });

    resizeObserver.observe(container);

    // Also handle window resize
    window.addEventListener('resize', handleResize);

    return () => {
      resizeObserver.disconnect();
      window.removeEventListener('resize', handleResize);
    };
  }, [handleResize]);

  useEffect(() => {
    if (xtermRef.current && isTerminalReady) {
      xtermRef.current.options.fontSize = fontSize;
      fitAddonRef.current?.fit();
    }
  }, [fontSize, isTerminalReady]);

  useEffect(() => {
    if (xtermRef.current && isTerminalReady) {
      xtermRef.current.options.fontFamily = fontFamily;
      fitAddonRef.current?.fit();
    }
  }, [fontFamily, isTerminalReady]);

  useEffect(() => {
    if (xtermRef.current && isTerminalReady) {
      xtermRef.current.options.lineHeight = lineHeight;
      fitAddonRef.current?.fit();
    }
  }, [lineHeight, isTerminalReady]);

  // Focus terminal when becoming active or when terminal becomes ready
  useEffect(() => {
    if (isActive && isTerminalReady && xtermRef.current && !showSearch) {
      xtermRef.current.focus();
    }
  }, [isActive, isTerminalReady, showSearch]);

  // Focus search input when search bar opens
  useEffect(() => {
    if (showSearch && searchInputRef.current) {
      searchInputRef.current.focus();
      searchInputRef.current.select();
    }
  }, [showSearch]);

  // Monitor scroll position to show/hide "Jump to bottom" button
  useEffect(() => {
    if (!isTerminalReady || !terminalRef.current) return;

    // xterm creates a viewport element with class .xterm-viewport
    const viewport = terminalRef.current.querySelector('.xterm-viewport') as HTMLElement | null;
    if (!viewport) return;

    const checkScrollPosition = () => {
      // Check if scrolled to bottom (with small tolerance for rounding)
      const scrollTop = viewport.scrollTop;
      const scrollHeight = viewport.scrollHeight;
      const clientHeight = viewport.clientHeight;
      const isBottom = scrollHeight - scrollTop - clientHeight <= 5;
      setIsAtBottom(isBottom);
    };

    // Initial check
    checkScrollPosition();

    // Listen for scroll events
    viewport.addEventListener('scroll', checkScrollPosition, { passive: true });

    return () => {
      viewport.removeEventListener('scroll', checkScrollPosition);
    };
  }, [isTerminalReady]);

  // Update terminal font size when it changes
  useEffect(() => {
    if (xtermRef.current && isTerminalReady) {
      xtermRef.current.options.fontSize = fontSize;
      // Refit after font size change
      if (fitAddonRef.current && terminalRef.current) {
        const rect = terminalRef.current.getBoundingClientRect();
        // Only fit if container has any size
        if (rect.width > 0 && rect.height > 0) {
          fitAddonRef.current.fit();
          // Notify server of new dimensions
          const { cols, rows } = xtermRef.current;
          if (wsRef.current?.readyState === WebSocket.OPEN) {
            wsRef.current.send(JSON.stringify({ type: 'resize', cols, rows }));
          }
        }
      }
    }
  }, [fontSize, isTerminalReady]);

  // Update terminal theme when app theme changes (including system preference)
  useEffect(() => {
    if (xtermRef.current && isTerminalReady) {
      // Clear any search decorations first to prevent stale color artifacts
      searchAddonRef.current?.clearDecorations();
      const terminalTheme = getTerminalTheme(resolvedTheme);
      xtermRef.current.options.theme = terminalTheme;
    }
  }, [resolvedTheme, isTerminalReady]);

  // Handle keyboard shortcuts for zoom (Ctrl+Plus, Ctrl+Minus, Ctrl+0)
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      // Only handle if Ctrl (or Cmd on Mac) is pressed
      if (!e.ctrlKey && !e.metaKey) return;

      // Ctrl/Cmd + Plus (Equal key or NumpadAdd for international keyboard support)
      if (e.code === 'Equal' || e.code === 'NumpadAdd') {
        e.preventDefault();
        e.stopPropagation();
        zoomIn();
        return;
      }

      // Ctrl/Cmd + Minus (Minus key or NumpadSubtract)
      if (e.code === 'Minus' || e.code === 'NumpadSubtract') {
        e.preventDefault();
        e.stopPropagation();
        zoomOut();
        return;
      }

      // Ctrl/Cmd + 0 to reset (Digit0 or Numpad0)
      if (e.code === 'Digit0' || e.code === 'Numpad0') {
        e.preventDefault();
        e.stopPropagation();
        resetZoom();
        return;
      }
    };

    container.addEventListener('keydown', handleKeyDown);
    return () => container.removeEventListener('keydown', handleKeyDown);
  }, [zoomIn, zoomOut, resetZoom]);

  // Handle mouse wheel zoom (Ctrl+Wheel)
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleWheel = (e: WheelEvent) => {
      // Only zoom if Ctrl (or Cmd on Mac) is pressed
      if (!e.ctrlKey && !e.metaKey) return;

      e.preventDefault();
      e.stopPropagation();

      if (e.deltaY < 0) {
        // Scroll up = zoom in
        zoomIn();
      } else if (e.deltaY > 0) {
        // Scroll down = zoom out
        zoomOut();
      }
    };

    // Use passive: false to allow preventDefault
    container.addEventListener('wheel', handleWheel, { passive: false });
    return () => container.removeEventListener('wheel', handleWheel);
  }, [zoomIn, zoomOut]);

  // Context menu actions for keyboard navigation
  const menuActions = ['copy', 'paste', 'selectAll', 'clear'] as const;

  // Keep ref in sync with state for use in event handlers
  useEffect(() => {
    focusedMenuIndexRef.current = focusedMenuIndex;
  }, [focusedMenuIndex]);

  // Close context menu on click outside or scroll, handle keyboard navigation
  useEffect(() => {
    if (!contextMenu) return;

    // Reset focus index and focus menu when opened
    setFocusedMenuIndex(0);
    focusedMenuIndexRef.current = 0;
    requestAnimationFrame(() => {
      const firstButton =
        contextMenuRef.current?.querySelector<HTMLButtonElement>('[role="menuitem"]');
      firstButton?.focus();
    });

    const handleClick = () => closeContextMenu();
    const handleScroll = () => closeContextMenu();
    const handleKeyDown = (e: KeyboardEvent) => {
      const updateFocusIndex = (newIndex: number) => {
        focusedMenuIndexRef.current = newIndex;
        setFocusedMenuIndex(newIndex);
      };

      switch (e.key) {
        case 'Escape':
          e.preventDefault();
          e.stopPropagation();
          closeContextMenu();
          xtermRef.current?.focus();
          break;
        case 'ArrowDown':
          e.preventDefault();
          e.stopPropagation();
          updateFocusIndex((focusedMenuIndexRef.current + 1) % menuActions.length);
          break;
        case 'ArrowUp':
          e.preventDefault();
          e.stopPropagation();
          updateFocusIndex(
            (focusedMenuIndexRef.current - 1 + menuActions.length) % menuActions.length
          );
          break;
        case 'Enter':
        case ' ':
          e.preventDefault();
          e.stopPropagation();
          handleContextMenuAction(menuActions[focusedMenuIndexRef.current]);
          break;
        case 'Tab':
          e.preventDefault();
          e.stopPropagation();
          closeContextMenu();
          break;
      }
    };

    document.addEventListener('click', handleClick);
    document.addEventListener('scroll', handleScroll, true);
    document.addEventListener('keydown', handleKeyDown);

    return () => {
      document.removeEventListener('click', handleClick);
      document.removeEventListener('scroll', handleScroll, true);
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [contextMenu, closeContextMenu, handleContextMenuAction]);

  // Focus the correct menu item when navigation changes
  useEffect(() => {
    if (!contextMenu || !contextMenuRef.current) return;
    const buttons = contextMenuRef.current.querySelectorAll<HTMLButtonElement>('[role="menuitem"]');
    buttons[focusedMenuIndex]?.focus();
  }, [focusedMenuIndex, contextMenu]);

  // Handle right-click context menu with boundary checking
  const handleContextMenu = useCallback((e: React.MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();

    // Menu dimensions (approximate)
    const menuWidth = 160;
    const menuHeight = 152; // 4 items + separator + padding
    const padding = 8;

    // Calculate position with boundary checks
    let x = e.clientX;
    let y = e.clientY;

    // Check right edge
    if (x + menuWidth + padding > window.innerWidth) {
      x = window.innerWidth - menuWidth - padding;
    }

    // Check bottom edge
    if (y + menuHeight + padding > window.innerHeight) {
      y = window.innerHeight - menuHeight - padding;
    }

    // Ensure not negative
    x = Math.max(padding, x);
    y = Math.max(padding, y);

    setContextMenu({ x, y });
  }, []);

  // Convert file to base64
  const fileToBase64 = useCallback((file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        if (typeof reader.result === 'string') {
          resolve(reader.result);
        } else {
          reject(new Error('Failed to read file as base64'));
        }
      };
      reader.onerror = () => reject(new Error('Failed to read file'));
      reader.readAsDataURL(file);
    });
  }, []);

  // Save image to temp folder via Electron API
  const saveImageToTemp = useCallback(
    async (base64Data: string, filename: string, mimeType: string): Promise<string | null> => {
      try {
        const api = getElectronAPI();
        if (!api.saveImageToTemp) {
          // Fallback path when Electron API is not available (browser mode)
          console.warn('[Terminal] saveImageToTemp not available, returning fallback path');
          return `.automaker/images/${Date.now()}_${filename}`;
        }

        const projectPath = currentProject?.path;
        const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath);
        if (result.success && result.path) {
          return result.path;
        }
        console.error('[Terminal] Failed to save image:', result.error);
        return null;
      } catch (error) {
        console.error('[Terminal] Error saving image:', error);
        return null;
      }
    },
    [currentProject?.path]
  );

  // Check if drag event contains image files
  const hasImageFiles = useCallback((e: React.DragEvent): boolean => {
    const types = e.dataTransfer.types;
    const items = e.dataTransfer.items;

    // Check if Files type is present
    if (!types.includes('Files')) return false;

    // Check if any item is an image
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.kind === 'file' && ACCEPTED_IMAGE_TYPES.includes(item.type)) {
        return true;
      }
    }
    return false;
  }, []);

  // Handle image drag over terminal
  const handleImageDragOver = useCallback(
    (e: React.DragEvent) => {
      // Only handle if contains image files
      if (!hasImageFiles(e)) return;

      e.preventDefault();
      e.stopPropagation();
      setIsImageDragOver(true);
    },
    [hasImageFiles]
  );

  // Handle image drag leave
  const handleImageDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();

    // Only reset if leaving the actual container (not just moving to a child)
    const relatedTarget = e.relatedTarget as HTMLElement | null;
    if (relatedTarget && terminalRef.current?.contains(relatedTarget)) {
      return;
    }

    setIsImageDragOver(false);
  }, []);

  // Handle image drop on terminal
  const handleImageDrop = useCallback(
    async (e: React.DragEvent) => {
      e.preventDefault();
      e.stopPropagation();
      setIsImageDragOver(false);

      if (isProcessingImage) return;

      const files = e.dataTransfer.files;
      if (!files.length) return;

      // Filter to only image files
      const imageFiles: File[] = [];
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        if (ACCEPTED_IMAGE_TYPES.includes(file.type)) {
          if (file.size > MAX_IMAGE_SIZE) {
            toast.error(`Image too large: ${file.name}`, {
              description: 'Maximum size is 10MB',
            });
            continue;
          }
          imageFiles.push(file);
        }
      }

      if (imageFiles.length === 0) {
        toast.error('No valid images found', {
          description: 'Drop PNG, JPG, GIF, or WebP images',
        });
        return;
      }

      setIsProcessingImage(true);
      const savedPaths: string[] = [];

      for (const file of imageFiles) {
        try {
          const base64 = await fileToBase64(file);
          const savedPath = await saveImageToTemp(base64, file.name, file.type);
          if (savedPath) {
            savedPaths.push(savedPath);
          } else {
            toast.error(`Failed to save: ${file.name}`);
          }
        } catch (error) {
          console.error('[Terminal] Error processing image:', error);
          toast.error(`Error processing: ${file.name}`);
        }
      }

      setIsProcessingImage(false);

      if (savedPaths.length === 0) return;

      // Send image paths to terminal as input
      // Format: space-separated paths, each wrapped in quotes if containing spaces
      const formattedPaths = savedPaths.map((p) => (p.includes(' ') ? `"${p}"` : p)).join(' ');

      if (wsRef.current?.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify({ type: 'input', data: formattedPaths }));
        toast.success(
          savedPaths.length === 1
            ? 'Image path inserted'
            : `${savedPaths.length} image paths inserted`,
          { description: 'Press Enter to send' }
        );
      } else {
        toast.error('Terminal not connected');
      }
    },
    [isProcessingImage, fileToBase64, saveImageToTemp]
  );

  // Combine refs for the container
  const setRefs = useCallback(
    (node: HTMLDivElement | null) => {
      containerRef.current = node;
      setDropRef(node);
    },
    [setDropRef]
  );

  // Get current terminal theme for xterm styling (resolved for system preference)
  const currentTerminalTheme = getTerminalTheme(resolvedTheme);

  return (
    <div
      ref={setRefs}
      className={cn(
        'flex flex-col h-full relative',
        isActive && 'ring-1 ring-brand-500 ring-inset',
        // Visual feedback when dragging this terminal
        isDragging && 'opacity-50',
        // Visual feedback when hovering over as drop target
        isOver && isDropTarget && 'ring-2 ring-green-500 ring-inset'
      )}
      onClick={onFocus}
      onKeyDownCapture={handleContainerKeyDownCapture}
      tabIndex={0}
      data-terminal-container="true"
    >
      {/* Drop indicator overlay */}
      {isOver && isDropTarget && (
        <div className="absolute inset-0 bg-green-500/10 z-10 pointer-events-none flex items-center justify-center">
          <div className="px-3 py-2 bg-green-500/90 rounded-md text-white text-sm font-medium">
            Drop to swap
          </div>
        </div>
      )}

      {/* Image drop overlay */}
      {isImageDragOver && (
        <div className="absolute inset-0 bg-blue-500/20 z-20 pointer-events-none flex items-center justify-center border-2 border-dashed border-blue-400 rounded">
          <div className="flex flex-col items-center gap-2 px-4 py-3 bg-blue-500/90 rounded-md text-white">
            {isProcessingImage ? (
              <>
                <Loader2 className="h-6 w-6 animate-spin" />
                <span className="text-sm font-medium">Processing...</span>
              </>
            ) : (
              <>
                <ImageIcon className="h-6 w-6" />
                <span className="text-sm font-medium">Drop image for Claude Code</span>
              </>
            )}
          </div>
        </div>
      )}

      {/* Header bar with drag handle - uses app theme CSS variables */}
      <div className="flex items-center h-7 px-1 shrink-0 bg-card border-b border-border">
        {/* Drag handle */}
        <button
          ref={setDragRef}
          {...dragAttributes}
          {...dragListeners}
          className={cn(
            'p-1 rounded cursor-grab active:cursor-grabbing mr-1 transition-colors text-muted-foreground hover:text-foreground hover:bg-accent',
            isDragging && 'cursor-grabbing'
          )}
          title="Drag to swap terminals"
        >
          <GripHorizontal className="h-3 w-3" />
        </button>

        {/* Terminal icon and label */}
        <div className="flex items-center gap-1.5 flex-1 min-w-0">
          <Terminal className="h-3 w-3 shrink-0 text-muted-foreground" />
          <span className="text-xs truncate text-foreground">{shellName}</span>
          {/* Font size indicator - only show when not default */}
          {fontSize !== DEFAULT_FONT_SIZE && (
            <button
              onClick={(e) => {
                e.stopPropagation();
                resetZoom();
              }}
              className="text-[10px] px-1 rounded transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
              title="Click to reset zoom (Ctrl+0)"
            >
              {fontSize}px
            </button>
          )}
          {connectionStatus === 'reconnecting' && (
            <span className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-500 flex items-center gap-1">
              <Loader2 className="h-2.5 w-2.5 animate-spin" />
              Reconnecting...
            </span>
          )}
          {connectionStatus === 'disconnected' && (
            <span className="text-[10px] px-1.5 py-0.5 rounded bg-destructive/20 text-destructive">
              Disconnected
            </span>
          )}
          {connectionStatus === 'auth_failed' && (
            <span className="text-[10px] px-1.5 py-0.5 rounded bg-destructive/20 text-destructive">
              Auth Failed
            </span>
          )}
          {processExitCode !== null && (
            <span
              className={cn(
                'text-[10px] px-1.5 py-0.5 rounded flex items-center gap-1',
                processExitCode === 0
                  ? 'bg-green-500/20 text-green-500'
                  : 'bg-yellow-500/20 text-yellow-500'
              )}
            >
              Exited ({processExitCode})
            </span>
          )}
        </div>

        {/* Zoom and action buttons */}
        <div className="flex items-center gap-0.5">
          {/* Zoom controls */}
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground"
            onClick={(e) => {
              e.stopPropagation();
              zoomOut();
            }}
            title="Zoom Out (Ctrl+-)"
            disabled={fontSize <= MIN_FONT_SIZE}
          >
            <ZoomOut className="h-3 w-3" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground"
            onClick={(e) => {
              e.stopPropagation();
              zoomIn();
            }}
            title="Zoom In (Ctrl++)"
            disabled={fontSize >= MAX_FONT_SIZE}
          >
            <ZoomIn className="h-3 w-3" />
          </Button>

          {/* Settings popover */}
          <Popover>
            <PopoverTrigger asChild>
              <Button
                variant="ghost"
                size="icon"
                className="h-5 w-5 text-muted-foreground hover:text-foreground"
                onClick={(e) => e.stopPropagation()}
                title="Terminal Settings"
              >
                <Settings className="h-3 w-3" />
              </Button>
            </PopoverTrigger>
            <PopoverContent
              className="w-64 p-3"
              align="end"
              side="bottom"
              onClick={(e) => e.stopPropagation()}
            >
              <div className="space-y-4">
                <div className="space-y-2">
                  <div className="flex items-center justify-between">
                    <Label className="text-xs font-medium">Font Size</Label>
                    <span className="text-xs text-muted-foreground">{fontSize}px</span>
                  </div>
                  <div className="flex items-center gap-2">
                    <Slider
                      value={[fontSize]}
                      min={MIN_FONT_SIZE}
                      max={MAX_FONT_SIZE}
                      step={1}
                      onValueChange={([value]) => onFontSizeChange(value)}
                      className="flex-1"
                    />
                    <Button
                      variant="ghost"
                      size="icon"
                      className="h-6 w-6 shrink-0"
                      onClick={() => resetZoom()}
                      disabled={fontSize === DEFAULT_FONT_SIZE}
                      title="Reset to default"
                    >
                      <RotateCcw className="h-3 w-3" />
                    </Button>
                  </div>
                </div>

                <div className="space-y-2">
                  <Label className="text-xs font-medium">Run on New Terminal</Label>
                  <Input
                    value={defaultRunScript}
                    onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
                    placeholder="e.g., claude"
                    className="h-7 text-xs"
                  />
                  <p className="text-[10px] text-muted-foreground">
                    Command to run when creating a new terminal
                  </p>
                </div>

                <div className="space-y-2">
                  <Label className="text-xs font-medium">Font Family</Label>
                  <select
                    value={fontFamily}
                    onChange={(e) => {
                      setTerminalFontFamily(e.target.value);
                      toast.info('Font family changed', {
                        description: 'Restart terminal for changes to take effect',
                      });
                    }}
                    className="w-full h-7 text-xs bg-background border border-input rounded-md px-2"
                  >
                    {TERMINAL_FONT_OPTIONS.map((font) => (
                      <option key={font.value} value={font.value}>
                        {font.label}
                      </option>
                    ))}
                  </select>
                </div>

                <div className="space-y-2">
                  <div className="flex items-center justify-between">
                    <Label className="text-xs font-medium">Scrollback</Label>
                    <span className="text-xs text-muted-foreground">
                      {(scrollbackLines / 1000).toFixed(0)}k lines
                    </span>
                  </div>
                  <Slider
                    value={[scrollbackLines]}
                    min={1000}
                    max={100000}
                    step={1000}
                    onValueChange={([value]) => {
                      setTerminalScrollbackLines(value);
                    }}
                    onValueCommit={() => {
                      toast.info('Scrollback changed', {
                        description: 'Restart terminal for changes to take effect',
                      });
                    }}
                    className="flex-1"
                  />
                </div>

                <div className="space-y-2">
                  <div className="flex items-center justify-between">
                    <Label className="text-xs font-medium">Line Height</Label>
                    <span className="text-xs text-muted-foreground">{lineHeight.toFixed(1)}</span>
                  </div>
                  <Slider
                    value={[lineHeight]}
                    min={1.0}
                    max={2.0}
                    step={0.1}
                    onValueChange={([value]) => {
                      setTerminalLineHeight(value);
                    }}
                    onValueCommit={() => {
                      toast.info('Line height changed', {
                        description: 'Restart terminal for changes to take effect',
                      });
                    }}
                    className="flex-1"
                  />
                </div>

                <div className="flex items-center justify-between">
                  <div className="space-y-0.5">
                    <Label className="text-xs font-medium">Screen Reader</Label>
                    <p className="text-[10px] text-muted-foreground">Enable accessibility mode</p>
                  </div>
                  <Switch
                    checked={screenReaderMode}
                    onCheckedChange={(checked) => {
                      setTerminalScreenReaderMode(checked);
                      toast.info(checked ? 'Screen reader enabled' : 'Screen reader disabled', {
                        description: 'Restart terminal for changes to take effect',
                      });
                    }}
                  />
                </div>

                <div className="text-[10px] text-muted-foreground border-t pt-2">
                  <p>Zoom: Ctrl++ / Ctrl+- / Ctrl+0</p>
                  <p>Or use Ctrl+scroll wheel</p>
                </div>
              </div>
            </PopoverContent>
          </Popover>

          <div className="w-px h-3 mx-0.5 bg-border" />

          {/* Split/close buttons */}
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground"
            onClick={(e) => {
              e.stopPropagation();
              onSplitHorizontal();
            }}
            title="Split Right (Alt+D)"
          >
            <SplitSquareHorizontal className="h-3 w-3" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground"
            onClick={(e) => {
              e.stopPropagation();
              onSplitVertical();
            }}
            title="Split Down (Alt+S)"
          >
            <SplitSquareVertical className="h-3 w-3" />
          </Button>
          {onToggleMaximize && (
            <Button
              variant="ghost"
              size="icon"
              className="h-5 w-5 text-muted-foreground hover:text-foreground"
              onClick={(e) => {
                e.stopPropagation();
                onToggleMaximize();
              }}
              title={isMaximized ? 'Restore' : 'Maximize'}
            >
              {isMaximized ? <Minimize2 className="h-3 w-3" /> : <Maximize2 className="h-3 w-3" />}
            </Button>
          )}
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-destructive"
            onClick={(e) => {
              e.stopPropagation();
              onClose();
            }}
            title="Close Terminal (Alt+W)"
          >
            <X className="h-3 w-3" />
          </Button>
        </div>
      </div>

      {/* Search bar */}
      {showSearch && (
        <div className="flex items-center gap-1 px-2 py-1 bg-card border-b border-border shrink-0">
          <Search className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
          <input
            ref={searchInputRef}
            type="text"
            value={searchQuery}
            onChange={(e) => {
              setSearchQuery(e.target.value);
              // Auto-search as user types
              if (searchAddonRef.current && e.target.value) {
                searchAddonRef.current.findNext(e.target.value, searchOptions);
              } else if (searchAddonRef.current) {
                searchAddonRef.current.clearDecorations();
              }
            }}
            onKeyDown={(e) => {
              e.stopPropagation();
              if (e.key === 'Enter') {
                e.preventDefault();
                if (e.shiftKey) {
                  searchPrevious();
                } else {
                  searchNext();
                }
              } else if (e.key === 'Escape') {
                e.preventDefault();
                closeSearch();
              }
            }}
            placeholder="Search..."
            className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none min-w-0"
          />
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground shrink-0"
            onClick={searchPrevious}
            disabled={!searchQuery}
            title="Previous Match (Shift+Enter)"
          >
            <ChevronUp className="h-3.5 w-3.5" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground shrink-0"
            onClick={searchNext}
            disabled={!searchQuery}
            title="Next Match (Enter)"
          >
            <ChevronDown className="h-3.5 w-3.5" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="h-5 w-5 text-muted-foreground hover:text-foreground shrink-0"
            onClick={closeSearch}
            title="Close (Escape)"
          >
            <X className="h-3.5 w-3.5" />
          </Button>
        </div>
      )}

      {/* Terminal container - uses terminal theme */}
      <div
        ref={terminalRef}
        className="flex-1 overflow-hidden relative"
        style={{ backgroundColor: currentTerminalTheme.background }}
        onContextMenu={handleContextMenu}
        onDragOver={handleImageDragOver}
        onDragLeave={handleImageDragLeave}
        onDrop={handleImageDrop}
      />

      {/* Jump to bottom button - shown when scrolled up */}
      {!isAtBottom && (
        <Button
          variant="secondary"
          size="sm"
          className="absolute bottom-3 right-3 h-7 px-2 gap-1 shadow-md z-10 opacity-90 hover:opacity-100 transition-opacity"
          onClick={(e) => {
            e.stopPropagation();
            scrollToBottom();
          }}
          title="Jump to bottom"
        >
          <ArrowDown className="h-3.5 w-3.5" />
          <span className="text-xs">Bottom</span>
        </Button>
      )}

      {/* Context menu */}
      {contextMenu && (
        <div
          ref={contextMenuRef}
          role="menu"
          aria-label="Terminal context menu"
          className="fixed z-50 min-w-[160px] rounded-md border border-border bg-popover p-1 shadow-md animate-in fade-in-0 zoom-in-95"
          style={{ left: contextMenu.x, top: contextMenu.y }}
          onClick={(e) => e.stopPropagation()}
        >
          <button
            role="menuitem"
            tabIndex={focusedMenuIndex === 0 ? 0 : -1}
            className={cn(
              'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-popover-foreground cursor-default outline-none',
              focusedMenuIndex === 0
                ? 'bg-accent text-accent-foreground'
                : 'hover:bg-accent hover:text-accent-foreground'
            )}
            onClick={() => handleContextMenuAction('copy')}
          >
            <Copy className="h-4 w-4" />
            <span className="flex-1 text-left">Copy</span>
            <span className="text-xs text-muted-foreground">{isMac ? '⌘C' : 'Ctrl+C'}</span>
          </button>
          <button
            role="menuitem"
            tabIndex={focusedMenuIndex === 1 ? 0 : -1}
            className={cn(
              'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-popover-foreground cursor-default outline-none',
              focusedMenuIndex === 1
                ? 'bg-accent text-accent-foreground'
                : 'hover:bg-accent hover:text-accent-foreground'
            )}
            onClick={() => handleContextMenuAction('paste')}
          >
            <ClipboardPaste className="h-4 w-4" />
            <span className="flex-1 text-left">Paste</span>
            <span className="text-xs text-muted-foreground">{isMac ? '⌘V' : 'Ctrl+V'}</span>
          </button>
          <div role="separator" className="my-1 h-px bg-border" />
          <button
            role="menuitem"
            tabIndex={focusedMenuIndex === 2 ? 0 : -1}
            className={cn(
              'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-popover-foreground cursor-default outline-none',
              focusedMenuIndex === 2
                ? 'bg-accent text-accent-foreground'
                : 'hover:bg-accent hover:text-accent-foreground'
            )}
            onClick={() => handleContextMenuAction('selectAll')}
          >
            <CheckSquare className="h-4 w-4" />
            <span className="flex-1 text-left">Select All</span>
            <span className="text-xs text-muted-foreground">{isMac ? '⌘A' : 'Ctrl+A'}</span>
          </button>
          <button
            role="menuitem"
            tabIndex={focusedMenuIndex === 3 ? 0 : -1}
            className={cn(
              'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-popover-foreground cursor-default outline-none',
              focusedMenuIndex === 3
                ? 'bg-accent text-accent-foreground'
                : 'hover:bg-accent hover:text-accent-foreground'
            )}
            onClick={() => handleContextMenuAction('clear')}
          >
            <Trash2 className="h-4 w-4" />
            <span className="flex-1 text-left">Clear</span>
          </button>
        </div>
      )}
    </div>
  );
}
