import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Sidebar } from './components/Sidebar'
import { NoteList } from './components/NoteList'
import { Editor } from './components/Editor'
import { ResizeHandle } from './components/ResizeHandle'
import { CreateTypeDialog } from './components/CreateTypeDialog'
import { QuickOpenPalette } from './components/QuickOpenPalette'
import { CommandPalette } from './components/CommandPalette'
import { SearchPanel } from './components/SearchPanel'
import { Toast } from './components/Toast'
import { CommitDialog } from './components/CommitDialog'
import { PulseView } from './components/PulseView'
import { StatusBar } from './components/StatusBar'
import { SettingsPanel } from './components/SettingsPanel'
import { GitHubVaultModal } from './components/GitHubVaultModal'
import { WelcomeScreen } from './components/WelcomeScreen'
import { useMcpStatus } from './hooks/useMcpStatus'
import { useVaultLoader } from './hooks/useVaultLoader'
import { useSettings } from './hooks/useSettings'
import { useNoteActions, needsRenameOnSave } from './hooks/useNoteActions'
import { useCommitFlow } from './hooks/useCommitFlow'
import { useViewMode } from './hooks/useViewMode'
import { useEntryActions } from './hooks/useEntryActions'
import { useAppCommands } from './hooks/useAppCommands'
import { useDialogs } from './hooks/useDialogs'
import { useVaultSwitcher } from './hooks/useVaultSwitcher'
import { useGitHistory } from './hooks/useGitHistory'
import { useUpdater, restartApp } from './hooks/useUpdater'
import { useNavigationHistory } from './hooks/useNavigationHistory'
import { useAutoSync } from './hooks/useAutoSync'
import { useConflictResolver } from './hooks/useConflictResolver'
import { useIndexing } from './hooks/useIndexing'
import { useZoom } from './hooks/useZoom'
import { useVaultConfig } from './hooks/useVaultConfig'
import { useBuildNumber } from './hooks/useBuildNumber'
import { useOnboarding } from './hooks/useOnboarding'
import { useThemeManager } from './hooks/useThemeManager'
import { useEditorSaveWithLinks } from './hooks/useEditorSaveWithLinks'
import { useNavigationGestures } from './hooks/useNavigationGestures'
import { useAiActivity } from './hooks/useAiActivity'
import { ConflictResolverModal } from './components/ConflictResolverModal'
import { UpdateBanner } from './components/UpdateBanner'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from './mock-tauri'
import type { SidebarSelection, VaultEntry } from './types'
import type { NoteListItem } from './utils/ai-context'
import { filterEntries } from './utils/noteListHelpers'
import { openLocalFile } from './utils/url'
import { flushEditorContent } from './utils/autoSave'
import './App.css'

// Type declarations for mock content storage and test overrides
declare global {
  interface Window {
    __mockContent?: Record<string, string>
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock handler map for Playwright test overrides
    __mockHandlers?: Record<string, (args: any) => any>
  }
}

const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }

function useBulkActions(
  entryActions: { handleArchiveNote: (path: string) => Promise<void>; handleTrashNote: (path: string) => Promise<void> },
  setToastMessage: (msg: string | null) => void,
) {
  const handleBulkArchive = useCallback(async (paths: string[]) => {
    let ok = 0
    for (const path of paths) {
      try { await entryActions.handleArchiveNote(path); ok++ }
      catch { /* error toast already shown by flushBeforeAction */ }
    }
    if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} archived`)
  }, [entryActions, setToastMessage])

  const handleBulkTrash = useCallback(async (paths: string[]) => {
    let ok = 0
    for (const path of paths) {
      try { await entryActions.handleTrashNote(path); ok++ }
      catch { /* error toast already shown by flushBeforeAction */ }
    }
    if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} moved to trash`)
  }, [entryActions, setToastMessage])

  return { handleBulkArchive, handleBulkTrash }
}

function useLayoutPanels() {
  const [sidebarWidth, setSidebarWidth] = useState(250)
  const [noteListWidth, setNoteListWidth] = useState(300)
  const [inspectorWidth, setInspectorWidth] = useState(280)
  const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
  const handleSidebarResize = useCallback((delta: number) => setSidebarWidth((w) => Math.max(150, Math.min(400, w + delta))), [])
  const handleNoteListResize = useCallback((delta: number) => setNoteListWidth((w) => Math.max(200, Math.min(500, w + delta))), [])
  const handleInspectorResize = useCallback((delta: number) => setInspectorWidth((w) => Math.max(200, Math.min(500, w - delta))), [])
  return { sidebarWidth, noteListWidth, inspectorWidth, inspectorCollapsed, setInspectorCollapsed, handleSidebarResize, handleNoteListResize, handleInspectorResize }
}

/** Wraps useEditorSave to also keep outgoingLinks in sync on save and on content change. */
function App() {
  const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
  const layout = useLayoutPanels()
  const [toastMessage, setToastMessage] = useState<string | null>(null)
  const dialogs = useDialogs()

  // onSwitch closure captures `notes` declared below — safe because it's only
  // called on user interaction, never during render (refs inside the hook
  // guarantee the latest closure is always used).
  const vaultSwitcher = useVaultSwitcher({
    onSwitch: () => { setSelection(DEFAULT_SELECTION); notes.closeAllTabs() },
    onToast: (msg) => setToastMessage(msg),
  })

  const onboarding = useOnboarding(vaultSwitcher.vaultPath)

  // When onboarding resolves to a different vault path, update the switcher
  const resolvedPath = onboarding.state.status === 'ready' ? onboarding.state.vaultPath : vaultSwitcher.vaultPath
  const vault = useVaultLoader(resolvedPath)
  useVaultConfig(resolvedPath)
  const { settings, saveSettings } = useSettings()
  const themeManager = useThemeManager(resolvedPath, vault.entries)

  const { mcpStatus, installMcp } = useMcpStatus(resolvedPath, setToastMessage)

  const indexing = useIndexing(resolvedPath)

  const autoSync = useAutoSync({
    vaultPath: resolvedPath,
    intervalMinutes: settings.auto_pull_interval_minutes,
    onVaultUpdated: vault.reloadVault,
    onSyncUpdated: indexing.triggerIncrementalIndex,
    onConflict: (files) => {
      const names = files.map((f) => f.split('/').pop()).join(', ')
      setToastMessage(`Conflict in ${names} — click to resolve`)
    },
    onToast: (msg) => setToastMessage(msg),
  })

  // Ref bridges for conflict resolution callbacks (notes declared below)
  const openConflictFileRef = useRef<(relativePath: string) => void>(() => {})

  const conflictResolver = useConflictResolver({
    vaultPath: resolvedPath,
    onResolved: () => {
      dialogs.closeConflictResolver()
      autoSync.resumePull()
      vault.reloadVault()
      autoSync.triggerSync()
    },
    onToast: (msg) => setToastMessage(msg),
    onOpenFile: (relativePath) => openConflictFileRef.current(relativePath),
  })

  const handleOpenConflictResolver = useCallback(async () => {
    let files = autoSync.conflictFiles
    // If no cached conflicts, check directly — there may be pre-existing
    // conflicts from a prior session that the pull flow didn't detect.
    if (files.length === 0) {
      try {
        files = isTauri()
          ? await invoke<string[]>('get_conflict_files', { vaultPath: resolvedPath })
          : await mockInvoke<string[]>('get_conflict_files', { vaultPath: resolvedPath })
      } catch {
        return
      }
      if (files.length === 0) {
        setToastMessage('No merge conflicts to resolve')
        return
      }
    }
    autoSync.pausePull()
    conflictResolver.initFiles(files)
    dialogs.openConflictResolver()
  }, [autoSync, conflictResolver, dialogs, resolvedPath])

  const handleCloseConflictResolver = useCallback(() => {
    autoSync.resumePull()
    dialogs.closeConflictResolver()
  }, [autoSync, dialogs])

  // Ref bridges handleContentChange (created after notes) into useNoteActions.
  // Read at callback time, so it's always current when user presses Cmd+N.
  const contentChangeRef = useRef<(path: string, content: string) => void>(() => {})

  const notes = useNoteActions({ addEntry: vault.addEntry, removeEntry: vault.removeEntry, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry, vaultPath: resolvedPath, addPendingSave: vault.addPendingSave, removePendingSave: vault.removePendingSave, trackUnsaved: vault.trackUnsaved, clearUnsaved: vault.clearUnsaved, unsavedPaths: vault.unsavedPaths, markContentPending: (path, content) => contentChangeRef.current(path, content), onNewNotePersisted: vault.loadModifiedFiles, replaceEntry: vault.replaceEntry })

  // Keep tab entries in sync with vault entries so banners (trash/archive)
  // and read-only state react immediately without reopening the note.
  useEffect(() => {
    notes.setTabs(prev => {
      let changed = false
      const next = prev.map(tab => {
        const fresh = vault.entries.find(e => e.path === tab.entry.path)
        if (fresh && fresh !== tab.entry) {
          changed = true
          return { ...tab, entry: fresh }
        }
        return tab
      })
      return changed ? next : prev
    })
  }, [vault.entries]) // eslint-disable-line react-hooks/exhaustive-deps -- notes.setTabs is stable (useState setter)

  const navHistory = useNavigationHistory()

  // Push to navigation history whenever the active tab changes (user-initiated)
  const navFromHistoryRef = useRef(false)
  useEffect(() => {
    if (notes.activeTabPath && !navFromHistoryRef.current) {
      navHistory.push(notes.activeTabPath)
    }
    navFromHistoryRef.current = false
  }, [notes.activeTabPath]) // eslint-disable-line react-hooks/exhaustive-deps -- navHistory.push is stable

  const isEntryExists = useCallback((path: string) => vault.entries.some(e => e.path === path), [vault.entries])

  const handleGoBack = useCallback(() => {
    const target = navHistory.goBack(isEntryExists)
    if (target) {
      navFromHistoryRef.current = true
      if (notes.tabs.some(t => t.entry.path === target)) {
        notes.handleSwitchTab(target)
      } else {
        const entry = vault.entries.find(e => e.path === target)
        if (entry) notes.handleSelectNote(entry)
      }
    }
  }, [navHistory, isEntryExists, vault.entries, notes])

  const handleGoForward = useCallback(() => {
    const target = navHistory.goForward(isEntryExists)
    if (target) {
      navFromHistoryRef.current = true
      if (notes.tabs.some(t => t.entry.path === target)) {
        notes.handleSwitchTab(target)
      } else {
        const entry = vault.entries.find(e => e.path === target)
        if (entry) notes.handleSelectNote(entry)
      }
    }
  }, [navHistory, isEntryExists, vault.entries, notes])

  useNavigationGestures({ onGoBack: handleGoBack, onGoForward: handleGoForward })

  // O(1) path lookup map — rebuilt only when vault.entries changes
  const entriesByPath = useMemo(() => {
    const map = new Map<string, VaultEntry>()
    for (const e of vault.entries) map.set(e.path, e)
    return map
  }, [vault.entries])

  // MCP UI bridge: react to AI-driven open/highlight/vault-change events
  const openNoteByPath = useCallback((path: string) => {
    const entry = entriesByPath.get(path) ?? entriesByPath.get(`${resolvedPath}/${path}`)
    if (entry) {
      notes.handleSelectNote(entry)
    } else {
      // Entry not yet in vault (just created) — reload then open
      vault.reloadVault().then(freshEntries => {
        const fresh = (freshEntries as VaultEntry[]).find(e => e.path === path || e.path === `${resolvedPath}/${path}`)
        if (fresh) notes.handleSelectNote(fresh)
      })
    }
  }, [entriesByPath, vault, notes, resolvedPath])

  const aiActivity = useAiActivity({
    onOpenNote: openNoteByPath,
    onOpenTab: openNoteByPath,
    onSetFilter: (filterType) => {
      setSelection({ kind: 'sectionGroup', type: filterType })
    },
    onVaultChanged: () => { vault.reloadVault() },
  })

  // Stable callback for Pulse "open note" — never triggers reloadVault.
  // Pulse files always exist in the vault; if somehow not found, silently skip.
  const handlePulseOpenNote = useCallback((relativePath: string) => {
    const fullPath = `${resolvedPath}/${relativePath}`
    const entry = entriesByPath.get(fullPath) ?? entriesByPath.get(relativePath)
    if (entry) notes.handleSelectNote(entry)
  }, [entriesByPath, resolvedPath, notes])

  // Agent file operation handlers: auto-open created notes, live-refresh modified notes
  const handleAgentFileCreated = useCallback((relativePath: string) => {
    vault.reloadVault().then(freshEntries => {
      const entry = (freshEntries as VaultEntry[]).find(e => e.path === relativePath || e.path === `${resolvedPath}/${relativePath}`)
      if (entry) notes.handleSelectNote(entry)
    })
  }, [vault, notes, resolvedPath])

  const handleAgentFileModified = useCallback((relativePath: string) => {
    const fullPath = `${resolvedPath}/${relativePath}`
    const matchPath = notes.tabs.some(t => t.entry.path === relativePath) ? relativePath : fullPath
    if (notes.tabs.some(t => t.entry.path === matchPath)) {
      vault.reloadVault()
    }
  }, [vault, notes, resolvedPath])

  const handleAgentVaultChanged = useCallback(() => {
    vault.reloadVault()
  }, [vault])

  const { triggerIncrementalIndex } = indexing
  const onAfterSave = useCallback(() => {
    vault.loadModifiedFiles()
    triggerIncrementalIndex()
  }, [vault, triggerIncrementalIndex])

  const { notifyThemeSaved } = themeManager
  const onNotePersisted = useCallback((path: string, content: string) => {
    vault.clearUnsaved(path)
    notifyThemeSaved(path, content)
  }, [vault, notifyThemeSaved])

  const { handleSave: handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorSaveWithLinks({
    updateEntry: vault.updateEntry,
    setTabs: notes.setTabs, setToastMessage, onAfterSave,
    onNotePersisted,
  })
  useEffect(() => { contentChangeRef.current = handleContentChange }, [handleContentChange])

  // Refs for stable closure in flushBeforeAction (avoids re-creating on every tab/content change)
  const tabsRef = useRef(notes.tabs)
  tabsRef.current = notes.tabs // eslint-disable-line react-hooks/refs -- ref sync pattern
  const unsavedPathsRef = useRef(vault.unsavedPaths)
  unsavedPathsRef.current = vault.unsavedPaths // eslint-disable-line react-hooks/refs -- ref sync pattern

  /** Auto-save unsaved editor content before a destructive action (trash/archive). */
  const { clearUnsaved: vaultClearUnsaved } = vault
  const flushBeforeAction = useCallback(async (path: string) => {
    try {
      await flushEditorContent(path, {
        savePendingForPath,
        getTabContent: (p) => tabsRef.current.find(t => t.entry.path === p)?.content,
        isUnsaved: (p) => unsavedPathsRef.current.has(p),
        onSaved: (p) => { vaultClearUnsaved(p) },
      })
    } catch (err) {
      setToastMessage(`Auto-save failed: ${err}`)
      throw err
    }
  }, [savePendingForPath, vaultClearUnsaved, setToastMessage])

  // Wire conflict file opener now that notes is available
  useEffect(() => {
    openConflictFileRef.current = (relativePath: string) => {
      const fullPath = `${resolvedPath}/${relativePath}`
      const entry = vault.entries.find(e => e.path === fullPath)
      if (entry) {
        // Markdown note — open inside Laputa editor
        notes.handleSelectNote(entry)
        dialogs.closeConflictResolver()
      } else {
        // Non-note file (e.g. settings.json) —
        // open with system default app so the user can inspect/edit it
        openLocalFile(fullPath)
      }
    }
  }, [resolvedPath, vault.entries, notes, dialogs])

  const handleRenameTab = useCallback(async (path: string, newTitle: string) => {
    await savePendingForPath(path)
    await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles)
  }, [notes, resolvedPath, vault, savePendingForPath])

  // Wrap handleSave to also persist unsaved notes that have no pending edits (user pressed Cmd+S without typing)
  // and trigger file rename when the title slug doesn't match the filename.
  const handleSave = useCallback(async () => {
    const activeTab = notes.tabs.find(t => t.entry.path === notes.activeTabPath)
    const fallback = activeTab && vault.unsavedPaths.has(activeTab.entry.path)
      ? { path: activeTab.entry.path, content: activeTab.content }
      : undefined
    await handleSaveRaw(fallback)

    // After saving, check if filename needs to match the current title
    if (activeTab && needsRenameOnSave(activeTab.entry.title, activeTab.entry.filename)) {
      await handleRenameTab(activeTab.entry.path, activeTab.entry.title)
    }
  }, [handleSaveRaw, handleRenameTab, notes.tabs, notes.activeTabPath, vault.unsavedPaths])

  const commitFlow = useCommitFlow({ savePending, loadModifiedFiles: vault.loadModifiedFiles, commitAndPush: vault.commitAndPush, setToastMessage })

  const entryActions = useEntryActions({
    entries: vault.entries, updateEntry: vault.updateEntry,
    handleUpdateFrontmatter: notes.handleUpdateFrontmatter,
    handleDeleteProperty: notes.handleDeleteProperty, setToastMessage,
    createTypeEntry: notes.createTypeEntrySilent,
    onFrontmatterPersisted: vault.loadModifiedFiles,
    onBeforeAction: flushBeforeAction,
  })

  const handleDeleteNote = useCallback(async (path: string) => {
    try {
      if (isTauri()) await invoke('delete_note', { path })
      else await mockInvoke('delete_note', { path })
      notes.handleCloseTab(path)
      vault.removeEntry(path)
      setToastMessage('Note permanently deleted')
    } catch (e) {
      setToastMessage(`Failed to delete note: ${e}`)
    }
  }, [notes, vault, setToastMessage])

  const gitHistory = useGitHistory(notes.activeTabPath, vault.loadGitHistory)

  const handleCreateType = useCallback((name: string) => {
    notes.handleCreateType(name)
    setToastMessage(`Type "${name}" created`)
  }, [notes])

  /** H1→title sync: save pending content then rename file + update wikilinks. */
  const handleTitleSync = useCallback(async (path: string, newTitle: string) => {
    await savePendingForPath(path)
    await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles)
  }, [notes, resolvedPath, vault, savePendingForPath])

  const bulkActions = useBulkActions(entryActions, setToastMessage)

  // Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it
  const rawToggleRef = useRef<() => void>(() => {})
  // Diff-toggle ref: Editor registers its handleToggleDiff here so the command palette can call it
  const diffToggleRef = useRef<() => void>(() => {})

  const { setViewMode, sidebarVisible, noteListVisible } = useViewMode()
  const zoom = useZoom()
  const buildNumber = useBuildNumber()

  const { status: updateStatus, actions: updateActions } = useUpdater()

  const handleCheckForUpdates = useCallback(async () => {
    if (updateStatus.state === 'downloading') {
      setToastMessage('Update is downloading…')
      return
    }
    if (updateStatus.state === 'ready') {
      await restartApp()
      return
    }
    const result = await updateActions.checkForUpdates()
    if (result === 'up-to-date') {
      setToastMessage("You're on the latest version")
    } else if (result === 'error') {
      setToastMessage('Could not check for updates')
    }
    // 'available' → UpdateBanner handles it automatically
  }, [updateActions, updateStatus.state, setToastMessage])

  const handleRestoreDefaultThemes = useCallback(async () => {
    if (!resolvedPath) return
    try {
      const tauriInvoke = isTauri() ? invoke : mockInvoke
      const msg = await tauriInvoke<string>('restore_default_themes', { vaultPath: resolvedPath })
      await vault.reloadVault()
      await themeManager.reloadThemes()
      setToastMessage(msg)
    } catch (err) {
      setToastMessage(`Failed to restore themes: ${err}`)
    }
  }, [resolvedPath, vault, themeManager, setToastMessage])

  const handleRepairVault = useCallback(async () => {
    if (!resolvedPath) return
    try {
      const tauriInvoke = isTauri() ? invoke : mockInvoke
      const msg = await tauriInvoke<string>('repair_vault', { vaultPath: resolvedPath })
      await vault.reloadVault()
      await themeManager.reloadThemes()
      setToastMessage(msg)
    } catch (err) {
      setToastMessage(`Failed to repair vault: ${err}`)
    }
  }, [resolvedPath, vault, themeManager, setToastMessage])

  const commands = useAppCommands({
    activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef,
    handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs,
    entries: vault.entries,
    modifiedCount: vault.modifiedFiles.length,
    activeNoteModified: vault.modifiedFiles.some(f => f.path === notes.activeTabPath),
    selection,
    onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette,
    onSearch: dialogs.openSearch,
    onCreateNote: notes.handleCreateNoteImmediate,
    onOpenDailyNote: notes.handleOpenDailyNote,
    onCreateNoteOfType: notes.handleCreateNoteImmediate,
    onSave: handleSave,
    onOpenSettings: dialogs.openSettings,
    onTrashNote: entryActions.handleTrashNote, onRestoreNote: entryActions.handleRestoreNote,
    onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote,
    onCommitPush: commitFlow.openCommitDialog,
    onResolveConflicts: handleOpenConflictResolver,
    onSetViewMode: setViewMode,
    onToggleInspector: () => layout.setInspectorCollapsed(c => !c),
    onToggleDiff: () => diffToggleRef.current(),
    onToggleRawEditor: () => rawToggleRef.current(),
    onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset,
    zoomLevel: zoom.zoomLevel,
    onSelect: setSelection, onCloseTab: notes.handleCloseTab,
    onSwitchTab: notes.handleSwitchTab, onReplaceActiveTab: notes.handleReplaceActiveTab,
    onSelectNote: notes.handleSelectNote,
    onGoBack: handleGoBack, onGoForward: handleGoForward,
    canGoBack: navHistory.canGoBack, canGoForward: navHistory.canGoForward,
    themes: themeManager.themes, activeThemeId: themeManager.activeThemeId,
    onSwitchTheme: themeManager.switchTheme,
    onCreateTheme: async () => {
      const path = await themeManager.createTheme()
      const freshEntries = await vault.reloadVault()
      setSelection({ kind: 'sectionGroup', type: 'Theme' })
      if (path) {
        const entry = freshEntries.find(e => e.path === path)
        if (entry) notes.handleSelectNote(entry)
      }
    },
    onOpenTheme: (themeId: string) => {
      const entry = vault.entries.find(e => e.path === themeId)
      if (entry) notes.handleSelectNote(entry)
    },
    onOpenVault: vaultSwitcher.handleOpenLocalFolder,
    onCreateType: dialogs.openCreateType,
    onToggleAIChat: dialogs.toggleAIChat,
    onCheckForUpdates: handleCheckForUpdates,
    onRemoveActiveVault: () => vaultSwitcher.removeVault(vaultSwitcher.vaultPath),
    onRestoreGettingStarted: vaultSwitcher.restoreGettingStarted,
    onRestoreDefaultThemes: handleRestoreDefaultThemes,
    isGettingStartedHidden: vaultSwitcher.isGettingStartedHidden,
    vaultCount: vaultSwitcher.allVaults.length,
    mcpStatus,
    onInstallMcp: installMcp,
    onReindexVault: indexing.triggerFullReindex,
    onReloadVault: vault.reloadVault,
    onRepairVault: handleRepairVault,
  })

  const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null

  const aiNoteList = useMemo<NoteListItem[]>(() => {
    return filterEntries(vault.entries, selection).map(e => ({
      path: e.path, title: e.title, type: e.isA ?? 'Note',
    }))
  }, [vault.entries, selection])

  const aiNoteListFilter = useMemo(() => {
    if (selection.kind === 'sectionGroup') return { type: selection.type, query: '' }
    if (selection.kind === 'topic') return { type: null, query: selection.entry.title }
    if (selection.kind === 'entity') return { type: null, query: selection.entry.title }
    return { type: null, query: '' }
  }, [selection])

  // Show welcome/onboarding screen when vault doesn't exist
  if (onboarding.state.status === 'welcome' || onboarding.state.status === 'vault-missing') {
    const defaultPath = onboarding.state.defaultPath
    return (
      <div className="app-shell">
        <WelcomeScreen
          mode={onboarding.state.status === 'welcome' ? 'welcome' : 'vault-missing'}
          missingPath={onboarding.state.status === 'vault-missing' ? onboarding.state.vaultPath : undefined}
          defaultVaultPath={defaultPath}
          onCreateVault={onboarding.handleCreateVault}
          onOpenFolder={onboarding.handleOpenFolder}
          creating={onboarding.creating}
          error={onboarding.error}
        />
      </div>
    )
  }

  // Show loading spinner while checking vault
  if (onboarding.state.status === 'loading') {
    return (
      <div className="app-shell">
        <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--sidebar)' }}>
          <span style={{ color: 'var(--muted-foreground)', fontSize: 14 }}>Loading…</span>
        </div>
      </div>
    )
  }

  return (
    <div className="app-shell">
      <div className="app">
        {sidebarVisible && (
          <>
            <div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
              <Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} modifiedCount={vault.modifiedFiles.length} onCommitPush={commitFlow.openCommitDialog} isGitVault={!vault.modifiedFilesError} />
            </div>
            <ResizeHandle onResize={layout.handleSidebarResize} />
          </>
        )}
        {noteListVisible && (
          <>
            <div className={`app__note-list${aiActivity.highlightElement === 'notelist' ? ' ai-highlight' : ''}`} style={{ width: layout.noteListWidth }}>
              {selection.kind === 'filter' && selection.filter === 'pulse' ? (
                <PulseView vaultPath={resolvedPath} onOpenNote={handlePulseOpenNote} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => setViewMode('all')} />
              ) : (
                <NoteList entries={vault.entries} selection={selection} selectedNote={activeTab?.entry ?? null} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={notes.handleCreateNoteImmediate} onBulkArchive={bulkActions.handleBulkArchive} onBulkTrash={bulkActions.handleBulkTrash} onUpdateTypeSort={notes.handleUpdateFrontmatter} updateEntry={vault.updateEntry} />
              )}
            </div>
            <ResizeHandle onResize={layout.handleNoteListResize} />
          </>
        )}
        <div className={`app__editor${aiActivity.highlightElement === 'editor' || aiActivity.highlightElement === 'tab' ? ' ai-highlight' : ''}`}>
          <Editor
            tabs={notes.tabs}
            activeTabPath={notes.activeTabPath}
            entries={vault.entries}
            onSwitchTab={notes.handleSwitchTab}
            onCloseTab={notes.handleCloseTab}
            onReorderTabs={notes.handleReorderTabs}
            onNavigateWikilink={notes.handleNavigateWikilink}
            onLoadDiff={vault.loadDiff}
            onLoadDiffAtCommit={vault.loadDiffAtCommit}
            getNoteStatus={vault.getNoteStatus}
            onCreateNote={notes.handleCreateNoteImmediate}
            inspectorCollapsed={layout.inspectorCollapsed}
            onToggleInspector={() => layout.setInspectorCollapsed((c) => !c)}
            inspectorWidth={layout.inspectorWidth}
            onInspectorResize={layout.handleInspectorResize}
            inspectorEntry={activeTab?.entry ?? null}
            inspectorContent={activeTab?.content ?? null}
            gitHistory={gitHistory}
            onUpdateFrontmatter={notes.handleUpdateFrontmatter}
            onDeleteProperty={notes.handleDeleteProperty}
            onAddProperty={notes.handleAddProperty}
            onCreateAndOpenNote={notes.handleCreateNoteForRelationship}
            showAIChat={dialogs.showAIChat}
            onToggleAIChat={dialogs.toggleAIChat}
            vaultPath={resolvedPath}
            noteList={aiNoteList}
            noteListFilter={aiNoteListFilter}
            onTrashNote={entryActions.handleTrashNote}
            onRestoreNote={entryActions.handleRestoreNote}
            onDeleteNote={handleDeleteNote}
            onArchiveNote={entryActions.handleArchiveNote}
            onUnarchiveNote={entryActions.handleUnarchiveNote}
            onRenameTab={handleRenameTab}
            onContentChange={handleContentChange}
            onSave={handleSave}
            onTitleSync={handleTitleSync}
            rawToggleRef={rawToggleRef}
            diffToggleRef={diffToggleRef}
            canGoBack={navHistory.canGoBack}
            canGoForward={navHistory.canGoForward}
            onGoBack={handleGoBack}
            onGoForward={handleGoForward}
            leftPanelsCollapsed={!sidebarVisible && !noteListVisible}
            isDarkTheme={themeManager.isDark}
            onFileCreated={handleAgentFileCreated}
            onFileModified={handleAgentFileModified}
            onVaultChanged={handleAgentVaultChanged}
          />
        </div>
      </div>
      <UpdateBanner status={updateStatus} actions={updateActions} />
      <StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultSwitcher.vaultPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onConnectGitHub={dialogs.openGitHubVault} onClickPending={() => setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} onOpenConflictResolver={handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} indexingProgress={indexing.progress} lastIndexedTime={indexing.lastIndexedTime} onRetryIndexing={indexing.retryIndexing} onReindexVault={indexing.triggerFullReindex} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} />
      <Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
      <QuickOpenPalette open={dialogs.showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={dialogs.closeQuickOpen} />
      <CommandPalette open={dialogs.showCommandPalette} commands={commands} onClose={dialogs.closeCommandPalette} />
      <SearchPanel open={dialogs.showSearch} vaultPath={resolvedPath} entries={vault.entries} onSelectNote={notes.handleSelectNote} onClose={dialogs.closeSearch} />
      <CreateTypeDialog open={dialogs.showCreateTypeDialog} onClose={dialogs.closeCreateType} onCreate={handleCreateType} />
      <CommitDialog open={commitFlow.showCommitDialog} modifiedCount={vault.modifiedFiles.length} onCommit={commitFlow.handleCommitPush} onClose={commitFlow.closeCommitDialog} />
      <ConflictResolverModal
        open={dialogs.showConflictResolver}
        fileStates={conflictResolver.fileStates}
        allResolved={conflictResolver.allResolved}
        committing={conflictResolver.committing}
        error={conflictResolver.error}
        onResolveFile={conflictResolver.resolveFile}
        onOpenInEditor={conflictResolver.openInEditor}
        onCommit={conflictResolver.commitResolution}
        onClose={handleCloseConflictResolver}
      />
      <SettingsPanel open={dialogs.showSettings} settings={settings} onSave={saveSettings} onClose={dialogs.closeSettings} themeManager={themeManager} />
      <GitHubVaultModal
        open={dialogs.showGitHubVault}
        githubToken={settings.github_token}
        onClose={dialogs.closeGitHubVault}
        onVaultCloned={vaultSwitcher.handleVaultCloned}
        onOpenSettings={() => { dialogs.closeGitHubVault(); dialogs.openSettings() }}
        onGitHubConnected={(token, username) => saveSettings({ ...settings, github_token: token, github_username: username })}
      />
    </div>
  )
}

export default App
