'use client';

import { useState, useEffect, useCallback, useRef, useMemo, startTransition } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import Link from 'next/link';
import { Search, Settings, Menu, X, FolderInput } from 'lucide-react';
import ActivityBar from './ActivityBar';
import TitlebarRow from './TitlebarRow';
import Panel from './Panel';
import FileTree from './FileTree';
import Logo from './Logo';
import AskFab from './AskFab';
import SyncPopover from './panels/SyncPopover';
import KeyboardShortcuts from './KeyboardShortcuts';
import ChangesBanner from './changes/ChangesBanner';
import SpaceInitToast from './SpaceInitToast';
import OrganizeToast from './OrganizeToast';
import { getMobileSyncLabel, MobileSyncDot, useSyncStatus } from './SyncStatusBar';
import { FileNode } from '@/lib/types';
import type { MindSystemSlot } from '@/lib/mind-system';
import { useLocale } from '@/lib/stores/locale-store';
import { telemetry } from '@/lib/telemetry';
import dynamic from 'next/dynamic';

const SearchModal = dynamic(() => import('./SearchModal'), { ssr: false });
const AskModal = dynamic(() => import('./AskModal'), { ssr: false });
const SettingsModal = dynamic(() => import('./SettingsModal'), { ssr: false });
const CreateSpaceModal = dynamic(() => import('./CreateSpaceModal'), { ssr: false });
const ImportModal = dynamic(() => import('./ImportModal'), { ssr: false });
import McpStoreInit from '@/lib/stores/McpStoreInit';
import WalkthroughInit from '@/lib/stores/WalkthroughInit';
import '@/lib/renderers/index'; // client-side renderer registration source of truth
import { useLeftPanel } from '@/hooks/useLeftPanel';
import { useAskPanel } from '@/hooks/useAskPanel';
import { useAiOrganize } from '@/hooks/useAiOrganize';
import { useInboxOrganizeController } from '@/hooks/useInboxOrganizeController';
import { InboxOrganizeProvider } from '@/components/inbox/InboxOrganizeContext';
import { quickDropToInbox } from '@/lib/inbox-upload';
import {
  getActiveLeftPanel,
  getEffectivePanelMaximized,
  getPendingRoutePanel,
  getRailActivePanel,
  getRailPanelClickDecision,
  getRouteControlledPanel,
  isNeutralContentRoute,
  recoverStaleRoutePanel,
  type PanelId,
  type PendingRouteNav,
  type RoutePanelId,
} from '@/lib/navigation-panel';
import type { Tab } from './settings/types';
import { RIGHT_AGENT_DETAIL_PANEL, getLeftPanelWidth } from '@/lib/config/panel-sizes';

const noop = () => {};
const SYNC_POPOVER_ID = 'sync-popover';

const SearchPanel = dynamic(() => import('./panels/SearchPanel'), { ssr: false });
const CapturePanel = dynamic(() => import('./panels/CapturePanel'), { ssr: false });
function AgentsPanelLoading() {
  const { t } = useLocale();
  const p = t.panels.agents;
  return (
    <div className="flex h-full flex-col">
      <div className="h-[46px] shrink-0 border-b border-border px-4 flex items-center">
        <span className="text-sm font-medium text-foreground">{p.title}</span>
      </div>
      <div className="flex flex-1 items-center justify-center text-xs text-muted-foreground">
        {p.acpLoading}
      </div>
    </div>
  );
}

const AgentsPanel = dynamic(() => import('./panels/AgentsPanel'), {
  ssr: false,
  loading: AgentsPanelLoading,
});
const DiscoverPanel = dynamic(() => import('./panels/DiscoverPanel'), { ssr: false });
const EchoPanel = dynamic(() => import('./panels/EchoPanel'), { ssr: false });
const WorkflowsPanel = dynamic(() => import('./panels/WorkflowsPanel'), { ssr: false });
const RightAskPanel = dynamic(() => import('./RightAskPanel'), { ssr: false });
const RightAgentDetailPanel = dynamic(() => import('./RightAgentDetailPanel'), { ssr: false });

const RIGHT_AGENT_DETAIL_DEFAULT_WIDTH = RIGHT_AGENT_DETAIL_PANEL.DEFAULT;
const RIGHT_AGENT_DETAIL_MIN_WIDTH = RIGHT_AGENT_DETAIL_PANEL.MIN;
const RIGHT_AGENT_DETAIL_MAX_WIDTH = RIGHT_AGENT_DETAIL_PANEL.MAX_ABS;

function collectDirPaths(nodes: FileNode[], prefix = ''): string[] {
  const result: string[] = [];
  for (const n of nodes) {
    if (n.type === 'directory' && !n.name.startsWith('.')) {
      const p = prefix ? `${prefix}/${n.name}` : n.name;
      result.push(p);
      if (n.children) result.push(...collectDirPaths(n.children, p));
    }
  }
  return result;
}

interface SidebarLayoutProps {
  fileTree: FileNode[];
  mindSystemSlots: MindSystemSlot[];
  children: React.ReactNode;
}

export default function SidebarLayout({ fileTree, mindSystemSlots, children }: SidebarLayoutProps) {
  // ── Left panel state (extracted hook) ──
  const lp = useLeftPanel();

  // ── Right Ask AI panel state (extracted hook) ──
  const ap = useAskPanel();

  // ── Settings modal ──
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [settingsTab, setSettingsTab] = useState<Tab | undefined>(undefined);

  // ── Sync popover ──
  const [syncPopoverOpen, setSyncPopoverOpen] = useState(false);
  const [syncAnchorRect, setSyncAnchorRect] = useState<DOMRect | null>(null);

  // ── Agent MCP detail (right dock, does not replace left Agents list) ──
  const [agentDetailKey, setAgentDetailKey] = useState<string | null>(null);
  const [agentDetailWidth, setAgentDetailWidth] = useState(() => {
    if (typeof window === 'undefined') return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
    try {
      const stored = localStorage.getItem('right-agent-detail-panel-width');
      if (stored) {
        const w = parseInt(stored, 10);
        if (w >= RIGHT_AGENT_DETAIL_MIN_WIDTH && w <= RIGHT_AGENT_DETAIL_MAX_WIDTH) return w;
      }
    } catch { /* ignore */ }
    return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
  });

  // ── AI Organize (lifted from ImportModal so toast shares state) ──
  const aiOrganize = useAiOrganize();
  const [organizeToastVisible, setOrganizeToastVisible] = useState(false);

  // Show toast whenever organize is active
  useEffect(() => {
    if (aiOrganize.phase === 'organizing' || aiOrganize.phase === 'done' || aiOrganize.phase === 'error') {
      setOrganizeToastVisible(true);
    }
  }, [aiOrganize.phase]);

  const handleOrganizeToastDismiss = useCallback(() => {
    setOrganizeToastVisible(false);
    if (aiOrganize.phase !== 'organizing') {
      aiOrganize.reset();
    } else {
      aiOrganize.abort();
      aiOrganize.reset();
    }
  }, [aiOrganize]);

  const handleHistoryUpdate = useCallback(() => {
    window.dispatchEvent(new Event('mindos:organize-history-update'));
  }, []);

  // ── Import modal state ──
  const [importModalOpen, setImportModalOpen] = useState(false);
  const [importDefaultSpace, setImportDefaultSpace] = useState<string | undefined>(undefined);
  const [importInitialFiles, setImportInitialFiles] = useState<File[] | undefined>(undefined);
  const [dragOverlay, setDragOverlay] = useState(false);
  const dragCounterRef = useRef(0);

  const handleOpenImport = useCallback((space?: string) => {
    setImportDefaultSpace(space);
    setImportInitialFiles(undefined);
    setImportModalOpen(true);
  }, []);

  const handleCloseImport = useCallback(() => {
    setImportModalOpen(false);
    setImportDefaultSpace(undefined);
    setImportInitialFiles(undefined);
  }, []);

  // ── Mobile state ──
  const [mobileOpen, setMobileOpen] = useState(false);
  const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
  const [mobileAskOpen, setMobileAskOpen] = useState(false);

  const { t } = useLocale();
  const inboxOrganize = useInboxOrganizeController({ aiOrganize, labels: t.inbox });
  const router = useRouter();
  const pathname = usePathname();
  const dirPaths = useMemo(() => {
    const stop = telemetry.startTimer('sidebar.collect_dir_paths');
    const paths = collectDirPaths(fileTree);
    stop({ nodeCount: fileTree.length, pathCount: paths.length });
    return paths;
  }, [fileTree]);
  const { status: syncStatus, error: syncStatusError, stale: syncStatusStale, fetchStatus: syncStatusRefresh } = useSyncStatus();
  const mobileSyncLabel = getMobileSyncLabel({
    status: syncStatus,
    stale: syncStatusStale,
    loadError: syncStatusError,
    syncT: t.sidebar?.sync as Record<string, unknown> | undefined,
    prefix: t.sidebar?.syncLabel ?? 'Sync',
  });

  const currentFile = pathname.startsWith('/view/')
    ? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
    : undefined;

  // ── Optimistic rail navigation ──
  // A rail click toward another module records a pending target. Until the
  // route commits, the pending target IS the active panel: the panel content
  // and rail highlight switch in the click's render, and the local/route
  // mismatch can't flip any derived state back and forth (the rail-click
  // flicker). Any pathname change invalidates the pending entry in-render.
  const [pendingNav, setPendingNav] = useState<PendingRouteNav | null>(null);
  const pendingRoutePanel = getPendingRoutePanel(pathname, pendingNav);
  const activeLeftPanel = pendingRoutePanel ?? getActiveLeftPanel(pathname, lp.activePanel);
  const railActivePanel = pendingRoutePanel ?? getRailActivePanel(pathname, lp.activePanel);
  const agentDockOpen = agentDetailKey !== null && activeLeftPanel === 'agents';
  const panelOpen = activeLeftPanel !== null;
  const effectivePanelMaximized = getEffectivePanelMaximized(activeLeftPanel, lp.activePanel, lp.panelMaximized);
  // One width for all panels (user-resized wins, per-panel default otherwise).
  // Deriving width from WHICH state controlled the panel made every
  // navigation transition animate through 2-4 widths — the flicker.
  const effectivePanelWidth = getLeftPanelWidth(activeLeftPanel, lp.panelWidth);

  // Drop the pending entry once any route commits (the derivation above
  // already ignores it from that render on — this is just state hygiene).
  useEffect(() => {
    setPendingNav((prev) => (prev && prev.fromPathname !== pathname ? null : prev));
  }, [pathname]);

  // Auto-exit Ask panel maximize when navigating to a different page
  // or when left panel opens (content needs to be visible).
  // NOTE: the useEffect serves as a fallback for programmatic navigation;
  // the primary (synchronous) exit happens in exitAskMaximized() below.
  useEffect(() => {
    if (ap.askMaximized) ap.toggleAskMaximized();
  // Only react to pathname / left-panel changes, not askMaximized changes
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pathname, panelOpen]);

  // Synchronous helper — call in click handlers that activate content pages,
  // so the Ask panel exits maximized in the same render (no flicker).
  const exitAskMaximized = useCallback(() => {
    if (ap.askMaximized) ap.toggleAskMaximized();
  }, [ap.askMaximized, ap.toggleAskMaximized]);

  // Close right Ask panel when entering home page — home has its own embedded Ask
  useEffect(() => {
    if (pathname === '/') {
      if (ap.askPanelOpen) ap.closeAskPanel();
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pathname]);

  const [mountedPanels, setMountedPanels] = useState<Set<PanelId>>(() => new Set());
  const [rightAskMounted, setRightAskMounted] = useState(false);
  const [rightAgentDetailMounted, setRightAgentDetailMounted] = useState(false);
  const [mobileSearchMounted, setMobileSearchMounted] = useState(false);
  const [desktopAskPopupMounted, setDesktopAskPopupMounted] = useState(false);
  const [mobileAskMounted, setMobileAskMounted] = useState(false);
  const [settingsMounted, setSettingsMounted] = useState(false);
  const [importMounted, setImportMounted] = useState(false);

  useEffect(() => {
    const active = activeLeftPanel;
    if (!active || active === 'files') return;
    setMountedPanels((prev) => {
      if (prev.has(active)) return prev;
      const next = new Set(prev);
      next.add(active);
      return next;
    });
  }, [activeLeftPanel]);

  useEffect(() => { if (ap.askPanelOpen) setRightAskMounted(true); }, [ap.askPanelOpen]);
  useEffect(() => { if (agentDockOpen) setRightAgentDetailMounted(true); }, [agentDockOpen]);
  useEffect(() => { if (mobileSearchOpen) setMobileSearchMounted(true); }, [mobileSearchOpen]);
  useEffect(() => { if (ap.desktopAskPopupOpen) setDesktopAskPopupMounted(true); }, [ap.desktopAskPopupOpen]);
  useEffect(() => { if (mobileAskOpen) setMobileAskMounted(true); }, [mobileAskOpen]);
  useEffect(() => { if (settingsOpen) setSettingsMounted(true); }, [settingsOpen]);
  useEffect(() => { if (importModalOpen) setImportMounted(true); }, [importModalOpen]);

  const isPanelMounted = useCallback(
    (panel: PanelId) => activeLeftPanel === panel || mountedPanels.has(panel),
    [activeLeftPanel, mountedPanels],
  );

  // ── Event listeners ──

  // Listen for cross-component "open settings" events
  useEffect(() => {
    const handler = (e: Event) => {
      const tab = (e as CustomEvent).detail?.tab;
      if (tab) setSettingsTab(tab);
      setSettingsOpen(true);
    };
    window.addEventListener('mindos:open-settings', handler);
    return () => window.removeEventListener('mindos:open-settings', handler);
  }, []);

  useEffect(() => {
    const handler = () => handleOpenImport();
    window.addEventListener('mindos:open-import', handler);
    return () => window.removeEventListener('mindos:open-import', handler);
  }, [handleOpenImport]);

  useEffect(() => {
    const handler = (e: Event) => {
      const files = (e as CustomEvent).detail?.files as Array<{ name: string; path: string }> | undefined;
      if (!files || files.length === 0) return;
      void inboxOrganize.requestInboxOrganize(files, {
        providerOverride: (e as CustomEvent).detail?.providerOverride,
        modelOverride: (e as CustomEvent).detail?.modelOverride,
      });
    };
    window.addEventListener('mindos:inbox-organize', handler);
    return () => window.removeEventListener('mindos:inbox-organize', handler);
  }, [inboxOrganize]);

  // ── Session/Message: AI organize conversation content ──
  useEffect(() => {
    const handler = (e: Event) => {
      const detail = (e as CustomEvent).detail as { content: string; name: string } | undefined;
      if (!detail?.content) return;
      inboxOrganize.requestConversationOrganize(detail);
    };
    window.addEventListener('mindos:session-organize', handler);
    return () => window.removeEventListener('mindos:session-organize', handler);
  }, [inboxOrganize]);

  // Listen for cross-component "open panel" events (e.g. GuideCard → Agents)
  useEffect(() => {
    const handler = (e: Event) => {
      const panel = (e as CustomEvent).detail?.panel;
      if (panel) lp.setActivePanel(panel);
    };
    window.addEventListener('mindos:open-panel', handler);
    return () => window.removeEventListener('mindos:open-panel', handler);
  }, [lp]);

  // GuideCard first message handler
  const handleFirstMessage = useCallback(() => {
    const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
    if (ap.askOpenSource === 'guide') {
      fetch('/api/setup', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ guideState: { askedAI: true } }),
      }).then(notifyGuide).catch((err) => console.warn('Guide state update failed:', err));
    } else if (ap.askOpenSource === 'guide-next') {
      notifyGuide();
    }
  }, [ap.askOpenSource]);

  // Close mobile drawer on route change
  useEffect(() => {
    const id = requestAnimationFrame(() => setMobileOpen(false));
    return () => cancelAnimationFrame(id);
  }, [pathname]);

  // Deep-link workbench routes keep their matching left panel aligned with URL.
  // Files/Mind routes are intentionally excluded so users can close that panel
  // while still staying on /wiki or /view/* content.
  useEffect(() => {
    if (isNeutralContentRoute(pathname)) {
      lp.setActivePanel((panel) => (panel === 'search' || panel === 'workflows' ? panel : null));
      return;
    }
    const panel = getRouteControlledPanel(pathname);
    if (panel) {
      lp.setActivePanel(panel);
    }
  }, [pathname, lp.setActivePanel]);

  // When leaving a route-owned panel, a slow RSC transition can let the previous
  // route alignment effect run after the destination click. Once the destination
  // route commits, recover the matching panel without overwriting utility panels.
  // Skipped while a rail navigation is in flight — recovering would undo the
  // user's optimistic click and re-trigger the state tug-of-war.
  useEffect(() => {
    if (pendingRoutePanel) return;
    const recoveredPanel = recoverStaleRoutePanel(pathname, lp.activePanel);
    if (recoveredPanel) lp.setActivePanel(recoveredPanel);
  }, [pathname, lp.activePanel, lp.setActivePanel, pendingRoutePanel]);

  const handleAgentDetailWidthCommit = useCallback((w: number) => {
    setAgentDetailWidth(w);
    try {
      localStorage.setItem('right-agent-detail-panel-width', String(w));
    } catch { /* ignore */ }
  }, []);

  const closeAgentDetailPanel = useCallback(() => setAgentDetailKey(null), []);

  // Refresh file tree when server-side tree version changes.
  // Polls a lightweight version counter every 5s — only calls router.refresh()
  // (which rebuilds the full tree) when the version actually changes.
  // A 2-second cooldown prevents rapid-fire refreshes during bulk file operations.
  useEffect(() => {
    let lastVersion = -1;
    let stopped = false;
    let lastRefreshTime = 0;
    let pendingRefreshTimer: ReturnType<typeof setTimeout> | null = null;

    const doRefresh = (version: number, previousVersion: number) => {
      lastRefreshTime = Date.now();
      const stopRefresh = telemetry.startTimer('tree.refresh.trigger');
      startTransition(() => {
        router.refresh();
      });
      stopRefresh({ previousVersion, version, reason: 'tree_version_changed' });
      window.dispatchEvent(new Event('mindos:files-changed'));
    };

    const REFRESH_COOLDOWN_MS = 2000;
    const POLL_INTERVAL_MS = 5000;

    const checkVersion = async () => {
      if (stopped || document.visibilityState === 'hidden') return;
      const stop = telemetry.startTimer('tree.version.poll');
      try {
        const res = await fetch('/api/tree-version');
        if (!res.ok) {
          stop({ ok: false, changed: false });
          return;
        }
        const { v } = (await res.json()) as { v: number };
        if (lastVersion === -1) {
          lastVersion = v;
          stop({ ok: true, changed: false, version: v, initial: true });
          return;
        }
        if (v !== lastVersion) {
          const previousVersion = lastVersion;
          lastVersion = v;

          // Cooldown: if we refreshed recently, delay this one
          const elapsed = Date.now() - lastRefreshTime;
          if (elapsed < REFRESH_COOLDOWN_MS) {
            if (pendingRefreshTimer) clearTimeout(pendingRefreshTimer);
            pendingRefreshTimer = setTimeout(() => {
              pendingRefreshTimer = null;
              if (!stopped) doRefresh(v, previousVersion);
            }, REFRESH_COOLDOWN_MS - elapsed);
            stop({ ok: true, changed: true, previousVersion, version: v, deferred: true });
          } else {
            doRefresh(v, previousVersion);
            stop({ ok: true, changed: true, previousVersion, version: v });
          }
          return;
        }
        stop({ ok: true, changed: false, version: v });
      } catch (err) {
        stop({ ok: false, changed: false });
        console.debug('[tree-version] poll failed', err);
      }
    };

    const onVisible = () => {
      if (document.visibilityState === 'visible') void checkVersion();
    };

    void checkVersion();
    const interval = setInterval(() => void checkVersion(), POLL_INTERVAL_MS);
    document.addEventListener('visibilitychange', onVisible);

    return () => {
      stopped = true;
      clearInterval(interval);
      if (pendingRefreshTimer) clearTimeout(pendingRefreshTimer);
      document.removeEventListener('visibilitychange', onVisible);
    };
  }, [router]);

  // Unified keyboard shortcuts
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        if (effectivePanelMaximized) { lp.handlePanelMaximize(); return; }
        if (agentDockOpen) { setAgentDetailKey(null); return; }
        if (ap.askPanelOpen) { ap.closeAskPanel(); return; }
        if (ap.desktopAskPopupOpen) { ap.closeDesktopAskPopup(); return; }
      }
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        if (window.innerWidth >= 768) {
          lp.setActivePanel((p: PanelId | null) => p === 'search' ? null : 'search');
        } else {
          setMobileSearchOpen(v => !v);
        }
      }
      if ((e.metaKey || e.ctrlKey) && e.key === '/') {
        e.preventDefault();
        if (window.innerWidth >= 768) {
          ap.toggleAskPanel();
        } else {
          setMobileAskOpen(v => !v);
        }
      }
      if ((e.metaKey || e.ctrlKey) && e.key === ',') {
        e.preventDefault();
        setSettingsOpen(v => !v);
      }
      if ((e.metaKey || e.ctrlKey) && e.key === 'i') {
        e.preventDefault();
        setImportModalOpen(v => !v);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [agentDockOpen, effectivePanelMaximized, lp, ap]);

  // ── Settings helpers ──
  const openSyncSettings = useCallback(() => {
    setSettingsTab('sync');
    setSyncPopoverOpen(false);
    setSettingsOpen(true);
  }, []);

  const handleSettingsClick = useCallback(() => {
    setSettingsOpen(true);
    setSettingsTab(undefined);
  }, []);

  const closeSettings = useCallback(() => {
    setSettingsOpen(false);
    setSettingsTab(undefined);
  }, []);

  const handleSyncClick = useCallback((rect: DOMRect) => {
    setSyncAnchorRect(rect);
    setSyncPopoverOpen(prev => !prev);
  }, []);

  const handleExpandedChange = useCallback((expanded: boolean) => {
    lp.handleExpandedChange(expanded);
    setSyncPopoverOpen(false);
  }, [lp]);

  const handleMobileNavigate = useCallback(() => setMobileOpen(false), []);

  const handleRoutePanelClick = useCallback((
    event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,
    targetPanel: RoutePanelId,
  ) => {
    exitAskMaximized();
    const decision = getRailPanelClickDecision(pathname, activeLeftPanel, targetPanel);
    if (decision.preventDefault) {
      event.preventDefault();
    } else {
      // Real navigation starts — keep the clicked target active until it commits
      setPendingNav({ target: targetPanel, fromPathname: pathname });
    }
    lp.setActivePanel(decision.nextPanel);
    if (targetPanel === 'agents') setAgentDetailKey(null);
  }, [activeLeftPanel, exitAskMaximized, lp, pathname]);

  return (
    <InboxOrganizeProvider value={inboxOrganize}>
      <McpStoreInit />
      <WalkthroughInit />
      {/* Skip link */}
      <a
        href="#main-content"
        className="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-2 focus-visible:left-2 focus-visible:z-[60] focus-visible:px-4 focus-visible:py-2 focus-visible:rounded-lg focus-visible:text-sm focus-visible:font-medium bg-[var(--amber)] text-[var(--amber-foreground)]"
      >
        Skip to main content
      </a>

      {/* ── Desktop: Titlebar row + Activity Bar + Panel ── */}
      <TitlebarRow />
      <ActivityBar
        activePanel={railActivePanel}
        onPanelChange={lp.setActivePanel}
        onCaptureClick={(event) => handleRoutePanelClick(event, 'capture')}
        onEchoClick={(event) => handleRoutePanelClick(event, 'echo')}
        onAgentsClick={(event) => handleRoutePanelClick(event, 'agents')}
        onDiscoverClick={(event) => handleRoutePanelClick(event, 'discover')}
        onSpacesClick={(event) => handleRoutePanelClick(event, 'files')}
        syncStatus={syncStatus}
        syncStale={syncStatusStale}
        expanded={lp.railExpanded}
        onExpandedChange={handleExpandedChange}
        onSettingsClick={handleSettingsClick}
        onSyncClick={handleSyncClick}
        syncPopoverOpen={syncPopoverOpen}
        syncPopoverId={SYNC_POPOVER_ID}
      />

      <Panel
        activePanel={activeLeftPanel}
        fileTree={fileTree}
        mindSystemSlots={mindSystemSlots}
        onNavigate={noop}
        onOpenSyncSettings={openSyncSettings}
        railWidth={lp.railWidth}
        panelWidth={lp.panelWidth ?? undefined}
        onWidthChange={lp.handlePanelWidthChange}
        onWidthCommit={lp.handlePanelWidthCommit}
        maximized={effectivePanelMaximized}
        onMaximize={lp.handlePanelMaximize}
        onImport={handleOpenImport}
      >
        {isPanelMounted('echo') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'echo' ? '' : 'hidden'}`}>
            <EchoPanel active={activeLeftPanel === 'echo'} maximized={effectivePanelMaximized} onMaximize={lp.handlePanelMaximize} />
          </div>
        )}
        {isPanelMounted('capture') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'capture' ? '' : 'hidden'}`}>
            <CapturePanel />
          </div>
        )}
        {isPanelMounted('search') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'search' ? '' : 'hidden'}`}>
            <SearchPanel active={activeLeftPanel === 'search'} maximized={effectivePanelMaximized} onMaximize={lp.handlePanelMaximize} />
          </div>
        )}
        {isPanelMounted('agents') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'agents' ? '' : 'hidden'}`}>
            <AgentsPanel
              active={activeLeftPanel === 'agents'}
              maximized={effectivePanelMaximized}
              onMaximize={lp.handlePanelMaximize}
              selectedAgentKey={agentDockOpen ? agentDetailKey : null}
            />
          </div>
        )}
        {isPanelMounted('discover') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'discover' ? '' : 'hidden'}`}>
            <DiscoverPanel active={activeLeftPanel === 'discover'} maximized={effectivePanelMaximized} onMaximize={lp.handlePanelMaximize} />
          </div>
        )}
        {isPanelMounted('workflows') && (
          <div className={`flex flex-col h-full ${activeLeftPanel === 'workflows' ? '' : 'hidden'}`}>
            <WorkflowsPanel active={activeLeftPanel === 'workflows'} maximized={effectivePanelMaximized} onMaximize={lp.handlePanelMaximize} />
          </div>
        )}
      </Panel>

      {/* ── Right-side Ask AI Panel ── */}
      {rightAskMounted && (
        <RightAskPanel
          open={ap.askPanelOpen}
          onClose={ap.closeAskPanel}
          currentFile={currentFile}
          initialMessage={ap.askInitialMessage}
          initialAcpAgent={ap.askAcpAgent}
          initialAgentRuntime={ap.askAgentRuntime}
          onFirstMessage={handleFirstMessage}
          width={ap.askPanelWidth}
          onWidthChange={ap.handleAskWidthChange}
          onWidthCommit={ap.handleAskWidthCommit}
          askMode={ap.askMode}
          onModeSwitch={ap.handleAskModeSwitch}
          maximized={ap.askMaximized}
          onMaximize={ap.toggleAskMaximized}
          sidebarOffset={panelOpen ? lp.railWidth + effectivePanelWidth : lp.railWidth}
        />
      )}

      {rightAgentDetailMounted && (
        <RightAgentDetailPanel
          open={agentDockOpen}
          agentKey={agentDetailKey}
          onClose={closeAgentDetailPanel}
          rightOffset={ap.askPanelOpen ? ap.askPanelWidth : 0}
          width={agentDetailWidth}
          onWidthChange={setAgentDetailWidth}
          onWidthCommit={handleAgentDetailWidthCommit}
        />
      )}

      {desktopAskPopupMounted && (
        <AskModal
          open={ap.desktopAskPopupOpen}
          onClose={ap.closeDesktopAskPopup}
          currentFile={currentFile}
          initialMessage={ap.askInitialMessage}
          initialAcpAgent={ap.askAcpAgent}
          initialAgentRuntime={ap.askAgentRuntime}
          onFirstMessage={handleFirstMessage}
          askMode={ap.askMode}
          onModeSwitch={ap.handleAskModeSwitch}
        />
      )}

      <AskFab onToggle={ap.toggleAskPanel} askPanelOpen={ap.askPanelOpen || ap.desktopAskPopupOpen} />
      <KeyboardShortcuts />

      {settingsMounted && <SettingsModal open={settingsOpen} onClose={closeSettings} initialTab={settingsTab} />}

      <SyncPopover
        id={SYNC_POPOVER_ID}
        open={syncPopoverOpen}
        onClose={() => setSyncPopoverOpen(false)}
        anchorRect={syncAnchorRect}
        railWidth={lp.railWidth}
        onOpenSyncSettings={openSyncSettings}
        syncStatus={syncStatus}
        syncStale={syncStatusStale}
        syncLoadError={syncStatusError}
        onSyncStatusRefresh={syncStatusRefresh}
      />

      {/* ── Mobile ── */}
      {/* top: var(--app-titlebar-h) — when the mac shell viewport drops below md, the header sits below the titlebar drag row */}
      <header className="md:hidden fixed top-[var(--app-titlebar-h)] left-0 right-0 z-30 bg-card border-b border-border flex items-center justify-between px-3 py-2" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
        <button onClick={() => setMobileOpen(true)} className="p-3 -ml-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label="Open menu">
          <Menu size={20} />
        </button>
        <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
          <Logo id="mobile" />
          <span className="text-foreground text-sm font-brand">MindOS</span>
        </Link>
        <div className="flex items-center gap-0.5">
          <button
            onClick={openSyncSettings}
            className="p-3 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent flex items-center justify-center"
            aria-label={mobileSyncLabel}
          >
            <MobileSyncDot status={syncStatus} stale={syncStatusStale} loadError={syncStatusError} />
          </button>
          <button onClick={() => setMobileSearchOpen(true)} className="p-3 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.searchTitle}>
            <Search size={20} />
          </button>
          <button onClick={() => { setSettingsOpen(true); setSettingsTab(undefined); }} className="p-3 -mr-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.settingsTitle}>
            <Settings size={20} />
          </button>
        </div>
      </header>

      {mobileOpen && <div className="md:hidden fixed inset-0 z-40 overlay-backdrop" onClick={() => setMobileOpen(false)} />}
      <aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
        <div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
          <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
            <Logo id="drawer" />
            <span className="text-foreground text-sm font-brand">MindOS</span>
          </Link>
          <button onClick={() => setMobileOpen(false)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
            <X size={16} />
          </button>
        </div>
        <div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
          <FileTree nodes={fileTree} onNavigate={handleMobileNavigate} onImport={handleOpenImport} />
        </div>
      </aside>

      {mobileSearchMounted && <SearchModal open={mobileSearchOpen} onClose={() => setMobileSearchOpen(false)} />}
      {mobileAskMounted && <AskModal open={mobileAskOpen} onClose={() => setMobileAskOpen(false)} currentFile={currentFile} />}

      <main
        id="main-content"
        className={`min-h-screen transition-all duration-200 pt-[52px] md:pt-0`}
        onDragEnter={(e) => {
          if (!e.dataTransfer.types.includes('Files')) return;
          e.preventDefault();
          dragCounterRef.current++;
          if (dragCounterRef.current === 1) setDragOverlay(true);
        }}
        onDragOver={(e) => {
          if (!e.dataTransfer.types.includes('Files')) return;
          e.preventDefault();
        }}
        onDragLeave={() => {
          dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
          if (dragCounterRef.current === 0) setDragOverlay(false);
        }}
        onDrop={(e) => {
          e.preventDefault();
          dragCounterRef.current = 0;
          setDragOverlay(false);
          if (e.dataTransfer.files.length > 0 && !importModalOpen) {
            quickDropToInbox(Array.from(e.dataTransfer.files), t);
          }
        }}
      >
        {/* min-height subtracts the titlebar row: main already pads down by
            --app-titlebar-h, so a bare 100vh here would overflow the document
            by that amount and let content scroll underneath the fixed row */}
        <div className="min-h-[calc(100vh-var(--app-titlebar-h))] bg-background" style={{ overflowX: 'clip' }}>
          <ChangesBanner />
          {children}
        </div>

        <SpaceInitToast />
        <CreateSpaceModal t={t} dirPaths={dirPaths} />

        {/* Global drag overlay — Quick Drop to Inbox */}
        {dragOverlay && !importModalOpen && (
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm transition-opacity duration-200">
            <div className="border-2 border-dashed border-[var(--amber)]/50 rounded-xl p-12 flex flex-col items-center gap-3">
              <FolderInput size={48} className="text-[var(--amber)]/60" />
              <p className="text-sm text-foreground font-medium">{t.inbox.dropOverlay}</p>
              <p className="text-xs text-muted-foreground">{t.inbox.dropOverlayFormats}</p>
            </div>
          </div>
        )}
      </main>

      {importMounted && (
        <ImportModal
          open={importModalOpen}
          onClose={handleCloseImport}
          defaultSpace={importDefaultSpace}
          initialFiles={importInitialFiles}
          aiOrganize={aiOrganize}
          dirPaths={dirPaths}
        />
      )}

      {organizeToastVisible && (
        <OrganizeToast
          aiOrganize={aiOrganize}
          onDismiss={handleOrganizeToastDismiss}
          onCancel={() => { aiOrganize.abort(); aiOrganize.reset(); setOrganizeToastVisible(false); }}
          onHistoryUpdate={handleHistoryUpdate}
        />
      )}

      <style>{`
        @media (min-width: 768px) {
          :root {
            --rail-width: ${lp.railWidth}px;
            --right-panel-width: ${ap.askMaximized ? `calc(100vw - ${panelOpen ? lp.railWidth + effectivePanelWidth : lp.railWidth}px)` : `${ap.askPanelOpen ? ap.askPanelWidth : 0}px`};
            --right-agent-detail-width: ${agentDockOpen ? agentDetailWidth : 0}px;
          }
          #main-content {
            padding-left: ${panelOpen && effectivePanelMaximized ? '100vw' : `${panelOpen ? lp.railWidth + effectivePanelWidth : lp.railWidth}px`} !important;
            padding-right: calc(var(--right-panel-width) + var(--right-agent-detail-width) + var(--toc-extra-right, 0px)) !important;
            padding-top: var(--app-titlebar-h);
          }
        }
      `}</style>
    </InboxOrganizeProvider>
  );
}
