// Import styles (processed by Vite + TailwindCSS)
import './styles/main.css';

// Import types and dependencies
import type {
  HistoryItem,
  WindowData,
  Config,
  PasteResult,
  ImageResult,
  UserSettings,
  DirectoryInfo
} from './types';
import { EventHandler } from './event-handler';
import { HistorySearchManager } from './history-search';
import { AgentSkillManager } from './agent-skill-manager';
import { DomManager } from './dom-manager';
import { DraftManagerClient } from './draft-manager-client';
import { HistoryUIManager } from './history-ui-manager';
import { LifecycleManager } from './lifecycle-manager';
import { SimpleSnapshotManager } from './snapshot-manager';
import { MentionManager } from './mention-manager';
import { DirectoryDataHandler } from './directory-data-handler';
import { rendererLogger } from './utils/logger';
import { electronAPI } from './services/electron-api';

// Default display limit for history items
const DEFAULT_DISPLAY_LIMIT = 50;
// Number of items to load on each loadMore call
const LOAD_MORE_INCREMENT = 25;

// Export the renderer class for testing
export class PromptLineRenderer {
  private historyData: HistoryItem[] = [];
  private filteredHistoryData: HistoryItem[] = [];
  private totalMatchCount: number | undefined = undefined;
  private nonSearchDisplayLimit: number = DEFAULT_DISPLAY_LIMIT;
  private config: Config = {};
  private userSettings: UserSettings | null = null;
  private eventHandler: EventHandler | null = null;
  private searchManager: HistorySearchManager | null = null;
  private agentSkillManager: AgentSkillManager | null = null;
  private fileSearchManager: MentionManager | null = null;
  private directoryDataHandler: DirectoryDataHandler;
  private domManager: DomManager;
  private draftManager: DraftManagerClient;
  private historyUIManager: HistoryUIManager;
  private lifecycleManager: LifecycleManager;
  private snapshotManager: SimpleSnapshotManager;
  private defaultHintText: string = 'Multi-line text and Image supported'; // Default hint text
  // Queue for pending window-shown data to handle race condition with init()
  private pendingWindowData: WindowData | null = null;
  // Queue for pending directory-data-updated payloads that arrive before init()
  // finishes. Without this, the BG detection result silently no-ops because
  // domManager.initializeElements has not yet wired up #hintText.
  private pendingDirectoryData: DirectoryInfo | null = null;
  // Tracks the in-flight handleWindowShown promise so a directory-data-updated
  // event that arrives while window-shown is still awaiting its own internal
  // IPC roundtrips waits for it to settle. Otherwise the BG result lands first
  // and window-shown overwrites the hint with the cached/stale directory.
  private inflightWindowShown: Promise<void> | null = null;
  // Stored handler reference for cleanup
  private customSearchUpdateHandler: (() => void) | null = null;
  private commandErrorHandler: ((e: Event) => void) | null = null;
  private initCompleted: boolean = false;
  // Throttle timeout for mousemove events
  private mouseMoveThrottleTimeout: number | null = null;
  // Last search term that was rendered (for render skip optimization)
  private _lastRenderedSearchTerm: string = '';
  private lastCustomSearchChangeTimestamp: number = 0;

  constructor() {
    this.domManager = new DomManager();
    this.draftManager = new DraftManagerClient(
      electronAPI,
      () => this.domManager.getCurrentText(),
      () => this.domManager.getScrollTop()
    );
    this.snapshotManager = new SimpleSnapshotManager();
    this.historyUIManager = new HistoryUIManager(
      () => this.domManager.historyList,
      (text: string) => {
        this.domManager.setText(text);
        // Clear @path highlights when setting text from history
        this.fileSearchManager?.clearAtPaths();
      },
      () => this.domManager.focusTextarea(),
      () => this.searchManager,
      () => this.domManager.getCurrentText(),
      () => this.domManager.getCursorPosition(),
      (text: string, cursorPosition: number) => {
        this.snapshotManager.saveSnapshot(text, cursorPosition);
      },
      () => this.handleLoadMore()
    );
    this.lifecycleManager = new LifecycleManager(
      electronAPI,
      () => this.domManager.appNameEl,
      () => this.domManager.headerShortcutsEl,
      () => this.domManager.historyShortcutsEl,
      (name: string) => this.domManager.updateAppName(name),
      (text: string) => this.domManager.setText(text),
      () => this.domManager.focusTextarea(),
      (position: number) => this.domManager.setCursorPosition(position),
      () => this.domManager.selectAll(),
      (scrollTop: number) => this.domManager.setScrollTop(scrollTop)
    );
    this.directoryDataHandler = new DirectoryDataHandler({
      updateHintText: (text: string) => this.domManager.updateHintText(text),
      setDraggable: (enabled: boolean) => this.domManager.setDraggable(enabled),
      getMentionManager: () => this.fileSearchManager,
      handleLifecycleWindowShown: (data) => this.lifecycleManager.handleWindowShown(data),
      exitSearchMode: () => this.searchManager?.exitSearchMode(),
      resetHistoryScrollPosition: () => this.resetHistoryScrollPosition(),
      updateHistoryAndSettings: (data) => this.updateHistoryAndSettings(data),
      getDefaultHintText: () => this.defaultHintText,
      setDefaultHintText: (text: string) => { this.defaultHintText = text; }
    });
    // CRITICAL: Register IPC listeners BEFORE any async operations
    // to prevent race condition when window-shown event is sent
    this.setupIPCListeners();
    this.init();
  }

  private async init(): Promise<void> {
    try {
      this.domManager.initializeElements();
      this.config = await electronAPI.config.get('') as Config;
      this.draftManager.setConfig(this.config);

      this.setupEventHandler();
      this.setupSearchManager();
      this.setupAgentSkillManager();
      this.setupMentionManager();
      // Code search is now integrated into MentionManager
      // await this.setupCodeSearchManager();
      this.setupEventListeners();
      // Note: setupIPCListeners() is now called in constructor to prevent race condition

      // Mark initialization as complete
      this.initCompleted = true;
      console.debug('[PromptLineRenderer] Initialization completed');

      // Process any pending window-shown data that arrived before init completed
      if (this.pendingWindowData) {
        console.debug('[PromptLineRenderer] Processing pending window-shown data');
        await this.directoryDataHandler.handleWindowShown(this.pendingWindowData);
        this.pendingWindowData = null;
      }
      // Then process any pending background-detection update. Order matters:
      // window-shown sets the hint to the cached directory; the BG update
      // overwrites it with the freshly detected one.
      if (this.pendingDirectoryData) {
        console.debug('[PromptLineRenderer] Processing pending directory-data-updated');
        await this.directoryDataHandler.handleDirectoryDataUpdated(this.pendingDirectoryData);
        this.pendingDirectoryData = null;
      }
    } catch (error) {
      rendererLogger.error('Failed to initialize renderer:', error);
    }
  }

  private setupEventHandler(): void {
    this.eventHandler = new EventHandler({
      onTextPaste: this.handleTextPasteCallback.bind(this),
      onWindowHide: this.handleWindowHideCallback.bind(this),
      onTabKeyInsert: this.handleTabKeyCallback.bind(this),
      onShiftTabKeyPress: this.handleShiftTabKeyCallback.bind(this),
      onHistoryNavigation: this.navigateHistory.bind(this),
      onSearchToggle: this.handleSearchToggle.bind(this),
      onUndo: this.handleUndo.bind(this),
      onSaveDraftToHistory: this.handleSaveDraftToHistory.bind(this),
      onCustomSearchActivate: this.handleCustomSearchActivate.bind(this),
      onRunCommand: this.handleRunCommand.bind(this)
    });

    this.eventHandler.setTextarea(this.domManager.textarea);
    this.eventHandler.setDomManager(this.domManager);
    this.eventHandler.setupEventListeners();
  }

  private setupSearchManager(): void {
    this.searchManager = new HistorySearchManager({
      onSearchStateChange: this.handleSearchStateChange.bind(this)
    });

    this.searchManager.initializeElements();
    this.searchManager.setupEventListeners();

    // Setup scroll listener for infinite scroll
    this.historyUIManager.setupScrollListener();

    // Set SearchManager reference in EventHandler
    if (this.eventHandler) {
      this.eventHandler.setSearchManager(this.searchManager);
    }
  }

  private setupAgentSkillManager(): void {
    this.agentSkillManager = new AgentSkillManager({
      onSkillSelect: async (command: string) => {
        console.debug('Agent skill selected (Enter):', command);
        // Paste the selected command immediately
        if (command) {
          await this.handleTextPasteCallback(command);
        }
      },
      onSkillInsert: (command: string) => {
        console.debug('Agent skill inserted (Tab):', command);
        // Just insert into textarea for editing, don't paste
        // The command is already inserted by AgentSkillManager
      },
      onBeforeOpenFile: () => {
        // Suppress blur event to prevent window from closing when opening file
        this.eventHandler?.setSuppressBlurHide(true);
      },
      setDraggable: (enabled: boolean) => {
        // Enable/disable draggable state on header during file open
        this.domManager.setDraggable(enabled);
      },
      updateHintText: (text: string) => {
        this.domManager.updateHintText(text);
      },
      getDefaultHintText: () => this.defaultHintText
    });

    this.agentSkillManager.initializeElements();
    this.agentSkillManager.setupEventListeners();

    // Set AgentSkillManager reference in EventHandler
    if (this.eventHandler) {
      this.eventHandler.setAgentSkillManager(this.agentSkillManager);
    }

    // Pre-load skills
    this.agentSkillManager.loadSkills();
  }

  private setupMentionManager(): void {
    this.fileSearchManager = new MentionManager({
      onFileSelected: (_filePath: string) => {
        // File path is already inserted by MentionManager
        this.draftManager.saveDraftDebounced();
      },
      getTextContent: () => this.domManager.getCurrentText(),
      setTextContent: (text: string) => this.domManager.setText(text),
      getCursorPosition: () => this.domManager.getCursorPosition(),
      setCursorPosition: (position: number) => this.domManager.setCursorPosition(position),
      onBeforeOpenFile: () => {
        // Suppress blur event to prevent window from closing when opening file
        this.eventHandler?.setSuppressBlurHide(true);
      },
      updateHintText: (text: string) => {
        this.domManager.updateHintText(text);
      },
      getDefaultHintText: () => this.defaultHintText,
      setDraggable: (enabled: boolean) => {
        // Enable/disable draggable state on header during file open
        this.domManager.setDraggable(enabled);
      },
      replaceRangeWithUndo: (start: number, end: number, newText: string) => {
        // Replace text range with native Undo/Redo support
        this.domManager.replaceRangeWithUndo(start, end, newText);
      },
      insertTextAtCursor: (text: string) => {
        this.domManager.insertTextAtCursor(text);
      },
      getIsComposing: () => this.eventHandler?.getIsComposing() ?? false,
      showError: (message: string) => this.domManager.showError(message),
      getSkillSource: (skillName: string) => this.agentSkillManager?.getSkillSource(skillName),
      getSkillColor: (skillName: string) => this.agentSkillManager?.getSkillColor(skillName),
      getKnownSkillNames: () => this.agentSkillManager?.getKnownSkillNames() ?? [],
      getSkillTriggerPrefixes: () => this.agentSkillManager?.getTriggerPrefixes() ?? ['/']
    });

    this.fileSearchManager.initializeElements();
    this.fileSearchManager.setupEventListeners();

    // Set MentionManager reference in EventHandler
    if (this.eventHandler) {
      this.eventHandler.setMentionManager(this.fileSearchManager);
    }
  }

  private setupEventListeners(): void {
    if (!this.domManager.textarea) return;

    this.domManager.textarea.addEventListener('input', () => {
      this.domManager.updateCharCount();
      this.draftManager.saveDraftDebounced();
      this.historyUIManager.clearHistorySelection();

      // 編集開始時にスナップショットをクリア
      if (this.snapshotManager.hasSnapshot()) {
        this.snapshotManager.clearSnapshot();
        console.debug('Snapshot cleared on text edit');
      }
    });

    this.domManager.textarea.addEventListener('keydown', (e) => {
      this.handleKeyDown(e);
    });

    document.addEventListener('contextmenu', (e) => {
      e.preventDefault();
    });

    this.domManager.textarea.addEventListener('keypress', (e) => {
      if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) {
        e.stopPropagation();
      }
    });

    // Add mouse event listeners to disable keyboard navigation mode on mouse interaction
    document.addEventListener('mousemove', () => {
      if (this.mouseMoveThrottleTimeout) return;

      this.mouseMoveThrottleTimeout = window.setTimeout(() => {
        this.mouseMoveThrottleTimeout = null;
        this.historyUIManager.clearHistorySelection();
      }, 100);
    });

    document.addEventListener('mousedown', () => {
      this.historyUIManager.clearHistorySelection();
    });

    // Search navigation in search input (allow history navigation shortcuts even when search input is focused)
    if (this.domManager.searchInput) {
      this.domManager.searchInput.addEventListener('keydown', (e) => {
        // Use eventHandler's user settings if available
        if (this.eventHandler && this.userSettings?.shortcuts) {
          const handled = this.eventHandler.handleHistoryNavigationShortcuts(e, (direction) => {
            this.navigateHistory(e, direction);
          });
          // Prevent event propagation to avoid duplicate handling by document listener
          if (handled) {
            e.stopPropagation();
            e.stopImmediatePropagation();
          }
        }
      });
    }
  }

  private setupIPCListeners(): void {
    electronAPI.on('window-shown', (...args: unknown[]) => {
      const data = args[0] as WindowData;
      this.handleWindowShown(data);
    });

    // Listen for Stage 2 directory data updates
    electronAPI.on('directory-data-updated', (...args: unknown[]) => {
      const data = args[0] as DirectoryInfo;
      this.handleDirectoryDataUpdated(data);
    });

    // Listen for custom search source file changes (hot reload)
    this.customSearchUpdateHandler = () => {
      this.agentSkillManager?.invalidateCache();
      this.fileSearchManager?.clearAgentsCache();
      this.agentSkillManager?.prefetchSkills();
    };
    window.addEventListener('custom-search-updated', this.customSearchUpdateHandler);

    // Listen for custom search command source errors
    this.commandErrorHandler = (e: Event) => {
      const message = (e as CustomEvent<string>).detail;
      this.domManager.showError(message, 4000);
    };
    window.addEventListener('custom-search-command-error', this.commandErrorHandler);
  }

  private async handleKeyDown(e: KeyboardEvent): Promise<void> {
    try {
      if (!this.domManager.textarea) return;

      if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
        // Store current text content before paste operation
        const textBeforePaste = this.domManager.getCurrentText();
        const cursorPosition = this.domManager.textarea.selectionStart;
        
        // Let default paste happen first, then check if we need to handle image
        setTimeout(async () => {
          try {
            const result = (await electronAPI.invoke('paste-image')) as unknown as ImageResult;
            if (result.success && result.path) {
              // Image paste successful - remove any text that was pasted and insert image path
              this.domManager.setText(textBeforePaste);
              this.domManager.setCursorPosition(cursorPosition);
              // Use relative path with @ prefix when imagesDirectory is relative
              const insertPath = result.relativePath
                ? `@${result.relativePath}`
                : result.path;
              this.domManager.insertTextAtCursor(insertPath);
              this.draftManager.saveDraftDebounced();
              // Add pasted image to file search index
              if (result.relativePath) {
                this.fileSearchManager?.addFileToIndex({
                  name: result.relativePath.split('/').pop() || result.relativePath,
                  path: result.relativePath,
                  isDirectory: false,
                  mtimeMs: Date.now()
                });
              }
            }
            // If no image, the default text paste behavior is preserved
          } catch (error) {
            rendererLogger.error('Error handling image paste:', error);
          }
        }, 0);
        return;
      }

      // Skip shortcuts if IME is active to avoid conflicts with Japanese input
      const isComposing = this.eventHandler?.getIsComposing() || e.isComposing;
      if (isComposing) {
        return;
      }
    } catch (error) {
      rendererLogger.error('Error handling keydown:', error);
    }
  }


  private async handleTextPasteCallback(text: string): Promise<void> {
    const result = await electronAPI.pasteText(text) as PasteResult;
    if (!result.success) {
      rendererLogger.error('Paste error:', result.error);
      this.domManager.showError('Paste failed: ' + result.error);
    } else if (result.warning) {
      rendererLogger.warn('Paste warning:', result.warning);
    } else {
      // Clear snapshot after successful paste
      this.snapshotManager.clearSnapshot();
      await this.clearTextAndDraft();
      this.historyUIManager.clearHistorySelection();
    }
  }

  /**
   * Handle Cmd+S to save current draft to history
   */
  private async handleSaveDraftToHistory(): Promise<void> {
    const text = this.domManager.getCurrentText();
    const trimmedText = text.trim();
    if (!trimmedText) {
      rendererLogger.info('No text to save to history');
      return;
    }

    // Skip if same as latest history item
    if (this.historyData.length > 0 && this.historyData[0]?.text === trimmedText) {
      rendererLogger.info('Text is same as latest history, skipping');
      return;
    }

    try {
      const result = await electronAPI.invoke('save-draft-to-history', text) as unknown as { success: boolean; item?: HistoryItem; error?: string };
      if (result.success && result.item) {
        // Add item to local history data and re-render UI
        this.historyData.unshift(result.item);
        this.filteredHistoryData = this.historyData.slice(0, this.nonSearchDisplayLimit);
        this.totalMatchCount = this.historyData.length;
        this.renderHistory();
        rendererLogger.info('Draft saved to history via Cmd+S');
      } else {
        rendererLogger.error('Failed to save draft to history:', result.error);
      }
    } catch (error) {
      rendererLogger.error('Error saving draft to history:', error);
    }
  }

  /**
   * Handle Cmd+Z undo operation
   * @returns true if snapshot was restored, false otherwise
   */
  private handleUndo(): boolean {
    if (this.snapshotManager.hasSnapshot()) {
      const snapshot = this.snapshotManager.restore();
      if (snapshot) {
        this.domManager.setText(snapshot.text);
        this.domManager.setCursorPosition(snapshot.cursorPosition);
        this.domManager.focusTextarea();
        console.debug('Snapshot restored successfully');
        return true;
      }
    }
    // Let browser handle default undo
    console.debug('No snapshot, using browser default undo');
    return false;
  }

  private async handleWindowHideCallback(): Promise<void> {
    try {
      await this.draftManager.saveDraftImmediate();
      await electronAPI.window.hide();
    } catch (error) {
      rendererLogger.error('Error handling window hide:', error);
    }
  }

  private handleTabKeyCallback(e: KeyboardEvent): void {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();

    this.domManager.insertTextAtCursor('\t');
    this.draftManager.saveDraftDebounced();
  }

  private handleShiftTabKeyCallback(e: KeyboardEvent): void {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();

    this.domManager.outdentAtCursor();
    this.draftManager.saveDraftDebounced();
  }


  private async clearTextAndDraft(): Promise<void> {
    this.domManager.clearText();
    await this.draftManager.clearDraft();
    // Clear tracked @paths when text is cleared
    this.fileSearchManager?.clearAtPaths();
  }



  private async handleWindowShown(data: WindowData): Promise<void> {
    // If init hasn't completed yet, queue the data for later processing
    // This prevents race condition where window-shown arrives before fileSearchManager is initialized
    if (!this.initCompleted) {
      console.debug('[PromptLineRenderer] Queueing window-shown data (init not complete)');
      this.pendingWindowData = data;
      return;
    }

    // Mark window-shown as in-flight so a concurrent directory-data-updated
    // does not overtake us and get its hint write overwritten when the
    // window-shown body resumes.
    const work = (async (): Promise<void> => {
      try {
        const lastChange = await electronAPI.customSearch.getLastChangeTimestamp();
        if (lastChange > this.lastCustomSearchChangeTimestamp) {
          this.lastCustomSearchChangeTimestamp = lastChange;
          this.agentSkillManager?.invalidateCache();
          this.fileSearchManager?.clearAgentsCache();
          this.agentSkillManager?.prefetchSkills();
        }
      } catch {
        this.agentSkillManager?.invalidateCache();
        this.fileSearchManager?.clearAgentsCache();
        electronAPI.invoke('invalidate-custom-search').catch(() => {});
      }
      await this.directoryDataHandler.handleWindowShown(data);
    })();
    this.inflightWindowShown = work;
    try {
      await work;
    } finally {
      if (this.inflightWindowShown === work) this.inflightWindowShown = null;
    }
  }

  private async handleDirectoryDataUpdated(data: DirectoryInfo): Promise<void> {
    // Defer in two cases:
    //   1. init() hasn't finished wiring up domManager — the hint write would
    //      land on a null hintTextEl.
    //   2. window-shown is still pending (init's queue has not consumed it
    //      yet) — without this, the BG result lands first, then window-shown
    //      runs and overwrites the hint with the cached/stale directory.
    if (!this.initCompleted || this.pendingWindowData) {
      console.debug('[PromptLineRenderer] Queueing directory-data-updated', {
        initCompleted: this.initCompleted,
        hasPendingWindow: !!this.pendingWindowData
      });
      this.pendingDirectoryData = data;
      return;
    }
    // If window-shown is mid-flight (started before us but its body is still
    // awaiting an internal IPC), wait for it to settle before applying the
    // BG-detection update. Otherwise we'd be overwritten by its tail.
    if (this.inflightWindowShown) {
      try {
        await this.inflightWindowShown;
      } catch {
        // window-shown errors are logged elsewhere; we just need ordering
      }
    }
    await this.directoryDataHandler.handleDirectoryDataUpdated(data);
  }


  private updateHistoryAndSettings(data: WindowData): void {
    this.historyData = data.history || [];
    // Reset display limit and limit initial display
    this.nonSearchDisplayLimit = DEFAULT_DISPLAY_LIMIT;
    this.filteredHistoryData = this.historyData.slice(0, this.nonSearchDisplayLimit);
    this.totalMatchCount = this.historyData.length;
    this.searchManager?.updateHistoryData(this.historyData);

    // Update user settings if provided
    if (data.settings) {
      this.userSettings = data.settings;
      // Pass settings to event handler
      if (this.eventHandler) {
        this.eventHandler.setUserSettings(data.settings);
      }
      // Update custom search shortcuts
      this.updateCustomSearchShortcuts();
    }

    this.renderHistory();
  }





  private renderHistory(): void {
    this.historyUIManager.renderHistory(this.filteredHistoryData, this.totalMatchCount);
  }




  private navigateHistory(e: KeyboardEvent, direction: 'next' | 'prev'): void {
    this.historyUIManager.navigateHistory(e, direction, this.filteredHistoryData);
  }





  // Public API methods
  public getCurrentText(): string {
    return this.domManager.getCurrentText();
  }

  public setText(text: string): void {
    this.domManager.setText(text);
  }

  public clearText(): void {
    this.domManager.clearText();
  }

  public focus(): void {
    this.domManager.focusTextarea();
  }

  /**
   * Activate custom search by inserting trigger text into textarea
   */
  private handleCustomSearchActivate(triggerText: string): void {
    if (!this.domManager.textarea) return;

    const textarea = this.domManager.textarea;
    const cursorPos = textarea.selectionStart ?? textarea.value.length;
    const before = textarea.value.substring(0, cursorPos);
    const after = textarea.value.substring(textarea.selectionEnd ?? cursorPos);

    // Insert trigger text at current cursor position
    this.domManager.setText(before + triggerText + after);
    this.domManager.setCursorPosition(cursorPos + triggerText.length);
    this.domManager.focusTextarea();

    // Dispatch input event to trigger mention detection
    textarea.dispatchEvent(new Event('input', { bubbles: true }));
  }

  /**
   * Execute a "run=" shortcut command via main process and close the window.
   * Fire-and-forget: the window always closes after the IPC roundtrip,
   * regardless of command success/failure (errors are logged to ~/.prompt-line/app.log).
   * This matches the typical use case (launching an editor, opening a folder).
   */
  private async handleRunCommand(command: string): Promise<void> {
    try {
      const result = await electronAPI.shortcut.exec(command);
      if (!result.success) {
        rendererLogger.error('Shortcut command execution failed:', result.error);
      }
    } catch (error) {
      rendererLogger.error('Failed to invoke shortcut command:', error);
    }
    await this.handleWindowHideCallback();
  }

  /**
   * Extract shortcut entries from customShortcuts and dispatch to event handler.
   * Supported action prefixes:
   *   "input=<text>" — insert <text> into the textarea (and trigger custom-search detection)
   *   "run=<command>" — execute <command> as a shell command (template variables allowed)
   */
  private updateCustomSearchShortcuts(): void {
    const customSearchShortcuts: Array<{ shortcut: string; triggerText: string }> = [];
    const runShortcuts: Array<{ shortcut: string; command: string }> = [];

    if (this.userSettings?.customShortcuts) {
      for (const { key, action } of this.userSettings.customShortcuts) {
        if (action.startsWith('input=')) {
          customSearchShortcuts.push({ shortcut: key, triggerText: action.slice('input='.length) });
        } else if (action.startsWith('run=')) {
          runShortcuts.push({ shortcut: key, command: action.slice('run='.length) });
        }
      }
    }

    this.eventHandler?.setCustomSearchShortcuts(customSearchShortcuts);
    this.eventHandler?.setRunShortcuts(runShortcuts);
  }

  // Search functionality callbacks
  private handleSearchToggle(): void {
    this.searchManager?.toggleSearchMode();
  }

  // Flag to track when loadMore is in progress (for incremental append optimization)
  private _isLoadingMore: boolean = false;

  private handleSearchStateChange(isSearchMode: boolean, filteredData: HistoryItem[], totalMatches?: number): void {
    // Incremental append for loadMore (avoids full DOM rebuild)
    if (this._isLoadingMore && isSearchMode) {
      const previousLength = this.filteredHistoryData.length;
      this.filteredHistoryData = filteredData;
      this.totalMatchCount = totalMatches;
      const newItems = filteredData.slice(previousLength);
      if (newItems.length > 0) {
        this.historyUIManager.appendHistoryItems(newItems, totalMatches);
      }
      return;
    }

    // Skip render if search results AND search term haven't changed
    // (search term check is required because highlight needs updating even when items are the same)
    if (isSearchMode && this.filteredHistoryData.length > 0) {
      const currentSearchTerm = this.searchManager?.getSearchTerm() || '';
      const prevData = this.filteredHistoryData;
      if (currentSearchTerm === this._lastRenderedSearchTerm &&
          filteredData.length === prevData.length &&
          filteredData[0]?.id === prevData[0]?.id &&
          filteredData[filteredData.length - 1]?.id === prevData[prevData.length - 1]?.id) {
        this.filteredHistoryData = filteredData;
        this.totalMatchCount = totalMatches;
        return;
      }
      this._lastRenderedSearchTerm = currentSearchTerm;
    }

    // Only clear history selection when entering search mode or when items are filtered down (not when loading more)
    const isLoadingMore = filteredData.length > this.filteredHistoryData.length;
    const shouldClearSelection = !isSearchMode || (filteredData.length !== this.filteredHistoryData.length && !isLoadingMore);

    if (isSearchMode) {
      // In search mode: use filtered data from search manager
      this.filteredHistoryData = filteredData;
      this.totalMatchCount = totalMatches;
    } else {
      // Not in search mode: apply non-search display limit
      this.nonSearchDisplayLimit = DEFAULT_DISPLAY_LIMIT;
      this.filteredHistoryData = this.historyData.slice(0, this.nonSearchDisplayLimit);
      this.totalMatchCount = this.historyData.length;
      this._lastRenderedSearchTerm = '';
    }
    this.renderHistory();

    if (shouldClearSelection) {
      this.historyUIManager.clearHistorySelection();
    }

    if (!isSearchMode) {
      // Return focus to main textarea when exiting search
      this.searchManager?.focusMainTextarea();
    }
  }

  private handleLoadMore(): void {
    this._isLoadingMore = true;
    try {
      if (this.searchManager?.isInSearchMode()) {
        // Search mode: loadMore triggers handleSearchStateChange which uses incremental append
        this.searchManager.loadMore();
      } else {
        // Non-search mode: incremental append directly
        if (this.filteredHistoryData.length >= this.historyData.length) {
          return;
        }
        const previousLength = this.filteredHistoryData.length;
        this.nonSearchDisplayLimit += LOAD_MORE_INCREMENT;
        this.filteredHistoryData = this.historyData.slice(0, this.nonSearchDisplayLimit);
        const newItems = this.filteredHistoryData.slice(previousLength);
        this.historyUIManager.appendHistoryItems(newItems, this.historyData.length);
      }
    } finally {
      this._isLoadingMore = false;
    }
  }

  private resetHistoryScrollPosition(): void {
    if (this.domManager.historyList) {
      this.domManager.historyList.scrollTop = 0;
    }
  }

  // Cleanup method
  public cleanup(): void {
    this.draftManager.cleanup();
    this.historyUIManager.cleanup();
    this.fileSearchManager?.destroy();
    // Clear throttle timeout
    if (this.mouseMoveThrottleTimeout) {
      clearTimeout(this.mouseMoveThrottleTimeout);
      this.mouseMoveThrottleTimeout = null;
    }
    // Remove custom search update listener
    if (this.customSearchUpdateHandler) {
      window.removeEventListener('custom-search-updated', this.customSearchUpdateHandler);
      this.customSearchUpdateHandler = null;
    }
    if (this.commandErrorHandler) {
      window.removeEventListener('custom-search-command-error', this.commandErrorHandler);
      this.commandErrorHandler = null;
    }
  }
}

document.addEventListener('DOMContentLoaded', () => {
  (window as any).promptLineRenderer = new PromptLineRenderer();
});