"use client";

import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { PanelLeftOpen, GripVertical, X } from 'lucide-react';
import SettingsModal, { SettingsTab } from '../settings/SettingsModal';
import SearchModal from '../nodes/SearchModal';
import { ContextSummary, Node } from '@/types/database';
import { DatabaseEvent } from '@/services/events';
import { usePersistentState } from '@/hooks/usePersistentState';
import { useTheme } from '@/hooks/useTheme';

import LeftToolbar from './LeftToolbar';
import SplitHandle from './SplitHandle';

import { NodePane, ContextsPane, DimensionsPane, MapPane, ViewsPane, TablePane, SkillsPane } from '../panes';
import QuickAddInput from '../agents/QuickAddInput';
import type { PaneType, SlotState, PaneAction, SlotId } from '../panes/types';

export interface PendingNode {
  id: string;
  input: string;
  inputType: string;
  submittedAt: number;
  status: 'processing' | 'error';
  error?: string;
}

const SLOT_A_KEY = 'ui.slotA.v6';
const SLOT_B_KEY = 'ui.slotB.v6';
const SLOT_C_KEY = 'ui.slotC.v6';
const PANEL_A_EXPANDED_KEY = 'ui.panelA.expanded.v1';
const PANEL_B_EXPANDED_KEY = 'ui.panelB.expanded.v1';
const PANEL_C_EXPANDED_KEY = 'ui.panelC.expanded.v1';
const PANEL_A_WEIGHT_KEY = 'ui.panelA.weight.v1';
const PANEL_B_WEIGHT_KEY = 'ui.panelB.weight.v1';
const PANEL_C_WEIGHT_KEY = 'ui.panelC.weight.v1';
const LEFT_NAV_EXPANDED_KEY = 'ui.leftNavExpanded';
const ACTIVE_CONTEXT_KEY = 'ui.focus.activeContextId';
const ACTIVE_DIMENSION_KEY = 'ui.focus.activeDimension';

const DEFAULT_SLOT_A: SlotState = { type: 'views' };
const VALID_PANE_TYPES = new Set<PaneType>(['node', 'contexts', 'dimensions', 'map', 'views', 'table', 'skills']);

function normalizeSlotState(raw: SlotState | null): SlotState | null {
  if (!raw) return null;
  const rawType = raw.type as string;
  if (rawType === 'guides') {
    return { ...raw, type: 'skills' };
  }
  if (!VALID_PANE_TYPES.has(raw.type)) {
    return null;
  }
  return raw;
}

export default function ThreePanelLayout() {
  const containerRef = useRef<HTMLDivElement>(null);
  const panelRefs = useRef<Record<SlotId, HTMLDivElement | null>>({ A: null, B: null, C: null });
  const [theme, toggleTheme] = useTheme();

  const [slotA, setSlotA] = usePersistentState<SlotState | null>(SLOT_A_KEY, DEFAULT_SLOT_A);
  const [slotB, setSlotB] = usePersistentState<SlotState | null>(SLOT_B_KEY, null);
  const [slotC, setSlotC] = usePersistentState<SlotState | null>(SLOT_C_KEY, null);

  const [panelAExpanded, setPanelAExpanded] = usePersistentState<boolean>(PANEL_A_EXPANDED_KEY, true);
  const [panelBExpanded, setPanelBExpanded] = usePersistentState<boolean>(PANEL_B_EXPANDED_KEY, false);
  const [panelCExpanded, setPanelCExpanded] = usePersistentState<boolean>(PANEL_C_EXPANDED_KEY, false);
  const [panelAWeight, setPanelAWeight] = usePersistentState<number>(PANEL_A_WEIGHT_KEY, 1);
  const [panelBWeight, setPanelBWeight] = usePersistentState<number>(PANEL_B_WEIGHT_KEY, 1);
  const [panelCWeight, setPanelCWeight] = usePersistentState<number>(PANEL_C_WEIGHT_KEY, 1);
  const [leftNavExpanded, setLeftNavExpanded] = usePersistentState<boolean>(LEFT_NAV_EXPANDED_KEY, false);

  const [activePane, setActivePane] = useState<SlotId>('A');
  const [showSettings, setShowSettings] = useState(false);
  const [settingsInitialTab, setSettingsInitialTab] = useState<SettingsTab>();
  const handleCloseSettings = useCallback(() => {
    setShowSettings(false);
    setSettingsInitialTab(undefined);
  }, []);

  const [showSearchModal, setShowSearchModal] = useState(false);
  const [showAddStuff, setShowAddStuff] = useState(false);
  const [selectedNodes, setSelectedNodes] = useState<Set<number>>(new Set<number>());
  const [openTabsData, setOpenTabsData] = useState<Node[]>([]);
  const [nodesPanelRefresh, setNodesPanelRefresh] = useState(0);
  const [focusPanelRefresh, setFocusPanelRefresh] = useState(0);
  const [folderViewRefresh, setFolderViewRefresh] = useState(0);
  const [availableContexts, setAvailableContexts] = useState<ContextSummary[]>([]);
  const [activeContextId, setActiveContextId] = usePersistentState<number | null>(ACTIVE_CONTEXT_KEY, null);
  const [activeDimension, setActiveDimension] = usePersistentState<string | null>(ACTIVE_DIMENSION_KEY, null);
  const [browseContextFilters, setBrowseContextFilters] = useState<Record<SlotId, number | null>>({
    A: null,
    B: null,
    C: null,
  });
  const [browseDimensionFilters, setBrowseDimensionFilters] = useState<Record<SlotId, string | null>>({
    A: null,
    B: null,
    C: null,
  });
  const [highlightedPassage, setHighlightedPassage] = useState<{
    nodeId: number;
    nodeTitle: string;
    selectedText: string;
  } | null>(null);
  const [pendingNodes, setPendingNodes] = useState<PendingNode[]>([]);
  const openTabsRef = useRef<number[]>([]);

  useEffect(() => {
    setSlotA((prev) => normalizeSlotState(prev) ?? DEFAULT_SLOT_A);
    setSlotB((prev) => normalizeSlotState(prev));
    setSlotC((prev) => normalizeSlotState(prev));
  }, [setSlotA, setSlotB, setSlotC]);

  useEffect(() => {
    void (async () => {
      try {
        const response = await fetch('/api/contexts');
        const data = await response.json();
        if (response.ok && data.success) {
          setAvailableContexts(data.data || []);
        }
      } catch (error) {
        console.error('Failed to fetch contexts:', error);
      }
    })();
  }, [nodesPanelRefresh, folderViewRefresh]);

  const getSlotState = useCallback((slot: SlotId): SlotState | null => {
    switch (slot) {
      case 'A':
        return slotA;
      case 'B':
        return slotB;
      case 'C':
        return slotC;
    }
  }, [slotA, slotB, slotC]);

  const getSlotSetter = useCallback((slot: SlotId) => {
    switch (slot) {
      case 'A':
        return setSlotA;
      case 'B':
        return setSlotB;
      case 'C':
        return setSlotC;
    }
  }, [setSlotA, setSlotB, setSlotC]);

  const isPanelExpanded = useCallback((slot: SlotId) => {
    switch (slot) {
      case 'A':
        return panelAExpanded;
      case 'B':
        return panelBExpanded;
      case 'C':
        return panelCExpanded;
    }
  }, [panelAExpanded, panelBExpanded, panelCExpanded]);

  const setPanelExpanded = useCallback((slot: SlotId, expanded: boolean) => {
    switch (slot) {
      case 'A':
        setPanelAExpanded(expanded);
        break;
      case 'B':
        setPanelBExpanded(expanded);
        break;
      case 'C':
        setPanelCExpanded(expanded);
        break;
    }
  }, [setPanelAExpanded, setPanelBExpanded, setPanelCExpanded]);

  const getPanelWeight = useCallback((slot: SlotId) => {
    switch (slot) {
      case 'A':
        return panelAWeight;
      case 'B':
        return panelBWeight;
      case 'C':
        return panelCWeight;
    }
  }, [panelAWeight, panelBWeight, panelCWeight]);

  const setPanelWeight = useCallback((slot: SlotId, weight: number) => {
    const next = Math.max(0.6, Math.min(3, weight));
    switch (slot) {
      case 'A':
        setPanelAWeight(next);
        break;
      case 'B':
        setPanelBWeight(next);
        break;
      case 'C':
        setPanelCWeight(next);
        break;
    }
  }, [setPanelAWeight, setPanelBWeight, setPanelCWeight]);

  const isSlotEmpty = useCallback((slot: SlotId) => {
    return !getSlotState(slot);
  }, [getSlotState]);

  const { openTabs, activeTab } = useMemo(() => {
    const collectTabs = (state: SlotState | null): number[] =>
      state?.type === 'node' ? (state.nodeTabs ?? []) : [];

    const slotStates: Record<SlotId, SlotState | null> = { A: slotA, B: slotB, C: slotC };
    const activeSlotState = slotStates[activePane];
    const activeNodes = collectTabs(activeSlotState);
    const otherNodes = (['A', 'B', 'C'] as SlotId[])
      .filter((slot) => slot !== activePane)
      .flatMap((slot) => collectTabs(slotStates[slot]));
    const allNodes = [...new Set([...activeNodes, ...otherNodes])];

    let active: number | null = null;
    if (activeSlotState?.type === 'node' && activeSlotState.activeNodeTab != null) {
      active = activeSlotState.activeNodeTab;
    }
    if (active == null) {
      for (const slot of ['A', 'B', 'C'] as SlotId[]) {
        const state = slotStates[slot];
        if (slot !== activePane && state?.type === 'node' && state.activeNodeTab != null) {
          active = state.activeNodeTab;
          break;
        }
      }
    }
    if (active == null && allNodes.length > 0) {
      active = allNodes[0];
    }

    return { openTabs: allNodes, activeTab: active };
  }, [slotA, slotB, slotC, activePane]);

  const fetchOpenTabsData = async (tabIds: number[]) => {
    if (tabIds.length === 0) {
      setOpenTabsData([]);
      return;
    }

    try {
      const nodePromises = tabIds.map(async (id) => {
        const response = await fetch(`/api/nodes/${id}`);
        if (response.ok) {
          const data = await response.json();
          return data.node as Node;
        }
        return null;
      });

      const nodes = await Promise.all(nodePromises);
      const validNodes = nodes.filter((node): node is Node => Boolean(node)).map(node => ({
        id: node.id,
        title: node.title,
        link: node.link,
        source: node.source,
        dimensions: node.dimensions,
        created_at: node.created_at,
        updated_at: node.updated_at,
        chunk_status: node.chunk_status,
        metadata: node.metadata,
      }));
      setOpenTabsData(validNodes);
    } catch (error) {
      console.error('Failed to fetch tab data:', error);
      setOpenTabsData([]);
    }
  };

  const openTabsKey = openTabs.join(',');
  useEffect(() => {
    openTabsRef.current = openTabs;
    fetchOpenTabsData(openTabs);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [openTabsKey, focusPanelRefresh]);

  const handleRefreshAll = useCallback(() => {
    setNodesPanelRefresh(prev => prev + 1);
    setFolderViewRefresh(prev => prev + 1);
    setFocusPanelRefresh(prev => prev + 1);
  }, []);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setShowSearchModal(true);
      }

      if ((e.metaKey || e.ctrlKey) && e.key === '\\') {
        e.preventDefault();
        const collapsed = (['A', 'B', 'C'] as SlotId[]).find((slot) => !isPanelExpanded(slot));
        if (collapsed) {
          setPanelExpanded(collapsed, true);
          setActivePane(collapsed);
        }
      }

      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'r') {
        e.preventDefault();
        handleRefreshAll();
      }

      if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
        if (document.activeElement?.closest('[data-rah-app]')) {
          e.preventDefault();
          setShowAddStuff(true);
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [handleRefreshAll, isPanelExpanded, setPanelExpanded]);

  useEffect(() => {
    let eventSource: EventSource | null = null;

    try {
      eventSource = new EventSource('/api/events');

      eventSource.onmessage = (event) => {
        try {
          const data: DatabaseEvent = JSON.parse(event.data);

          switch (data.type) {
            case 'NODE_CREATED':
              setNodesPanelRefresh(prev => prev + 1);
              break;
            case 'NODE_UPDATED': {
              const currentOpenTabs = openTabsRef.current;
              const updatedNodeId = Number(data.data.nodeId);
              if (currentOpenTabs.includes(updatedNodeId)) {
                setFocusPanelRefresh(prev => prev + 1);
              }
              setNodesPanelRefresh(prev => prev + 1);
              break;
            }
            case 'NODE_DELETED':
              handleNodeDeleted(data.data.nodeId);
              setNodesPanelRefresh(prev => prev + 1);
              break;
            case 'EDGE_CREATED':
            case 'EDGE_DELETED': {
              const currentOpenTabsForEdge = openTabsRef.current;
              if (currentOpenTabsForEdge.includes(data.data.fromNodeId) ||
                  currentOpenTabsForEdge.includes(data.data.toNodeId)) {
                setFocusPanelRefresh(prev => prev + 1);
              }
              break;
            }
            case 'DIMENSION_UPDATED':
              setNodesPanelRefresh(prev => prev + 1);
              setFolderViewRefresh(prev => prev + 1);
              break;
            case 'HELPER_UPDATED':
            case 'AGENT_UPDATED':
              if (typeof window !== 'undefined') {
                window.dispatchEvent(new CustomEvent('agents:updated', { detail: data.data }));
              }
              break;
            case 'GUIDE_UPDATED':
              if (typeof window !== 'undefined') {
                window.dispatchEvent(new CustomEvent('guides:updated', { detail: data.data }));
                window.dispatchEvent(new CustomEvent('skills:updated', { detail: data.data }));
              }
              break;
            case 'QUICK_ADD_COMPLETED':
              if (data.data?.quickAddId) {
                setPendingNodes(prev => prev.filter(p => p.id !== data.data.quickAddId));
              }
              break;
            case 'QUICK_ADD_FAILED':
              if (data.data?.quickAddId) {
                setPendingNodes(prev => prev.map(p =>
                  p.id === data.data.quickAddId
                    ? { ...p, status: 'error' as const, error: data.data.error || 'Unknown error' }
                    : p
                ));
              }
              break;
          }
        } catch (error) {
          console.error('Failed to parse SSE event:', error);
        }
      };
    } catch (error) {
      console.error('Failed to establish SSE connection:', error);
    }

    return () => {
      eventSource?.close();
    };
  }, []);

  useEffect(() => {
    if (pendingNodes.length === 0) return;
    const interval = setInterval(() => {
      const now = Date.now();
      setPendingNodes(prev => prev.filter(p => {
        const age = now - p.submittedAt;
        if (p.status === 'processing' && age > 90_000) return false;
        if (p.status === 'error' && age > 120_000) return false;
        return true;
      }));
    }, 5000);
    return () => clearInterval(interval);
  }, [pendingNodes.length]);

  const setSingletonPaneInSlot = useCallback((slot: SlotId, paneType: Exclude<PaneType, 'node'>) => {
    setPanelExpanded(slot, true);
    getSlotSetter(slot)({ type: paneType });
  }, [getSlotSetter, setPanelExpanded]);

  const focusPaneIfOpen = useCallback((paneType: Exclude<PaneType, 'node'>): SlotId | null => {
    for (const slot of ['A', 'B', 'C'] as SlotId[]) {
      if (getSlotState(slot)?.type === paneType) {
        setActivePane(slot);
        return slot;
      }
    }
    return null;
  }, [getSlotState]);

  const openPaneSingleton = useCallback((paneType: Exclude<PaneType, 'node'>, preferredSlot?: SlotId) => {
    const existing = focusPaneIfOpen(paneType);
    if (existing) return existing;

    const orderedSlots = preferredSlot
      ? [preferredSlot, ...(['A', 'B', 'C'] as SlotId[]).filter((slot) => slot !== preferredSlot)]
      : (['A', 'B', 'C'] as SlotId[]);

    const expandedEmpty = orderedSlots.find((slot) => isPanelExpanded(slot) && isSlotEmpty(slot));
    if (expandedEmpty) {
      setSingletonPaneInSlot(expandedEmpty, paneType);
      setActivePane(expandedEmpty);
      return expandedEmpty;
    }

    const collapsedFree = orderedSlots.find((slot) => !isPanelExpanded(slot));
    if (collapsedFree) {
      setPanelExpanded(collapsedFree, true);
      setSingletonPaneInSlot(collapsedFree, paneType);
      setActivePane(collapsedFree);
      return collapsedFree;
    }

    const activeHasContext = isPanelExpanded(activePane) && !isSlotEmpty(activePane);
    const replacementTarget = activeHasContext
      ? activePane
      : orderedSlots.find((slot) => isPanelExpanded(slot) && !isSlotEmpty(slot))
        ?? orderedSlots.find((slot) => isPanelExpanded(slot))
        ?? orderedSlots[0];

    setSingletonPaneInSlot(replacementTarget, paneType);
    setActivePane(replacementTarget);
    return replacementTarget;
  }, [activePane, focusPaneIfOpen, isPanelExpanded, isSlotEmpty, setPanelExpanded, setSingletonPaneInSlot]);

  const addNodeTabToSlot = useCallback((slot: SlotId, nodeId: number) => {
    setPanelExpanded(slot, true);
    const state = getSlotState(slot);
    if (state?.type === 'node') {
      const currentTabs = state.nodeTabs || [];
      const newTabs = currentTabs.includes(nodeId) ? currentTabs : [...currentTabs, nodeId];
      getSlotSetter(slot)({
        ...state,
        nodeTabs: newTabs,
        activeNodeTab: nodeId,
      });
      return;
    }

    getSlotSetter(slot)({
      type: 'node',
      nodeTabs: [nodeId],
      activeNodeTab: nodeId,
    });
  }, [getSlotSetter, getSlotState, setPanelExpanded]);

  const openNodeFromSlot = useCallback((nodeId: number, fromSlot?: SlotId) => {
    const targetOrder: SlotId[] = fromSlot === 'A'
      ? ['B', 'C']
      : fromSlot === 'B'
        ? ['C', 'A']
        : fromSlot === 'C'
          ? ['B', 'A']
          : ['B', 'C', 'A'];

    for (const slot of ['A', 'B', 'C'] as SlotId[]) {
      const state = getSlotState(slot);
      if (state?.type === 'node' && (state.nodeTabs || []).includes(nodeId)) {
        getSlotSetter(slot)({ ...state, activeNodeTab: nodeId });
        setSelectedNodes(new Set([nodeId]));
        setActivePane(slot);
        return;
      }
    }

    const emptyTarget = targetOrder.find((slot) => slot !== fromSlot && isSlotEmpty(slot));
    const fallbackTarget = targetOrder.find((slot) => slot !== fromSlot) ?? 'B';
    const target = emptyTarget ?? fallbackTarget;

    addNodeTabToSlot(target, nodeId);
    setSelectedNodes(new Set([nodeId]));
    setActivePane(target);
  }, [addNodeTabToSlot, getSlotSetter, getSlotState, isSlotEmpty]);

  const handleNodeSelect = useCallback((nodeId: number, _multiSelect: boolean) => {
    openNodeFromSlot(nodeId);
  }, [openNodeFromSlot]);

  const handleTabSelect = useCallback((slot: SlotId, tabId: number) => {
    const state = getSlotState(slot);
    if (state?.type !== 'node') return;
    getSlotSetter(slot)({ ...state, activeNodeTab: tabId });
    setSelectedNodes(new Set([tabId]));
    setActivePane(slot);
  }, [getSlotSetter, getSlotState]);

  const handleCloseTab = useCallback((slot: SlotId, tabId: number) => {
    const state = getSlotState(slot);
    if (state?.type !== 'node') return;

    const currentTabs = state.nodeTabs || [];
    const newTabs = currentTabs.filter(id => id !== tabId);
    const newActiveTab = state.activeNodeTab === tabId
      ? (newTabs.length > 0 ? newTabs[Math.min(currentTabs.indexOf(tabId), newTabs.length - 1)] : null)
      : state.activeNodeTab ?? null;

    if (newTabs.length === 0) {
      getSlotSetter(slot)(null);
      return;
    }

    getSlotSetter(slot)({
      ...state,
      nodeTabs: newTabs,
      activeNodeTab: newActiveTab,
    });
  }, [getSlotSetter, getSlotState]);

  const handleNodeDeleted = useCallback((nodeId: number) => {
    for (const slot of ['A', 'B', 'C'] as SlotId[]) {
      handleCloseTab(slot, nodeId);
    }
  }, [handleCloseTab]);

  const handleReorderTabs = useCallback((slot: SlotId, fromIndex: number, toIndex: number) => {
    const state = getSlotState(slot);
    if (state?.type !== 'node') return;
    const currentTabs = state.nodeTabs || [];
    if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= currentTabs.length || toIndex >= currentTabs.length) {
      return;
    }
    const updated = [...currentTabs];
    const [moved] = updated.splice(fromIndex, 1);
    updated.splice(toIndex, 0, moved);
    getSlotSetter(slot)({ ...state, nodeTabs: updated });
  }, [getSlotSetter, getSlotState]);

  const handleFolderViewDataChanged = useCallback(() => {
    setFolderViewRefresh(prev => prev + 1);
    setNodesPanelRefresh(prev => prev + 1);
  }, []);

  const handleNodeOpenFromDimensions = useCallback((nodeId: number) => {
    openNodeFromSlot(nodeId, 'A');
  }, [openNodeFromSlot]);

  const handleContextSelect = useCallback((slot: SlotId, contextId: number | null, _contextName?: string | null) => {
    setBrowseContextFilters((prev) => ({ ...prev, [slot]: contextId }));
    setActiveContextId(contextId);

    if (contextId == null) return;

    setPanelExpanded(slot, true);
    getSlotSetter(slot)({ type: 'views' });
    setActivePane(slot);
  }, [getSlotSetter, setActiveContextId, setPanelExpanded]);

  const handleDimensionPaneSelect = useCallback((slot: SlotId, dimensionName: string | null) => {
    setBrowseDimensionFilters((prev) => ({ ...prev, [slot]: dimensionName }));
    setActiveDimension(dimensionName);

    if (!dimensionName) return;

    setPanelExpanded(slot, true);
    getSlotSetter(slot)({ type: 'views' });
    setActivePane(slot);
  }, [getSlotSetter, setActiveDimension, setPanelExpanded]);

  const handleQuickAddSubmit = useCallback(async ({ input, mode, description }: { input: string; mode: 'link' | 'text'; description?: string }) => {
    try {
      const response = await fetch('/api/quick-add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ input, mode, description, contextId: activeContextId }),
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || 'Failed to submit Quick Add');
      }

      const data = await response.json();
      const result = data.result as { id: string; inputType: string } | undefined;

      if (result?.id) {
        setPendingNodes(prev => [{
          id: result.id,
          input: input.trim(),
          inputType: result.inputType || 'note',
          submittedAt: Date.now(),
          status: 'processing',
        }, ...prev]);
      }

      openPaneSingleton('views', 'A');
      setShowAddStuff(false);
    } catch (error) {
      console.error('[ThreePanelLayout] Quick Add error:', error);
    }
  }, [activeContextId, openPaneSingleton]);

  const handleCloseSlotA = useCallback(() => {
    setSlotA(null);
    setPanelAExpanded(false);
    setActivePane(panelBExpanded ? 'B' : panelCExpanded ? 'C' : 'A');
  }, [panelBExpanded, panelCExpanded, setPanelAExpanded, setSlotA]);

  const handleCloseSlotB = useCallback(() => {
    setSlotB(null);
    setPanelBExpanded(false);
    setActivePane(panelAExpanded ? 'A' : panelCExpanded ? 'C' : 'B');
  }, [panelAExpanded, panelCExpanded, setPanelBExpanded, setSlotB]);

  const handleCloseSlotC = useCallback(() => {
    setSlotC(null);
    setPanelCExpanded(false);
    setActivePane(panelBExpanded ? 'B' : panelAExpanded ? 'A' : 'C');
  }, [panelAExpanded, panelBExpanded, setPanelCExpanded, setSlotC]);

  const handleSlotAction = useCallback((slot: SlotId, action: PaneAction) => {
    switch (action.type) {
      case 'switch-pane-type':
        if (action.paneType !== 'node') {
          setSingletonPaneInSlot(slot, action.paneType);
          setActivePane(slot);
        }
        break;
      case 'open-context':
        handleContextSelect(action.targetSlot ?? slot, action.contextId, action.contextName);
        break;
      case 'open-dimension':
        setBrowseDimensionFilters((prev) => ({ ...prev, [action.targetSlot ?? slot]: action.dimension }));
        setActiveDimension(action.dimension);
        setSingletonPaneInSlot(action.targetSlot ?? slot, 'views');
        setActivePane(action.targetSlot ?? slot);
        break;
      case 'open-node':
        openNodeFromSlot(action.nodeId, slot);
        break;
    }
  }, [handleContextSelect, openNodeFromSlot, setActiveDimension, setSingletonPaneInSlot]);

  const handleSearchNodeSelect = useCallback((nodeId: number) => {
    handleNodeSelect(nodeId, false);
    setShowSearchModal(false);
  }, [handleNodeSelect]);

  const handleSwapPanes = useCallback((source: SlotId, target: SlotId) => {
    if (source === target) return;

    const sourceState = getSlotState(source);
    const targetState = getSlotState(target);
    const sourceExpanded = isPanelExpanded(source);
    const targetExpanded = isPanelExpanded(target);

    getSlotSetter(source)(targetState);
    getSlotSetter(target)(sourceState);
    setPanelExpanded(source, targetExpanded);
    setPanelExpanded(target, sourceExpanded);

    const sourceWeight = getPanelWeight(source);
    const targetWeight = getPanelWeight(target);
    setPanelWeight(source, targetWeight);
    setPanelWeight(target, sourceWeight);

    if (activePane === source) setActivePane(target);
    else if (activePane === target) setActivePane(source);
  }, [activePane, getPanelWeight, getSlotSetter, getSlotState, isPanelExpanded, setPanelExpanded, setPanelWeight]);

  const [dragOverSlot, setDragOverSlot] = useState<SlotId | null>(null);

  const handleSlotDragOver = useCallback((e: React.DragEvent, slot: SlotId) => {
    if (e.dataTransfer.types.includes('application/x-rah-pane') ||
        e.dataTransfer.types.includes('application/x-rah-tab') ||
        e.dataTransfer.types.includes('application/node-info')) {
      e.preventDefault();
      e.dataTransfer.dropEffect = e.dataTransfer.types.includes('application/x-rah-pane') ? 'move' : 'copy';
      setDragOverSlot(slot);
    }
  }, []);

  const handleSlotDragLeave = useCallback(() => {
    setDragOverSlot(null);
  }, []);

  const handleSlotDrop = useCallback((e: React.DragEvent, targetSlot: SlotId) => {
    setDragOverSlot(null);

    const paneData = e.dataTransfer.getData('application/x-rah-pane');
    if (paneData) {
      const sourceSlot = paneData as SlotId;
      if (sourceSlot !== targetSlot) handleSwapPanes(sourceSlot, targetSlot);
      return;
    }

    let tabData = e.dataTransfer.getData('application/x-rah-tab');
    if (!tabData) {
      tabData = e.dataTransfer.getData('application/node-info');
    }
    if (!tabData) return;

    try {
      const parsed = JSON.parse(tabData);
      const nodeId = parsed.id as number | undefined;
      const sourceSlot = parsed.sourceSlot as SlotId | undefined;
      if (typeof nodeId !== 'number') return;

      if (sourceSlot && sourceSlot === targetSlot) {
        handleTabSelect(targetSlot, nodeId);
        return;
      }

      if (sourceSlot) {
        handleCloseTab(sourceSlot, nodeId);
      }

      addNodeTabToSlot(targetSlot, nodeId);
      setActivePane(targetSlot);
    } catch (err) {
      console.error('Failed to parse dropped tab data:', err);
    }
  }, [addNodeTabToSlot, handleCloseTab, handleSwapPanes, handleTabSelect]);

  const handleResizePanels = useCallback((left: SlotId, right: SlotId, clientX: number) => {
    const leftEl = panelRefs.current[left];
    const rightEl = panelRefs.current[right];
    if (!leftEl || !rightEl) return;

    const leftRect = leftEl.getBoundingClientRect();
    const rightRect = rightEl.getBoundingClientRect();
    const combinedWidth = leftRect.width + rightRect.width;
    if (combinedWidth <= 0) return;

    const minWidth = 220;
    const rawLeftWidth = clientX - leftRect.left;
    const nextLeftWidth = Math.max(minWidth, Math.min(combinedWidth - minWidth, rawLeftWidth));
    const nextRightWidth = combinedWidth - nextLeftWidth;
    const totalWeight = getPanelWeight(left) + getPanelWeight(right);

    setPanelWeight(left, (nextLeftWidth / combinedWidth) * totalWeight);
    setPanelWeight(right, (nextRightWidth / combinedWidth) * totalWeight);
  }, [getPanelWeight, setPanelWeight]);

  const renderSlot = (slot: SlotId, state: SlotState) => {
    const isActive = activePane === slot;
    const onCollapse = slot === 'A' ? handleCloseSlotA : slot === 'B' ? handleCloseSlotB : handleCloseSlotC;

    switch (state.type) {
      case 'node':
        return (
          <NodePane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            openTabs={state.nodeTabs || []}
            activeTab={state.activeNodeTab || null}
            onTabSelect={(tabId) => handleTabSelect(slot, tabId)}
            onTabClose={(tabId) => handleCloseTab(slot, tabId)}
            onNodeClick={(nodeId) => {
              addNodeTabToSlot(slot, nodeId);
              setSelectedNodes(new Set([nodeId]));
              setActivePane(slot);
            }}
            onReorderTabs={(fromIndex, toIndex) => handleReorderTabs(slot, fromIndex, toIndex)}
            refreshTrigger={focusPanelRefresh}
            onOpenInOtherSlot={(nodeId) => openNodeFromSlot(nodeId, slot)}
            onTextSelect={(nodeId, nodeTitle, text) => {
              setHighlightedPassage({ nodeId, nodeTitle, selectedText: text });
            }}
            highlightedPassage={highlightedPassage}
          />
        );

      case 'contexts':
        return (
          <ContextsPane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            onContextSelect={(contextId, contextName) => handleContextSelect(slot, contextId, contextName)}
          />
        );

      case 'dimensions':
        return (
          <DimensionsPane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            onNodeOpen={handleNodeOpenFromDimensions}
            refreshToken={folderViewRefresh}
            onDataChanged={handleFolderViewDataChanged}
            onDimensionSelect={(dimension) => handleDimensionPaneSelect(slot, dimension)}
          />
        );

      case 'map':
        return (
          <MapPane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            onNodeClick={(nodeId) => openNodeFromSlot(nodeId, slot)}
            activeTabId={activeTab}
          />
        );

      case 'views':
        return (
          <ViewsPane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            onNodeClick={(nodeId) => {
              openNodeFromSlot(nodeId, slot);
              setActivePane(slot);
            }}
            onNodeOpenInOtherPane={(nodeId) => openNodeFromSlot(nodeId, slot)}
            refreshToken={nodesPanelRefresh}
            pendingNodes={pendingNodes}
            onDismissPending={(id) => setPendingNodes(prev => prev.filter(p => p.id !== id))}
            externalContextFilterId={browseContextFilters[slot]}
            externalDimensionFilter={browseDimensionFilters[slot]}
            onContextFilterSelect={(contextId) => {
              setBrowseContextFilters((prev) => ({ ...prev, [slot]: contextId }));
              setActiveContextId(contextId);
            }}
            onClearExternalContextFilter={() => {
              setBrowseContextFilters((prev) => ({ ...prev, [slot]: null }));
              setActiveContextId(null);
            }}
            onClearExternalDimensionFilter={() => {
              setBrowseDimensionFilters((prev) => ({ ...prev, [slot]: null }));
              setActiveDimension(null);
            }}
          />
        );

      case 'table':
        return (
          <TablePane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
            onNodeClick={(nodeId) => {
              openNodeFromSlot(nodeId, slot);
              setActivePane(slot);
            }}
            refreshToken={nodesPanelRefresh}
          />
        );

      case 'skills':
        return (
          <SkillsPane
            slot={slot}
            isActive={isActive}
            onPaneAction={(action) => handleSlotAction(slot, action)}
            onCollapse={onCollapse}
            onSwapPanes={handleSwapPanes}
          />
        );

      default:
        return null;
    }
  };

  const slotStates: Record<SlotId, SlotState | null> = { A: slotA, B: slotB, C: slotC };
  const panelExpandedState: Record<SlotId, boolean> = { A: panelAExpanded, B: panelBExpanded, C: panelCExpanded };

  const getSlotContainerStyle = (slot: SlotId) => {
    const state = slotStates[slot];
    const expanded = panelExpandedState[slot];
    const hasContent = Boolean(state);
    const weight = getPanelWeight(slot);

    return {
      flex: expanded ? `${weight} ${weight} 0` : '0 0 44px',
      minWidth: expanded ? 0 : '44px',
      overflow: 'hidden',
      display: 'flex',
      flexDirection: 'column' as const,
      background: expanded ? 'var(--rah-bg-surface)' : 'var(--rah-bg-subtle)',
      borderRadius: '10px',
      border: expanded && hasContent ? '1px solid transparent' : '1px dashed var(--rah-border)',
      outline: dragOverSlot === slot ? '2px dashed var(--rah-accent-green)' : 'none',
      outlineOffset: '-4px',
      transition: 'outline 0.15s ease, background 0.15s ease',
    };
  };

  const renderCollapsedPanel = (slot: SlotId) => (
    <div
      style={{
        flex: 1,
        minHeight: 0,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        padding: '8px 0',
      }}
    >
      <button
        type="button"
        onClick={() => {
          setPanelExpanded(slot, true);
          setActivePane(slot);
        }}
        title="Expand panel"
        style={{
          width: '28px',
          height: '28px',
          borderRadius: '8px',
          border: '1px solid var(--rah-border-strong)',
          background: 'var(--rah-bg-elevated)',
          color: 'var(--rah-text-secondary)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          cursor: 'pointer',
        }}
      >
        <PanelLeftOpen size={14} />
      </button>
    </div>
  );

  const renderExpandedEmptyPanel = (slot: SlotId) => (
    <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
      <div
        draggable
        onDragStart={(e) => {
          e.dataTransfer.setData('application/x-rah-pane', slot);
          e.dataTransfer.effectAllowed = 'move';
        }}
        style={{
          minHeight: '48px',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          padding: '8px 12px',
          cursor: 'grab',
        }}
      >
        <GripVertical size={14} color="var(--rah-text-muted)" />
        <button
          type="button"
          onClick={() => {
            getSlotSetter(slot)(null);
            setPanelExpanded(slot, false);
          }}
          title="Collapse panel"
          style={{
            width: '28px',
            height: '28px',
            borderRadius: '8px',
            border: '1px solid var(--rah-border-strong)',
            background: 'var(--rah-bg-elevated)',
            color: 'var(--rah-text-secondary)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            cursor: 'pointer',
          }}
        >
          <X size={14} />
        </button>
      </div>
      <div
        style={{
          flex: 1,
          minHeight: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'var(--rah-text-muted)',
          fontSize: '13px',
        }}
      >
        Select a pane from the nav
      </div>
    </div>
  );

  return (
    <div
      data-rah-app
      style={{
        display: 'flex',
        height: '100vh',
        width: '100vw',
        background: 'var(--rah-bg-base)',
        overflow: 'hidden',
      }}
    >
      <LeftToolbar
        onSearchClick={() => setShowSearchModal(true)}
        onAddStuffClick={() => setShowAddStuff(true)}
        onSettingsClick={() => {
          setSettingsInitialTab(undefined);
          setShowSettings(true);
        }}
        onPaneTypeClick={openPaneSingleton}
        isExpanded={leftNavExpanded}
        onToggleExpanded={() => setLeftNavExpanded(prev => !prev)}
        openTabTypes={new Set([slotA?.type, slotB?.type, slotC?.type].filter((t): t is PaneType => t != null))}
        activeTabType={getSlotState(activePane)?.type ?? null}
        onRefreshClick={handleRefreshAll}
        theme={theme}
        onThemeToggle={toggleTheme}
        contexts={availableContexts}
        onContextQuickSelect={(contextId, contextName) => {
          const target = openPaneSingleton('views', 'A');
          setBrowseContextFilters((prev) => ({ ...prev, [target]: contextId }));
          setBrowseDimensionFilters((prev) => ({ ...prev, [target]: null }));
          setActiveContextId(contextId);
          setActivePane(target);
          void contextName;
        }}
      />

      <div
        ref={containerRef}
        style={{ flex: 1, display: 'flex', overflow: 'hidden', padding: '8px', gap: '4px' }}
      >
        {(['A', 'B', 'C'] as SlotId[]).flatMap((slot, index, allSlots) => {
          const state = slotStates[slot];
          const expanded = panelExpandedState[slot];
          const items: React.ReactNode[] = [];

          items.push(
            <div
              key={`panel-${slot}`}
              ref={(node) => {
                panelRefs.current[slot] = node;
              }}
              onClick={() => setActivePane(slot)}
              onDragOver={(e) => handleSlotDragOver(e, slot)}
              onDragLeave={handleSlotDragLeave}
              onDrop={(e) => handleSlotDrop(e, slot)}
              style={getSlotContainerStyle(slot)}
            >
              {!expanded ? renderCollapsedPanel(slot) : state ? renderSlot(slot, state) : renderExpandedEmptyPanel(slot)}
            </div>
          );

          const nextSlot = allSlots[index + 1];
          if (nextSlot && expanded && panelExpandedState[nextSlot]) {
            items.push(
              <SplitHandle
                key={`split-${slot}-${nextSlot}`}
                onResize={(clientX) => handleResizePanels(slot, nextSlot, clientX)}
                title="Drag to resize panels"
              />
            );
          }

          return items;
        })}
      </div>

      <SearchModal
        isOpen={showSearchModal}
        onClose={() => setShowSearchModal(false)}
        onNodeSelect={handleSearchNodeSelect}
        existingFilters={[]}
      />

      <SettingsModal
        isOpen={showSettings}
        onClose={handleCloseSettings}
        initialTab={settingsInitialTab}
      />

      <QuickAddInput
        isOpen={showAddStuff}
        onClose={() => setShowAddStuff(false)}
        onSubmit={handleQuickAddSubmit}
      />
    </div>
  );
}
