import React, { createContext, memo, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from 'react';

import { activeTabStore } from '../../application/state/activeTabStore';
import { useTerminalLayoutSuppressActive } from '../../application/state/terminalLayoutSuppressStore';
import type { TerminalSessionExitEvent } from '../../application/state/resolveTerminalSessionExitIntent';
import { createTerminalSelectionAttachment } from '../../application/state/terminalSelectionAttachment';
import { useAIState } from '../../application/state/useAIState';
import { useStoredBoolean } from '../../application/state/useStoredBoolean';
import { SplitDirection } from '../../domain/workspace';
import { KeyBinding, TerminalSettings } from '../../domain/models';
import { STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION } from '../../infrastructure/config/storageKeys';
import { cn } from '../../lib/utils';
import type { DropEntry } from '../../lib/sftpFileUtils';
import type { GroupConfig, Host, Identity, KnownHost, ProxyProfile, SSHKey, Snippet, TerminalSession, TerminalTheme, Workspace } from '../../types';
import type { ExecutorContext } from '../../infrastructure/ai/cattyAgent/executor';
import { AIChatSidePanel } from '../AIChatSidePanel';
import Terminal from '../Terminal';
import { removePaneVisible, setPaneVisible } from '../terminal/paneVisibilityStore';
import {
  getTerminalPaneRenderSnapshot,
  parseTerminalPaneRenderSnapshot,
} from '../terminalPaneVisibility';

export type SidePanelTab = 'sftp' | 'scripts' | 'history' | 'theme' | 'ai' | 'system';

export type WorkspaceRect = { x: number; y: number; w: number; h: number };

export type SplitHint = {
  direction: 'horizontal' | 'vertical';
  position: 'left' | 'right' | 'top' | 'bottom';
  targetSessionId?: string;
  rect?: { x: number; y: number; w: number; h: number };
} | null;

export type ResizerHandle = {
  id: string;
  splitId: string;
  index: number;
  direction: 'vertical' | 'horizontal';
  rect: { x: number; y: number; w: number; h: number };
  splitArea: { w: number; h: number };
};

export type PendingSftpUpload = {
  requestId: string;
  hostId: string;
  /** Full connection identity (id:hostname:port:protocol) for session-override awareness */
  connectionKey: string;
  targetPath?: string;
  entries: DropEntry[];
};

export type SnippetExecutor = (
  command: string,
  noAutoRun?: boolean,
  options?: { broadcast?: boolean },
) => void;

export type PendingTerminalSelectionForAI = {
  requestId: string;
  tabId: string;
  text: string;
};

export function hexToHslToken(hex: string): string {
  const normalized = hex.startsWith('#') ? hex : `#${hex}`;
  const r = parseInt(normalized.slice(1, 3), 16) / 255;
  const g = parseInt(normalized.slice(3, 5), 16) / 255;
  const b = parseInt(normalized.slice(5, 7), 16) / 255;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  let h = 0;
  let s = 0;
  const l = (max + min) / 2;

  if (max !== min) {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r:
        h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
        break;
      case g:
        h = ((b - r) / d + 2) / 6;
        break;
      default:
        h = ((r - g) / d + 4) / 6;
        break;
    }
  }

  return `${Math.round(h * 3600) / 10} ${Math.round(s * 1000) / 10}% ${Math.round(l * 1000) / 10}%`;
}

export function adjustLightnessToken(hsl: string, delta: number): string {
  const parts = hsl.split(/\s+/);
  const newL = Math.max(0, Math.min(100, parseFloat(parts[2]) + delta));
  return `${parts[0]} ${parts[1]} ${Math.round(newL * 10) / 10}%`;
}

export function adjustSaturationToken(hsl: string, factor: number): string {
  const parts = hsl.split(/\s+/);
  const newS = Math.max(0, Math.min(100, parseFloat(parts[1]) * factor));
  return `${parts[0]} ${Math.round(newS * 10) / 10}% ${parts[2]}`;
}

export const clearTerminalPreviewVars = (sessionId: string | null) => {
  if (!sessionId || typeof document === 'undefined') return;
  const pane = document.querySelector<HTMLElement>(`[data-session-id="${sessionId}"]`);
  if (!pane) return;
  pane.style.removeProperty('--terminal-preview-bg');
  pane.style.removeProperty('--terminal-preview-fg');
  pane.style.removeProperty('--terminal-preview-border');
  pane.style.removeProperty('--terminal-preview-toolbar-btn');
  pane.style.removeProperty('--terminal-preview-toolbar-btn-hover');
  pane.style.removeProperty('--terminal-preview-toolbar-btn-active');
};

export const setStylePropertyIfChanged = (element: HTMLElement, property: string, value: string) => {
  if (element.style.getPropertyValue(property) === value) return;
  element.style.setProperty(property, value);
};

const removeStylePropertyIfSet = (element: HTMLElement, property: string) => {
  if (!element.style.getPropertyValue(property)) return;
  element.style.removeProperty(property);
};

export const clearTopTabsPreviewVars = () => {
  if (typeof document === 'undefined') return;
  const tabsRoot = document.querySelector<HTMLElement>('[data-top-tabs-root]');
  if (!tabsRoot) return;
  removeStylePropertyIfSet(tabsRoot, '--top-tabs-bg');
  removeStylePropertyIfSet(tabsRoot, '--top-tabs-fg');
  removeStylePropertyIfSet(tabsRoot, '--top-tabs-muted');
  removeStylePropertyIfSet(tabsRoot, '--top-tabs-active-bg');
  removeStylePropertyIfSet(tabsRoot, '--top-tabs-accent');
  removeStylePropertyIfSet(tabsRoot, '--background');
  removeStylePropertyIfSet(tabsRoot, '--foreground');
  removeStylePropertyIfSet(tabsRoot, '--accent');
  removeStylePropertyIfSet(tabsRoot, '--primary');
  removeStylePropertyIfSet(tabsRoot, '--secondary');
  removeStylePropertyIfSet(tabsRoot, '--border');
  removeStylePropertyIfSet(tabsRoot, '--muted-foreground');
};

export const filterTabsMap = <T,>(source: Map<string, T>, validIds: Set<string>): Map<string, T> => {
  let changed = false;
  const next = new Map<string, T>();
  for (const [id, value] of source) {
    if (validIds.has(id)) {
      next.set(id, value);
    } else {
      changed = true;
    }
  }
  return changed ? next : source;
};

// eslint-disable-next-line no-control-regex
const TERMINAL_OSC_SEQUENCE_REGEX = new RegExp('\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)', 'g');
// eslint-disable-next-line no-control-regex
const TERMINAL_ESCAPE_SEQUENCE_REGEX = new RegExp('\\u001B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])', 'g');
// eslint-disable-next-line no-control-regex
const TERMINAL_CONTROL_CHAR_REGEX = new RegExp('[\\u0000-\\u0008\\u000B-\\u001F\\u007F]', 'g');
// eslint-disable-next-line no-control-regex
const INCOMPLETE_ESCAPE_TAIL_REGEX = new RegExp('\\u001B(?:\\][^\\u0007\\u001B]*(?:\\u001B)?|\\[[0-?]*[ -/]*)?$');

const stripTerminalControlSequences = (data: string): string => {
  return data
    .replace(TERMINAL_OSC_SEQUENCE_REGEX, '')
    .replace(TERMINAL_ESCAPE_SEQUENCE_REGEX, '')
    .replace(TERMINAL_CONTROL_CHAR_REGEX, '');
};

export class ChunkedEscapeFilter {
  private pending = '';

  feed(chunk: string): string {
    const data = this.pending + chunk;
    const tailMatch = INCOMPLETE_ESCAPE_TAIL_REGEX.exec(data);
    if (tailMatch) {
      this.pending = tailMatch[0];
      return stripTerminalControlSequences(data.slice(0, tailMatch.index));
    }
    this.pending = '';
    return stripTerminalControlSequences(data);
  }
}

export const hasNotifiableTerminalOutput = (filter: ChunkedEscapeFilter, chunk: string): boolean => {
  return filter.feed(chunk).trim().length > 0;
};

export type AITerminalSessionInfo = {
  sessionId: string;
  hostId: string;
  hostname: string;
  label: string;
  os?: string;
  username?: string;
  protocol?: string;
  shellType?: string;
  deviceType?: string;
  connected: boolean;
};

export type AIPanelContext = {
  scopeType: 'terminal' | 'workspace';
  scopeTargetId?: string;
  scopeHostIds: string[];
  scopeLabel: string;
  terminalSessions: AITerminalSessionInfo[];
};

type AIStateValue = ReturnType<typeof useAIState>;

const AIStateContext = createContext<AIStateValue | null>(null);

export const buildAITerminalSessionInfo = (
  session: TerminalSession | undefined,
  host: Host | undefined,
  localOs: 'linux' | 'macos' | 'windows',
): AITerminalSessionInfo => {
  const protocol = session?.protocol || host?.protocol;
  const isLocalSession = protocol === 'local' || session?.hostId?.startsWith('local-');
  return {
    sessionId: session?.id || '',
    hostId: session?.hostId || '',
    hostname: host?.hostname || session?.hostname || '',
    label: host?.label || session?.hostLabel || '',
    os: host?.os || (isLocalSession ? localOs : undefined),
    username: host?.username || session?.username,
    protocol,
    shellType: session?.shellType && session.shellType !== 'unknown' ? session.shellType : undefined,
    // Suppress deviceType for Mosh / ET sessions — both require a shell-backed
    // PTY and cannot connect to vendor CLIs, so network device mode doesn't apply.
    deviceType: (session?.moshEnabled || host?.moshEnabled || session?.etEnabled || host?.etEnabled) ? undefined : host?.deviceType,
    connected: session?.status === 'connected',
  };
};

interface AIChatPanelsHostProps {
  mountedTabIds: string[];
  activeTabId: string | null;
  activeSidePanelTab: SidePanelTab | null;
  contextsByTabId: Map<string, AIPanelContext>;
  resolveExecutorContext: (scope: {
    type: 'terminal' | 'workspace';
    targetId?: string;
    label?: string;
  }) => ExecutorContext;
  pendingTerminalSelection?: PendingTerminalSelectionForAI | null;
  onPendingTerminalSelectionConsumed?: (requestId: string) => void;
}

interface AIStateMaintenanceHostProps {
  validAIScopeTargetIds: Set<string>;
}

const AIStateProviderInner: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const aiState = useAIState();
  return (
    <AIStateContext.Provider value={aiState}>
      {children}
    </AIStateContext.Provider>
  );
};

export const AIStateProvider = memo(AIStateProviderInner);
AIStateProvider.displayName = 'AIStateProvider';

const AIStateMaintenanceHostInner: React.FC<AIStateMaintenanceHostProps> = ({
  validAIScopeTargetIds,
}) => {
  const aiState = useContext(AIStateContext);

  if (!aiState) {
    throw new Error('AIStateMaintenanceHost must be rendered inside AIStateProvider');
  }

  const { cleanupOrphanedSessions } = aiState;

  useEffect(() => {
    cleanupOrphanedSessions(validAIScopeTargetIds);
  }, [cleanupOrphanedSessions, validAIScopeTargetIds]);

  return null;
};

export const AIStateMaintenanceHost = memo(AIStateMaintenanceHostInner);
AIStateMaintenanceHost.displayName = 'AIStateMaintenanceHost';

interface AISidePanelStateRootProps {
  validAIScopeTargetIds: Set<string>;
  children: React.ReactNode;
}

const AISidePanelStateRootInner: React.FC<AISidePanelStateRootProps> = ({
  validAIScopeTargetIds,
  children,
}) => (
  <AIStateProvider>
    <AIStateMaintenanceHost validAIScopeTargetIds={validAIScopeTargetIds} />
    {children}
  </AIStateProvider>
);

export const AISidePanelStateRoot = memo(AISidePanelStateRootInner);
AISidePanelStateRoot.displayName = 'AISidePanelStateRoot';

function aiChatPanelsHostAreEqual(
  prev: AIChatPanelsHostProps,
  next: AIChatPanelsHostProps,
): boolean {
  if (prev.mountedTabIds !== next.mountedTabIds) return false;
  if (prev.contextsByTabId !== next.contextsByTabId) return false;
  if (prev.activeSidePanelTab !== next.activeSidePanelTab) return false;
  if (prev.pendingTerminalSelection !== next.pendingTerminalSelection) return false;
  if (prev.onPendingTerminalSelectionConsumed !== next.onPendingTerminalSelectionConsumed) return false;
  if (prev.resolveExecutorContext !== next.resolveExecutorContext) return false;
  if (prev.activeTabId === next.activeTabId) return true;

  for (let i = 0; i < prev.mountedTabIds.length; i += 1) {
    const tabId = prev.mountedTabIds[i];
    const prevAiVisible = prev.activeTabId === tabId && prev.activeSidePanelTab === 'ai';
    const nextAiVisible = next.activeTabId === tabId && next.activeSidePanelTab === 'ai';
    if (prevAiVisible !== nextAiVisible) return false;
  }
  return true;
}

const AIChatPanelsHostInner: React.FC<AIChatPanelsHostProps> = ({
  mountedTabIds,
  activeTabId,
  activeSidePanelTab,
  contextsByTabId,
  resolveExecutorContext,
  pendingTerminalSelection,
  onPendingTerminalSelectionConsumed,
}) => {
  const aiState = useContext(AIStateContext);

  if (!aiState) {
    throw new Error('AIChatPanelsHost must be rendered inside AIStateProvider');
  }
  const {
    activeSessionIdMap,
    defaultAgentId,
    panelViewByScope,
    showDraftView,
    updateDraft,
  } = aiState;

  useEffect(() => {
    if (!pendingTerminalSelection) return;

    const context = contextsByTabId.get(pendingTerminalSelection.tabId);
    if (!context) return;

    const attachment = createTerminalSelectionAttachment(pendingTerminalSelection.text);
    if (!attachment) {
      onPendingTerminalSelectionConsumed?.(pendingTerminalSelection.requestId);
      return;
    }

    const scopeKey = `${context.scopeType}:${context.scopeTargetId ?? ''}`;
    const isSessionView =
      panelViewByScope[scopeKey]?.mode === 'session'
      || activeSessionIdMap[scopeKey] != null;
    if (!isSessionView) {
      showDraftView(scopeKey);
    }
    updateDraft(scopeKey, defaultAgentId, (draft) => ({
      ...draft,
      attachments: [...draft.attachments, attachment],
    }));
    onPendingTerminalSelectionConsumed?.(pendingTerminalSelection.requestId);
  }, [
    activeSessionIdMap,
    contextsByTabId,
    defaultAgentId,
    onPendingTerminalSelectionConsumed,
    panelViewByScope,
    pendingTerminalSelection,
    showDraftView,
    updateDraft,
  ]);

  return (
    <>
      {mountedTabIds.map((tabId) => {
        const context = contextsByTabId.get(tabId);
        if (!context) return null;

        const isVisible = activeTabId === tabId && activeSidePanelTab === 'ai';

        return (
          <div
            key={tabId}
            className={cn("absolute inset-0 z-10", !isVisible && "hidden")}
          >
            <AIChatSidePanel
                  sessions={aiState.sessions}
                  activeSessionIdMap={aiState.activeSessionIdMap}
                  draftsByScope={aiState.draftsByScope}
                  panelViewByScope={aiState.panelViewByScope}
                  setActiveSessionId={aiState.setActiveSessionId}
                  ensureDraftForScope={aiState.ensureDraftForScope}
                  updateDraft={aiState.updateDraft}
                  showDraftView={aiState.showDraftView}
                  showSessionView={aiState.showSessionView}
                  clearDraftForScope={aiState.clearDraftForScope}
                  addDraftFiles={aiState.addDraftFiles}
                  removeDraftFile={aiState.removeDraftFile}
                  createSession={aiState.createSession}
                  deleteSession={aiState.deleteSession}
                  updateSessionTitle={aiState.updateSessionTitle}
                  updateSessionExternalSessionId={aiState.updateSessionExternalSessionId}
                  addMessageToSession={aiState.addMessageToSession}
                  updateLastMessage={aiState.updateLastMessage}
                  updateMessageById={aiState.updateMessageById}
                  providers={aiState.providers}
                  activeProviderId={aiState.activeProviderId}
                  activeModelId={aiState.activeModelId}
                  defaultAgentId={aiState.defaultAgentId}
                  toolIntegrationMode={aiState.toolIntegrationMode}
                  externalAgents={aiState.externalAgents}
                  setExternalAgents={aiState.setExternalAgents}
                  agentModelMap={aiState.agentModelMap}
                  setAgentModel={aiState.setAgentModel}
                  agentProviderMap={aiState.agentProviderMap}
                  setAgentProvider={aiState.setAgentProvider}
                  globalPermissionMode={aiState.globalPermissionMode}
                  setGlobalPermissionMode={aiState.setGlobalPermissionMode}
                  commandBlocklist={aiState.commandBlocklist}
                  maxIterations={aiState.maxIterations}
                  webSearchConfig={aiState.webSearchConfig}
                  quickMessages={aiState.quickMessages}
                  scopeType={context.scopeType}
                  scopeTargetId={context.scopeTargetId}
                  scopeHostIds={context.scopeHostIds}
                  scopeLabel={context.scopeLabel}
                  terminalSessions={context.terminalSessions}
                  resolveExecutorContext={resolveExecutorContext}
                  isVisible={isVisible}
                />
          </div>
        );
      })}
    </>
  );
};

export const AIChatPanelsHost = memo(AIChatPanelsHostInner, aiChatPanelsHostAreEqual);
AIChatPanelsHost.displayName = 'AIChatPanelsHost';

export interface TerminalLayerProps {
  hosts: Host[];
  customGroups: string[];
  groupConfigs: GroupConfig[];
  proxyProfiles: ProxyProfile[];
  keys: SSHKey[];
  identities: Identity[];
  snippets: Snippet[];
  snippetPackages: string[];
  sessions: TerminalSession[];
  workspaces: Workspace[];
  knownHosts?: KnownHost[];
  draggingSessionId: string | null;
  terminalTheme: TerminalTheme;
  followAppTerminalTheme?: boolean;
  accentMode?: 'theme' | 'custom';
  customAccent?: string;
  terminalSettings?: TerminalSettings;
  terminalFontFamilyId: string;
  fontSize?: number;
  hotkeyScheme?: 'disabled' | 'mac' | 'pc';
  keyBindings?: KeyBinding[];
  onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
  onUpdateTerminalThemeId?: (themeId: string) => void;
  onUpdateTerminalFontFamilyId?: (fontFamilyId: string) => void;
  onUpdateTerminalFontSize?: (fontSize: number) => void;
  onUpdateTerminalFontWeight?: (fontWeight: number) => void;
  onUpdateSessionFontSize?: (sessionId: string, fontSize: number) => void;
  onClearSessionFontSizeOverride?: (sessionId: string) => void;
  onCloseSession: (sessionId: string, e?: React.MouseEvent) => void;
  onUpdateSessionStatus: (sessionId: string, status: TerminalSession['status']) => void;
  onUpdateHostDistro: (hostId: string, distro: string) => void;
  onUpdateHost: (host: Host) => void;
  onAddKnownHost?: (knownHost: KnownHost) => void;
  onCommandExecuted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
  shellHistory?: import('../../types').ShellHistoryEntry[];
  onTerminalDataCapture?: (sessionId: string, data: string) => void;
  onCreateWorkspaceFromSessions: (baseSessionId: string, joiningSessionId: string, hint: Exclude<SplitHint, null>) => void;
  onAddSessionToWorkspace: (workspaceId: string, sessionId: string, hint: Exclude<SplitHint, null>) => void;
  onRequestAddToWorkspace?: (workspaceId: string) => void;
  onUpdateSplitSizes: (workspaceId: string, splitId: string, sizes: number[]) => void;
  onSetDraggingSessionId: (id: string | null) => void;
  onToggleWorkspaceViewMode?: (workspaceId: string) => void;
  onSetWorkspaceFocusedSession?: (workspaceId: string, sessionId: string) => void;
  onReorderWorkspaceSessions?: (workspaceId: string, draggedSessionId: string, targetSessionId: string, position: 'before' | 'after') => void;
  onSplitSession?: (sessionId: string, direction: SplitDirection) => void;
  onConnectToHost: (host: Host) => void;
  onCreateLocalTerminal?: () => void;
  // Broadcast mode
  isBroadcastEnabled?: (workspaceId: string) => boolean;
  onToggleBroadcast?: (workspaceId: string) => void;
  // SFTP side panel
  updateHosts: (hosts: Host[]) => void;
  updateSnippets?: (snippets: Snippet[]) => void;
  updateSnippetPackages?: (packages: string[]) => void;
  sftpDefaultViewMode: 'list' | 'tree';
  sftpDoubleClickBehavior: 'open' | 'transfer';
  sftpAutoSync: boolean;
  sftpShowHiddenFiles: boolean;
  sftpUseCompressedUpload: boolean;
  sftpAutoOpenSidebar: boolean;
  sftpFollowTerminalCwd: boolean;
  setSftpFollowTerminalCwd: (enabled: boolean) => void;
  editorWordWrap: boolean;
  setEditorWordWrap: (value: boolean) => void;
  // Session log settings for real-time streaming
  sessionLogsEnabled?: boolean;
  sessionLogsDir?: string;
  sessionLogsFormat?: string;
  sessionLogsTimestampsEnabled?: boolean;
  sshDebugLogsEnabled?: boolean;
  showHostTreeSidebar?: boolean;
  toggleScriptsSidePanelRef?: React.MutableRefObject<(() => void) | null>;
  toggleSidePanelRef?: React.MutableRefObject<(() => void) | null>;
}

interface TerminalPaneProps {
  session: TerminalSession;
  host: Host;
  chainHosts?: Host[];
  sudoAutofillPassword?: string;
  workspaceById: Map<string, Workspace>;
  workspaceRectsById: Map<string, Record<string, WorkspaceRect>>;
  isTerminalLayerVisible: boolean;
  workspaceFocusHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  workspaceBroadcastHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  splitHorizontalHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  splitVerticalHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  themePreview: { targetSessionId: string | null; themeId: string | null };
  keys: SSHKey[];
  identities: Identity[];
  snippets: Snippet[];
  knownHosts: KnownHost[];
  terminalFontFamilyId: string;
  fontSize: number;
  terminalTheme: TerminalTheme;
  followAppTerminalTheme?: boolean;
  accentMode?: 'theme' | 'custom';
  customAccent?: string;
  terminalSettings?: TerminalSettings;
  hotkeyScheme?: 'disabled' | 'mac' | 'pc';
  keyBindings?: KeyBinding[];
  isResizing: boolean;
  isComposeBarOpen: boolean;
  sessionLog?: { enabled: true; directory: string; format: string; timestampsEnabled?: boolean };
  sshDebugLogEnabled?: boolean;
  onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
  onTerminalFontSizeChange?: (sessionId: string, fontSize: number) => void;
  onOpenSftp: (
    host: Host,
    initialPath?: string,
    pendingUploadEntries?: DropEntry[],
    sourceSessionId?: string,
  ) => void;
  onTerminalCwdChange: (sessionId: string, cwd: string | null) => void;
  onOpenScripts: () => void;
  onOpenHistory?: () => void;
  onOpenTheme: () => void;
  onOpenSystem?: () => void;
  onCloseSession: (sessionId: string) => void;
  onStatusChange: (sessionId: string, status: TerminalSession['status']) => void;
  onSessionExit: (sessionId: string, evt: TerminalSessionExitEvent) => void;
  onTerminalDataCapture?: (sessionId: string, data: string) => void;
  onOsDetected: (hostId: string, distro: string) => void;
  onUpdateHost: (host: Host) => void;
  onAddKnownHost?: (knownHost: KnownHost) => void;
  onCommandExecuted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
  shellHistory?: import('../../types').ShellHistoryEntry[];
  onCommandSubmitted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
  onSetWorkspaceFocusedSession?: (workspaceId: string, sessionId: string) => void;
  onSplitSession?: (sessionId: string, direction: SplitDirection) => void;
  isBroadcastEnabled?: (workspaceId: string) => boolean;
  onBroadcastInput: (data: string, sourceSessionId: string) => void;
  onToggleWorkspaceComposeBar: () => void;
  onSnippetExecutorChange: (
    sessionId: string,
    executor: SnippetExecutor | null,
  ) => void;
  onAddSelectionToAI?: (sessionId: string, selection: string) => void;
  showSelectionAIAction: boolean;
}

const getPaneThemePreviewId = (props: TerminalPaneProps): string | null => (
  props.session.id === props.themePreview.targetSessionId
    ? props.themePreview.themeId
    : null
);

const getPaneWorkspaceRect = (props: Pick<TerminalPaneProps, 'session' | 'workspaceRectsById'>): WorkspaceRect | null => {
  const workspaceId = props.session.workspaceId;
  if (!workspaceId) return null;
  return props.workspaceRectsById.get(workspaceId)?.[props.session.id] ?? null;
};

const getPaneActiveWorkspaceRect = (props: Pick<TerminalPaneProps, 'session' | 'workspaceById' | 'workspaceRectsById'>): WorkspaceRect | null => {
  const workspaceId = props.session.workspaceId;
  if (!workspaceId) return null;
  if (activeTabStore.getActiveTabId() !== workspaceId) return null;
  const workspace = props.workspaceById.get(workspaceId);
  if (!workspace || workspace.viewMode === 'focus') return null;
  return props.workspaceRectsById.get(workspaceId)?.[props.session.id] ?? null;
};

const workspaceRectsEqual = (a: WorkspaceRect | null, b: WorkspaceRect | null): boolean => {
  if (a === b) return true;
  if (!a || !b) return false;
  return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
};

const terminalPanePropsAreEqual = (
  prev: TerminalPaneProps,
  next: TerminalPaneProps,
): boolean => (
  prev.session === next.session &&
  prev.host === next.host &&
  prev.chainHosts === next.chainHosts &&
  prev.sudoAutofillPassword === next.sudoAutofillPassword &&
  prev.workspaceById === next.workspaceById &&
  workspaceRectsEqual(getPaneActiveWorkspaceRect(prev), getPaneActiveWorkspaceRect(next)) &&
  prev.isTerminalLayerVisible === next.isTerminalLayerVisible &&
  prev.workspaceFocusHandlersRef === next.workspaceFocusHandlersRef &&
  prev.workspaceBroadcastHandlersRef === next.workspaceBroadcastHandlersRef &&
  prev.splitHorizontalHandlersRef === next.splitHorizontalHandlersRef &&
  prev.splitVerticalHandlersRef === next.splitVerticalHandlersRef &&
  getPaneThemePreviewId(prev) === getPaneThemePreviewId(next) &&
  prev.keys === next.keys &&
  prev.identities === next.identities &&
  prev.snippets === next.snippets &&
  prev.knownHosts === next.knownHosts &&
  prev.terminalFontFamilyId === next.terminalFontFamilyId &&
  prev.fontSize === next.fontSize &&
  prev.terminalTheme === next.terminalTheme &&
  prev.followAppTerminalTheme === next.followAppTerminalTheme &&
  prev.accentMode === next.accentMode &&
  prev.customAccent === next.customAccent &&
  prev.terminalSettings === next.terminalSettings &&
  prev.hotkeyScheme === next.hotkeyScheme &&
  prev.keyBindings === next.keyBindings &&
  prev.isResizing === next.isResizing &&
  prev.isComposeBarOpen === next.isComposeBarOpen &&
  prev.sessionLog === next.sessionLog &&
  prev.sshDebugLogEnabled === next.sshDebugLogEnabled &&
  prev.onHotkeyAction === next.onHotkeyAction &&
  prev.onTerminalFontSizeChange === next.onTerminalFontSizeChange &&
  prev.onOpenSftp === next.onOpenSftp &&
  prev.onTerminalCwdChange === next.onTerminalCwdChange &&
  prev.onOpenScripts === next.onOpenScripts &&
  prev.onOpenHistory === next.onOpenHistory &&
  prev.onOpenTheme === next.onOpenTheme &&
  prev.onOpenSystem === next.onOpenSystem &&
  prev.onCloseSession === next.onCloseSession &&
  prev.onStatusChange === next.onStatusChange &&
  prev.onSessionExit === next.onSessionExit &&
  prev.onTerminalDataCapture === next.onTerminalDataCapture &&
  prev.onOsDetected === next.onOsDetected &&
  prev.onUpdateHost === next.onUpdateHost &&
  prev.onAddKnownHost === next.onAddKnownHost &&
  prev.onCommandExecuted === next.onCommandExecuted &&
  prev.onCommandSubmitted === next.onCommandSubmitted &&
  prev.onSetWorkspaceFocusedSession === next.onSetWorkspaceFocusedSession &&
  prev.onSplitSession === next.onSplitSession &&
  prev.isBroadcastEnabled === next.isBroadcastEnabled &&
  prev.onBroadcastInput === next.onBroadcastInput &&
  prev.onToggleWorkspaceComposeBar === next.onToggleWorkspaceComposeBar &&
  prev.onSnippetExecutorChange === next.onSnippetExecutorChange &&
  prev.onAddSelectionToAI === next.onAddSelectionToAI &&
  prev.showSelectionAIAction === next.showSelectionAIAction
);

const TerminalPane: React.FC<TerminalPaneProps> = memo(({
  session,
  host,
  chainHosts,
  sudoAutofillPassword,
  workspaceById,
  workspaceRectsById,
  isTerminalLayerVisible,
  workspaceFocusHandlersRef,
  workspaceBroadcastHandlersRef,
  splitHorizontalHandlersRef,
  splitVerticalHandlersRef,
  themePreview,
  keys,
  identities,
  snippets,
  knownHosts,
  terminalFontFamilyId,
  fontSize,
  terminalTheme,
  followAppTerminalTheme,
  accentMode,
  customAccent,
  terminalSettings,
  hotkeyScheme,
  keyBindings,
  isResizing,
  isComposeBarOpen,
  sessionLog,
  sshDebugLogEnabled,
  onHotkeyAction,
  onTerminalFontSizeChange,
  onOpenSftp,
  onTerminalCwdChange,
  onOpenScripts,
  onOpenHistory,
  onOpenTheme,
  onOpenSystem,
  onCloseSession,
  onStatusChange,
  onSessionExit,
  onTerminalDataCapture,
  onOsDetected,
  onUpdateHost,
  onAddKnownHost,
  onCommandExecuted,
  onCommandSubmitted,
  onSetWorkspaceFocusedSession,
  onSplitSession,
  isBroadcastEnabled,
  onBroadcastInput,
  onToggleWorkspaceComposeBar,
  onSnippetExecutorChange,
  onAddSelectionToAI,
  showSelectionAIAction,
}) => {
  const layoutSuppressActive = useTerminalLayoutSuppressActive();
  const deferPaneLayoutUpdate = isResizing || layoutSuppressActive;

  const getRenderSnapshot = useCallback(
    () => getTerminalPaneRenderSnapshot({
      activeTabId: activeTabStore.getActiveTabId(),
      sessionId: session.id,
      sessionWorkspaceId: session.workspaceId,
      workspaceById,
      isTerminalLayerVisible,
    }),
    [isTerminalLayerVisible, session.id, session.workspaceId, workspaceById],
  );
  const renderSnapshot = useSyncExternalStore(activeTabStore.subscribe, getRenderSnapshot, getRenderSnapshot);
  const { paneState, isFocusedPane } = parseTerminalPaneRenderSnapshot(renderSnapshot);
  const activeWorkspaceId = paneState.workspaceId;
  const isVisible = paneState.isVisible;

  // Publish visibility to the per-session store so TerminalServerStats /
  // TerminalAutocomplete can self-subscribe — keeping isVisible out of the
  // TerminalView ctx so visibility toggles don't re-render TerminalView.
  useEffect(() => {
    setPaneVisible(session.id, isVisible);
  }, [session.id, isVisible]);
  useEffect(() => () => removePaneVisible(session.id), [session.id]);
  const inActiveWorkspace = !!activeWorkspaceId;
  const isFocusMode = paneState.mode === 'focus';
  const isSplitViewVisible = paneState.mode === 'split';
  const rect = activeWorkspaceId && isSplitViewVisible
    ? workspaceRectsById.get(activeWorkspaceId)?.[session.id] ?? null
    : null;
  const layoutStyle = rect
    ? {
      left: `${rect.x}px`,
      top: `${rect.y}px`,
      width: `${rect.w}px`,
      height: `${rect.h}px`,
    }
    : { left: 0, top: 0, width: '100%', height: '100%' };
  const livePaneLayoutKey = isSplitViewVisible && rect
    ? `${Math.round(rect.w)}x${Math.round(rect.h)}`
    : 'full';
  const paneLayoutKeyRef = useRef(livePaneLayoutKey);
  const [, bumpPaneLayoutKeyVersion] = useState(0);
  const shouldCommitLayoutImmediately =
    !deferPaneLayoutUpdate && (!isSplitViewVisible || isFocusMode || isFocusedPane);
  if (shouldCommitLayoutImmediately && paneLayoutKeyRef.current !== livePaneLayoutKey) {
    paneLayoutKeyRef.current = livePaneLayoutKey;
  }
  useEffect(() => {
    if (deferPaneLayoutUpdate || !isVisible || !isSplitViewVisible || isFocusMode || isFocusedPane) return;
    if (paneLayoutKeyRef.current === livePaneLayoutKey) return;

    let cancelled = false;
    const commitDeferredLayout = () => {
      if (cancelled || !isVisible) return;
      if (paneLayoutKeyRef.current === livePaneLayoutKey) return;
      paneLayoutKeyRef.current = livePaneLayoutKey;
      bumpPaneLayoutKeyVersion((version) => version + 1);
    };

    if (typeof requestIdleCallback === 'function') {
      const idleId = requestIdleCallback(commitDeferredLayout, { timeout: 500 });
      return () => {
        cancelled = true;
        cancelIdleCallback(idleId);
      };
    }

    const timerId = setTimeout(commitDeferredLayout, 350);
    return () => {
      cancelled = true;
      clearTimeout(timerId);
    };
  }, [deferPaneLayoutUpdate, isFocusedPane, isFocusMode, isSplitViewVisible, isVisible, livePaneLayoutKey]);

  const paneLayoutKey = paneLayoutKeyRef.current;
  const style: React.CSSProperties = { ...layoutStyle };

  if (!isVisible) {
    style.visibility = 'hidden';
    style.pointerEvents = 'none';
    // Preserve xterm state while keeping hidden terminals out of layout.
    style.left = '-9999px';
    style.top = '-9999px';
  }

  const workspaceFocusHandler = activeWorkspaceId
    ? workspaceFocusHandlersRef.current.get(activeWorkspaceId)
    : undefined;
  const workspaceBroadcastHandler = activeWorkspaceId
    ? workspaceBroadcastHandlersRef.current.get(activeWorkspaceId)
    : undefined;
  const splitHorizontalHandler = splitHorizontalHandlersRef.current.get(session.id);
  const splitVerticalHandler = splitVerticalHandlersRef.current.get(session.id);
  const broadcastEnabled = activeWorkspaceId ? !!isBroadcastEnabled?.(activeWorkspaceId) : false;
  const themePreviewId = session.id === themePreview.targetSessionId
    ? themePreview.themeId ?? undefined
    : undefined;

  const handlePaneClick = useCallback(() => {
    if (activeWorkspaceId && !isFocusMode) {
      onSetWorkspaceFocusedSession?.(activeWorkspaceId, session.id);
    }
  }, [activeWorkspaceId, isFocusMode, onSetWorkspaceFocusedSession, session.id]);
  const handleOpenSystemForPane = useCallback(() => {
    if (activeWorkspaceId && !isFocusMode) {
      onSetWorkspaceFocusedSession?.(activeWorkspaceId, session.id);
    }
    onOpenSystem?.();
  }, [activeWorkspaceId, isFocusMode, onOpenSystem, onSetWorkspaceFocusedSession, session.id]);
  const handleTerminalFontSizeChange = useCallback((nextFontSize: number) => {
    onTerminalFontSizeChange?.(session.id, nextFontSize);
  }, [onTerminalFontSizeChange, session.id]);

  return (
    <div
      data-session-id={session.id}
      data-section="terminal-split-pane"
      data-focused={isFocusedPane ? 'true' : undefined}
      className={cn(
        "absolute bg-background",
        inActiveWorkspace && "workspace-pane",
        isVisible && "z-10",
      )}
      style={style}
      tabIndex={-1}
      onClick={handlePaneClick}
    >
      <Terminal
        host={host}
        keys={keys}
        identities={identities}
        snippets={snippets}
        chainHosts={chainHosts}
        themePreviewId={themePreviewId}
        knownHosts={knownHosts}
        isVisible={isVisible}
        paneLayoutKey={paneLayoutKey}
        inWorkspace={inActiveWorkspace}
        isResizing={isResizing}
        isFocusMode={isFocusMode}
        isFocused={isFocusedPane}
        fontFamilyId={terminalFontFamilyId}
        fontSize={fontSize}
        terminalTheme={terminalTheme}
        followAppTerminalTheme={followAppTerminalTheme}
        accentMode={accentMode}
        customAccent={customAccent}
        terminalSettings={terminalSettings}
        sessionId={session.id}
        startupCommand={session.startupCommand}
        noAutoRun={session.noAutoRun}
        reuseConnectionFromSessionId={session.reuseConnectionFromSessionId}
        serialConfig={session.serialConfig}
        hotkeyScheme={hotkeyScheme}
        keyBindings={keyBindings}
        onHotkeyAction={onHotkeyAction}
        onTerminalFontSizeChange={handleTerminalFontSizeChange}
        onOpenSftp={onOpenSftp}
        onTerminalCwdChange={onTerminalCwdChange}
        onOpenScripts={onOpenScripts}
        onOpenHistory={onOpenHistory}
        onOpenTheme={onOpenTheme}
        onOpenSystem={handleOpenSystemForPane}
        onCloseSession={onCloseSession}
        onStatusChange={onStatusChange}
        onSessionExit={onSessionExit}
        onTerminalDataCapture={onTerminalDataCapture}
        onOsDetected={onOsDetected}
        onUpdateHost={onUpdateHost}
        onAddKnownHost={onAddKnownHost}
        onCommandExecuted={onCommandExecuted}
        onCommandSubmitted={onCommandSubmitted}
        onExpandToFocus={inActiveWorkspace && !isFocusMode ? workspaceFocusHandler : undefined}
        onSplitHorizontal={onSplitSession ? splitHorizontalHandler : undefined}
        onSplitVertical={onSplitSession ? splitVerticalHandler : undefined}
        isBroadcastEnabled={broadcastEnabled}
        onToggleBroadcast={inActiveWorkspace ? workspaceBroadcastHandler : undefined}
        onToggleComposeBar={inActiveWorkspace ? onToggleWorkspaceComposeBar : undefined}
        isWorkspaceComposeBarOpen={inActiveWorkspace ? isComposeBarOpen : undefined}
        onBroadcastInput={broadcastEnabled ? onBroadcastInput : undefined}
        onSnippetExecutorChange={onSnippetExecutorChange}
        sessionLog={sessionLog}
        sshDebugLogEnabled={sshDebugLogEnabled}
        sudoAutofillPassword={sudoAutofillPassword}
        showSelectionAIAction={showSelectionAIAction}
        onAddSelectionToAI={onAddSelectionToAI}
      />
    </div>
  );
}, terminalPanePropsAreEqual);
TerminalPane.displayName = 'TerminalPane';

interface TerminalPanesHostProps {
  sessions: TerminalSession[];
  sessionHostsMap: Map<string, Host>;
  sessionChainHostsMap: Map<string, Host[]>;
  sessionSudoAutofillPasswordsMap: Map<string, string | undefined>;
  workspaceById: Map<string, Workspace>;
  workspaceRectsById: Map<string, Record<string, WorkspaceRect>>;
  isTerminalLayerVisible: boolean;
  workspaceFocusHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  workspaceBroadcastHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  splitHorizontalHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  splitVerticalHandlersRef: React.MutableRefObject<Map<string, () => void>>;
  themePreview: { targetSessionId: string | null; themeId: string | null };
  keys: SSHKey[];
  identities: Identity[];
  snippets: Snippet[];
  knownHosts: KnownHost[];
  terminalFontFamilyId: string;
  fontSize: number;
  terminalTheme: TerminalTheme;
  followAppTerminalTheme?: boolean;
  accentMode?: 'theme' | 'custom';
  customAccent?: string;
  terminalSettings?: TerminalSettings;
  hotkeyScheme?: 'disabled' | 'mac' | 'pc';
  keyBindings?: KeyBinding[];
  isResizing: boolean;
  isComposeBarOpen: boolean;
  sessionLog?: { enabled: true; directory: string; format: string; timestampsEnabled?: boolean };
  sshDebugLogEnabled?: boolean;
  onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
  onTerminalFontSizeChange?: TerminalPaneProps['onTerminalFontSizeChange'];
  onOpenSftp: TerminalPaneProps['onOpenSftp'];
  onTerminalCwdChange: TerminalPaneProps['onTerminalCwdChange'];
  onOpenScripts: () => void;
  onOpenHistory?: () => void;
  onOpenTheme: () => void;
  onOpenSystem?: () => void;
  onCloseSession: (sessionId: string) => void;
  onStatusChange: (sessionId: string, status: TerminalSession['status']) => void;
  onSessionExit: (sessionId: string, evt: TerminalSessionExitEvent) => void;
  onTerminalDataCapture?: (sessionId: string, data: string) => void;
  onOsDetected: (hostId: string, distro: string) => void;
  onUpdateHost: (host: Host) => void;
  onAddKnownHost?: (knownHost: KnownHost) => void;
  onCommandExecuted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
  shellHistory?: import('../../types').ShellHistoryEntry[];
  onCommandSubmitted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
  onSetWorkspaceFocusedSession?: (workspaceId: string, sessionId: string) => void;
  onSplitSession?: (sessionId: string, direction: SplitDirection) => void;
  isBroadcastEnabled?: (workspaceId: string) => boolean;
  onBroadcastInput: (data: string, sourceSessionId: string) => void;
  onToggleWorkspaceComposeBar: () => void;
  onSnippetExecutorChange: (
    sessionId: string,
    executor: SnippetExecutor | null,
  ) => void;
  onAddSelectionToAI?: (sessionId: string, selection: string) => void;
}

const terminalPanesHostPropsAreEqual = (
  prev: TerminalPanesHostProps,
  next: TerminalPanesHostProps,
): boolean => {
  if (prev.sessions !== next.sessions) return false;
  if (prev.sessionHostsMap !== next.sessionHostsMap) return false;
  if (prev.sessionChainHostsMap !== next.sessionChainHostsMap) return false;
  if (prev.sessionSudoAutofillPasswordsMap !== next.sessionSudoAutofillPasswordsMap) return false;
  if (prev.workspaceById !== next.workspaceById) return false;
  if (prev.isTerminalLayerVisible !== next.isTerminalLayerVisible) return false;
  if (prev.workspaceFocusHandlersRef !== next.workspaceFocusHandlersRef) return false;
  if (prev.workspaceBroadcastHandlersRef !== next.workspaceBroadcastHandlersRef) return false;
  if (prev.splitHorizontalHandlersRef !== next.splitHorizontalHandlersRef) return false;
  if (prev.splitVerticalHandlersRef !== next.splitVerticalHandlersRef) return false;
  if (prev.themePreview !== next.themePreview) return false;
  if (prev.keys !== next.keys) return false;
  if (prev.identities !== next.identities) return false;
  if (prev.snippets !== next.snippets) return false;
  if (prev.knownHosts !== next.knownHosts) return false;
  if (prev.terminalFontFamilyId !== next.terminalFontFamilyId) return false;
  if (prev.fontSize !== next.fontSize) return false;
  if (prev.terminalTheme !== next.terminalTheme) return false;
  if (prev.followAppTerminalTheme !== next.followAppTerminalTheme) return false;
  if (prev.accentMode !== next.accentMode) return false;
  if (prev.customAccent !== next.customAccent) return false;
  if (prev.terminalSettings !== next.terminalSettings) return false;
  if (prev.hotkeyScheme !== next.hotkeyScheme) return false;
  if (prev.keyBindings !== next.keyBindings) return false;
  if (prev.isResizing !== next.isResizing) return false;
  if (prev.isComposeBarOpen !== next.isComposeBarOpen) return false;
  if (prev.sessionLog !== next.sessionLog) return false;
  if (prev.sshDebugLogEnabled !== next.sshDebugLogEnabled) return false;
  if (prev.onHotkeyAction !== next.onHotkeyAction) return false;
  if (prev.onTerminalFontSizeChange !== next.onTerminalFontSizeChange) return false;
  if (prev.onOpenSftp !== next.onOpenSftp) return false;
  if (prev.onTerminalCwdChange !== next.onTerminalCwdChange) return false;
  if (prev.onOpenScripts !== next.onOpenScripts) return false;
  if (prev.onOpenHistory !== next.onOpenHistory) return false;
  if (prev.onOpenTheme !== next.onOpenTheme) return false;
  if (prev.onOpenSystem !== next.onOpenSystem) return false;
  if (prev.onCloseSession !== next.onCloseSession) return false;
  if (prev.onStatusChange !== next.onStatusChange) return false;
  if (prev.onSessionExit !== next.onSessionExit) return false;
  if (prev.onTerminalDataCapture !== next.onTerminalDataCapture) return false;
  if (prev.onOsDetected !== next.onOsDetected) return false;
  if (prev.onUpdateHost !== next.onUpdateHost) return false;
  if (prev.onAddKnownHost !== next.onAddKnownHost) return false;
  if (prev.onCommandExecuted !== next.onCommandExecuted) return false;
  if (prev.onCommandSubmitted !== next.onCommandSubmitted) return false;
  if (prev.onSetWorkspaceFocusedSession !== next.onSetWorkspaceFocusedSession) return false;
  if (prev.onSplitSession !== next.onSplitSession) return false;
  if (prev.isBroadcastEnabled !== next.isBroadcastEnabled) return false;
  if (prev.onBroadcastInput !== next.onBroadcastInput) return false;
  if (prev.onToggleWorkspaceComposeBar !== next.onToggleWorkspaceComposeBar) return false;
  if (prev.onSnippetExecutorChange !== next.onSnippetExecutorChange) return false;
  if (prev.onAddSelectionToAI !== next.onAddSelectionToAI) return false;

  if (prev.workspaceRectsById === next.workspaceRectsById) return true;

  const activeTabId = activeTabStore.getActiveTabId();
  const activeWorkspace = activeTabId ? next.workspaceById.get(activeTabId) : undefined;
  if (!activeWorkspace || activeWorkspace.viewMode === 'focus') return true;

  return prev.sessions.every((session) => {
    if (session.workspaceId !== activeWorkspace.id) return true;
    return workspaceRectsEqual(
      getPaneWorkspaceRect({ session, workspaceRectsById: prev.workspaceRectsById }),
      getPaneWorkspaceRect({ session, workspaceRectsById: next.workspaceRectsById }),
    );
  });
};

export const TerminalPanesHost: React.FC<TerminalPanesHostProps> = memo(({
  sessions,
  sessionHostsMap,
  sessionChainHostsMap,
  sessionSudoAutofillPasswordsMap,
  ...sharedProps
}) => {
  const [showSelectionAIAction] = useStoredBoolean(
    STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION,
    true,
  );

  return (
    <>
      {sessions.map((session) => {
        const host = sessionHostsMap.get(session.id);
        if (!host) return null;
        return (
          <TerminalPane
            key={session.id}
            session={session}
            host={host}
            chainHosts={sessionChainHostsMap.get(session.id)}
            sudoAutofillPassword={sessionSudoAutofillPasswordsMap.get(session.id)}
            showSelectionAIAction={showSelectionAIAction}
            {...sharedProps}
          />
        );
      })}
    </>
  );
}, terminalPanesHostPropsAreEqual);
TerminalPanesHost.displayName = 'TerminalPanesHost';
