import { useStore } from '@nanostores/react'
import { atom, computed } from 'nanostores'
import type { CSSProperties, ReactElement, PointerEvent as ReactPointerEvent } from 'react'

import { SessionDraftTitle } from '@/app/chat/session-draft-title'
import { SessionStatusDot } from '@/app/chat/session-status-dot'
import { PALETTE_AREA, type PaletteContribution, paletteToggle } from '@/app/command-palette/contrib'
import { type StatusbarItem } from '@/app/shell/statusbar-controls'
import { InlinePreviewDirective } from '@/components/assistant-ui/inline-preview-directive'
import { IdleMount } from '@/components/idle-mount'
import { $layoutEditMode, toggleLayoutEditMode } from '@/components/pane-shell/edit-mode'
import { allPaneIds, group, groupLeafIds, split } from '@/components/pane-shell/tree/model'
import { LayoutTreeRoot } from '@/components/pane-shell/tree/renderer'
import {
  $layoutTree,
  bindPaneVisibility,
  bindToolPaneCollapse,
  bindTreeSideVisibility,
  declareDefaultTree,
  dismissTreePane,
  isPaneVisible,
  markCollapsePane,
  mirrorLayoutTree,
  paneRootSide,
  registerLayoutResetHandler,
  registerPaneCloser,
  registerPaneOpener,
  removeTreePane,
  resetLayoutTree,
  revealTreePane,
  setStripTabHidden,
  targetZoneTabStripVisible,
  togglePaneVisible,
  toggleTargetZoneTabStrip,
  watchContributedPanes
} from '@/components/pane-shell/tree/store'
import { $workspaceOwnerLabels, workspaceOwnerTitle } from '@/components/pane-shell/workspace-scope'
import { SidebarProvider } from '@/components/ui/sidebar'
import { discoverBundledPlugins } from '@/contrib/plugins'
import { Slot } from '@/contrib/react/slot'
import { useContributions } from '@/contrib/react/use-contributions'
import { registry } from '@/contrib/registry'
import { discoverRuntimePlugins } from '@/contrib/runtime-loader'
import { translateNow } from '@/i18n'
import { NEW_SESSION_TITLE, sessionTitle as storedSessionTitle } from '@/lib/chat-runtime'
import { Download, FileText, LayoutDashboard, PanelBottom, PanelTop, Terminal, Upload, Zap } from '@/lib/icons'
import { type KeybindContribution, KEYBINDS_AREA } from '@/lib/keybinds/actions'
import { TRANSCRIPT_DIRECTIVE_AREA, type TranscriptDirectiveContribution } from '@/lib/transcript-directives'
import { setYoloEnabled } from '@/lib/yolo-session'
import { pruneComposerPopoutZones } from '@/store/composer-popout'
import {
  $fileBrowserOpen,
  $panesFlipped,
  $sidebarOpen,
  FILE_BROWSER_DEFAULT_WIDTH,
  FILE_BROWSER_MAX_WIDTH,
  FILE_BROWSER_MIN_WIDTH,
  setFileBrowserOpen,
  setSidebarOpen,
  SIDEBAR_DEFAULT_WIDTH,
  SIDEBAR_MAX_WIDTH
} from '@/store/layout'
import { runExportProfileFlow, runImportProfileFlow } from '@/store/profile-share'
import {
  $reviewOpen,
  $reviewScopeCwd,
  $reviewScopeTarget,
  closeReview,
  openReview,
  REVIEW_PANE_ID
} from '@/store/review'
import { $currentCwd, $selectedStoredSessionId, $sessions, $yoloActive, sessionMatchesStoredId } from '@/store/session'
import { watchSessionPins } from '@/store/session-pin-sync'
import { $botChatScopes } from '@/store/session-states'
import { watchUnreadWriteGuard } from '@/store/session-unread-remote'
import { $statusbarVisible } from '@/store/statusbar-prefs'
import { isBrowserWindow, isHudWindow } from '@/store/windows'

import { BrowserPopoutShell } from '../chat/browser-popout-shell'
import type { SessionDragPayload } from '../chat/composer/inline-refs'
import { watchPreviewTiles } from '../chat/preview-tile'
import { watchRouteTiles } from '../chat/route-tile'
import { startSessionDrag } from '../chat/session-drag'
import {
  SessionTileCloseConfirm,
  stackSessionTilesIntoMain,
  startUnrestoredTileTitleBackfill,
  watchSessionTiles,
  WorkspaceTabMenu
} from '../chat/session-tile'
import { AppContextMenu } from '../context-menu/app-context-menu'
import { HudShell } from '../hud/hud-shell'
import { $terminalTakeover, setTerminalTakeover } from '../right-sidebar/store'
import { $workspaceIsPage } from '../routes'

import { FilesPane, LogsPane, ReviewPaneContent } from './panes'
import { ContribWiring, WiredPane } from './wiring'

/**
 * Stripped-down app root (bb/contrib-areas) on the layout TREE model, mounting
 * the REAL app surfaces. The title bar and status bar sit OUTSIDE the grid
 * (fixed chrome) but are fully composable: title bar renders `titleBar.left/
 * right` slots; the status bar consumes `statusBar.left/right` DATA
 * contributions (payload = StatusbarItem). Core registers its items through
 * the same calls a plugin would use.
 */

// ---------------------------------------------------------------------------
// Pane contributions. `data.placement` = semantic role for grid presets;
// `data.minWidth/maxWidth/minHeight/maxHeight` = the SAME clamps the app's
// `Pane` props declare — the layout tree sizes zones by weight (percentage)
// but a zone never shrinks/grows past its active pane's clamp.
// Headers are contextual (tree-side): a pane alone in a zone shows no
// header/tab by default; stacked panes show chips. Double-click a zone
// toggles its header either way.
// ---------------------------------------------------------------------------

// ONE render identity for the workspace pane — syncWorkspaceTitle re-registers
// the contribution (new title) and a fresh closure would remount the chat.
const renderWorkspacePane = () => <WiredPane part="chatRoutes" />

// Boot-hidden panes mount behind display:none (instant-toggle contract) — defer
// them to idle so they're off the first-paint path, warm before reveal.
const idle = (node: ReactElement) => <IdleMount>{node}</IdleMount>
// The main tab carries the same session context menu as tile tabs (targets
// the loaded primary session; no menu on a fresh draft).
const wrapWorkspaceTab = (tab: ReactElement) => <WorkspaceTabMenu>{tab}</WorkspaceTabMenu>

/** The `@session` payload for the workspace tab — the loaded primary session,
 *  or null on a fresh draft / full-page view (nothing to link). */
const workspaceDragPayload = (): SessionDragPayload | null => {
  const selected = $selectedStoredSessionId.get()

  if (!selected || $workspaceIsPage.get()) {
    return null
  }

  const stored = $sessions.get().find(s => sessionMatchesStoredId(s, selected))

  return { id: selected, profile: stored?.profile ?? '', title: stored ? storedSessionTitle(stored) : '' }
}

// The main tab drags like a session tile — drop it on a composer to link the
// chat, on a zone/edge to stack/split. Defers (`false`) to the generic pane
// move when there's no loaded session to carry.
const workspaceTabDrag = (event: ReactPointerEvent<HTMLElement>, onTap: () => void) => {
  const payload = workspaceDragPayload()

  if (!payload) {
    return false
  }

  startSessionDrag(payload, event, { onTap })

  return true
}

registry.registerMany([
  {
    id: 'sessions',
    area: 'panes',
    title: translateNow('sidebar.sessions'),
    // Collapsible: leaves the grid on narrow viewports (edge overlay instead).
    // dock: where a RE-ADOPTED pane lands (healed from a stale dismissal) —
    // its default-ish spot beside main, not a random same-placement stack.
    data: {
      placement: 'left',
      tabTitle: () => translateNow('sidebar.sessions'),
      collapsible: true,
      dock: { pane: 'workspace', pos: 'left' },
      revealAliases: ['chat-sidebar'],
      // Standing chrome: no close gestures at all — the tab is shown/hidden
      // (zone menu Show/Hide rows + the auto-registered ⌘K toggle below).
      hideOnly: true,
      width: `${SIDEBAR_DEFAULT_WIDTH}px`,
      minWidth: `${SIDEBAR_DEFAULT_WIDTH}px`,
      maxWidth: `${SIDEBAR_MAX_WIDTH}px`
    },
    render: () => <WiredPane part="sidebar" />
  },
  {
    id: 'workspace',
    area: 'panes',
    // Live-retitled to the loaded session by syncWorkspaceTitle below.
    title: NEW_SESSION_TITLE,
    data: {
      placement: 'main',
      minWidth: '22vw',
      tabDrag: workspaceTabDrag,
      tabWrap: wrapWorkspaceTab,
      uncloseable: true
    },
    render: renderWorkspacePane
  },
  {
    id: 'terminal',
    area: 'panes',
    title: 'terminal',
    // revealOnPreset: choosing a layout that places the terminal (e.g.
    // "Terminal deck") turns takeover on so the zone actually shows, instead of
    // staying collapsed behind the ⌃` toggle. height sizes the fixed track (a
    // single-pane zone declaring a height is a fixed track — the preset weight
    // is moot): a short deck, not a third of the window.
    //
    // NO minHeight: a tool panel drags all the way down to its collapsed
    // header (the sash floors it at COLLAPSED_ZONE_PX and folds the zone to
    // its rail there). A real floor left a sliver of unusable terminal.
    data: {
      placement: 'bottom',
      height: '20vh',
      maxHeight: '80vh',
      revealOnPreset: true,
      lifecycleKeepAlive: true
    },
    render: () => <WiredPane part="terminal" />
  },
  {
    id: 'files',
    area: 'panes',
    title: 'files',
    // dock: re-adoption target after a stale dismissal (see sessions).
    data: {
      placement: 'right',
      collapsible: true,
      dock: { pane: 'workspace', pos: 'right' },
      revealAliases: ['file-browser'],
      width: FILE_BROWSER_DEFAULT_WIDTH,
      minWidth: FILE_BROWSER_MIN_WIDTH,
      maxWidth: FILE_BROWSER_MAX_WIDTH
    },
    render: () => idle(<FilesPane />)
  },
  {
    id: 'review',
    area: 'panes',
    title: 'review',
    // The second right sidebar: hidden until ⌘G ($reviewOpen) — bound below
    // like the other chrome toggles; its zone collapses while hidden.
    data: {
      placement: 'right',
      collapsible: true,
      revealAliases: [REVIEW_PANE_ID],
      width: FILE_BROWSER_DEFAULT_WIDTH,
      minWidth: FILE_BROWSER_MIN_WIDTH,
      maxWidth: FILE_BROWSER_MAX_WIDTH
    },
    render: () => idle(<ReviewPaneContent />)
  }
])

// ---------------------------------------------------------------------------
// Chrome contributions. The title bar and status bar are fixed chrome outside
// the grid, composable through these areas. Everything real lives in the real
// components (TitlebarControls / useStatusbarItems). Sample PLUGIN
// contributions don't live here — they're their own files under `src/plugins/`,
// auto-discovered by discoverBundledPlugins() below.
// ---------------------------------------------------------------------------

registry.registerMany([
  // Titlebar center stays empty on purpose: session title lives in tabs +
  // sidebar; place/cwd lives in the sidebar project tree. Center is drag
  // chrome (plugins can still contribute to titleBar.center if needed).
  // Layout edit mode registers through the SAME declarative surfaces plugins
  // use: a rebindable keybind (collision-checked in the panel) + a ⌘K row
  // whose hotkey hint tracks the live binding.
  {
    id: 'layout.editMode',
    area: KEYBINDS_AREA,
    data: {
      id: 'layout.editMode',
      label: 'Toggle layout edit mode',
      defaults: ['mod+shift+\\'],
      run: toggleLayoutEditMode
    } satisfies KeybindContribution
  },
  paletteToggle({
    id: 'layout.editMode',
    label: 'Toggle layout edit mode',
    action: 'layout.editMode',
    icon: LayoutDashboard,
    keywords: ['layout', 'zones', 'panes', 'edit', 'rearrange'],
    get: () => $layoutEditMode.get(),
    set: enabled => $layoutEditMode.set(enabled)
  }),
  // The agent's write -> see loop: rescan <hermes home>/desktop-plugins
  // without relaunching (same-id reloads dispose the previous incarnation).
  {
    id: 'plugins.reload',
    area: PALETTE_AREA,
    data: {
      id: 'plugins.reload',
      label: 'Reload desktop plugins',
      keywords: ['plugins', 'reload', 'refresh', 'desktop'],
      run: () => void discoverRuntimePlugins()
    } satisfies PaletteContribution
  },
  // The core `::preview{file="…"}` transcript directive — the model (or a
  // skill) renders a workspace HTML file LIVE inside its own message
  // (sandboxed srcdoc iframe; falls back to the classic preview card for
  // non-HTML targets and remote gateways). Also the reference consumer for
  // the `transcript.directives` area plugins register into.
  {
    id: 'transcript.preview',
    area: TRANSCRIPT_DIRECTIVE_AREA,
    data: {
      name: 'preview',
      render: ({ attrs, streaming }) => <InlinePreviewDirective attrs={attrs} streaming={streaming} />
    } satisfies TranscriptDirectiveContribution
  },
  {
    id: 'layout.reset',
    area: PALETTE_AREA,
    data: {
      id: 'layout.reset',
      label: 'Reset layout',
      icon: LayoutDashboard,
      keywords: ['layout', 'reset', 'default', 'panes'],
      run: resetLayoutTree
    } satisfies PaletteContribution
  },
  // Hiding the bar removes the surface that would otherwise offer it back, so
  // ⌘K is the guaranteed door in (alongside the rebindable ⌘⇧S).
  paletteToggle({
    id: 'view.toggleStatusbar',
    label: 'Toggle status bar',
    action: 'view.toggleStatusbar',
    icon: PanelBottom,
    keywords: ['status bar', 'statusbar', 'bottom bar', 'hide', 'show', 'chrome'],
    get: () => $statusbarVisible.get(),
    set: enabled => $statusbarVisible.set(enabled)
  }),
  paletteToggle({
    id: 'view.toggleTabStrip',
    label: 'Toggle tabs',
    action: 'view.toggleTabStrip',
    icon: PanelTop,
    keywords: ['tab strip', 'tab bar', 'tabs', 'header', 'zone', 'hide', 'show', 'chrome'],
    // On-screen truth for the zone the verbs target, not a stored flag: a zone
    // on auto has no stored value, and the row must read as "what pressing
    // this does to what I can see".
    get: () => Boolean(targetZoneTabStripVisible()),
    set: () => void toggleTargetZoneTabStrip()
  }),
  // The keybind panel's non-titlebar door (the keyboard icon is gone).
  {
    id: 'keybinds.panel',
    area: PALETTE_AREA,
    data: {
      id: 'keybinds.panel',
      label: 'Keyboard shortcuts',
      keywords: ['keybinds', 'shortcuts', 'hotkeys', 'keyboard'],
      run: () => window.dispatchEvent(new CustomEvent('hermes:open-keybinds'))
    } satisfies PaletteContribution
  },
  // Profile sharing: bundle the active profile (config, skills, theme, layout)
  // into a portable archive, or adopt someone else's. Both open native dialogs,
  // so the palette closing on select is correct.
  {
    id: 'profile.export',
    area: PALETTE_AREA,
    data: {
      id: 'profile.export',
      label: 'Export profile…',
      icon: Upload,
      keywords: ['profile', 'export', 'share', 'bundle', 'theme', 'settings', 'backup'],
      run: () => void runExportProfileFlow()
    } satisfies PaletteContribution
  },
  {
    id: 'profile.import',
    area: PALETTE_AREA,
    data: {
      id: 'profile.import',
      label: 'Import profile…',
      icon: Download,
      keywords: ['profile', 'import', 'share', 'bundle', 'archive', 'restore'],
      run: () => void runImportProfileFlow()
    } satisfies PaletteContribution
  }
])

// ---------------------------------------------------------------------------
// Layout presets — CHAT (main) always dominates.
// ---------------------------------------------------------------------------

// The REAL default: sessions left, chat main, and the right sidebars in column
// order main | … | review | file-browser (files outermost). Each is its OWN
// zone. Review collapses to nothing while its pane is hidden (⌘G off).
//
// Preview tiles are DYNAMIC panes (like session tiles), so no preset names one:
// they're registered by watchPreviewTiles as tabs open, and dockPaneBeside lands
// each one directly beside the file tree wherever that currently lives — so a
// file double-click still slides a preview open as its own pane next to the
// tree, never as a tab stacked into the files sidebar.
const DEFAULT_TREE = split(
  'row',
  [
    group(['sessions'], { id: 'grp-sessions' }),
    group(['workspace'], { id: 'grp-main' }),
    split(
      'column',
      [
        split(
          'row',
          [group(['review'], { id: 'grp-review' }), group(['files'], { id: 'grp-files' })],
          [1, 1.2],
          'spl-rail'
        ),
        group(['terminal'], { id: 'grp-terminal' })
      ],
      [1.6, 1],
      'spl-right'
    )
  ],
  [1, 3.4, 1.25],
  'spl-root'
)

const FOCUS_TREE = split('row', [group(['sessions']), group(['workspace', 'files', 'review', 'terminal'])], [1, 4.6])

const TERMINAL_TREE = split(
  'column',
  [
    split('row', [group(['sessions']), group(['workspace']), group(['files', 'review'])], [1, 3.2, 1.2]),
    group(['terminal'])
  ],
  [3, 1]
)

const QUAD_TREE = split(
  'column',
  [
    split('row', [group(['sessions', 'files']), group(['workspace'])], [1, 3]),
    split('row', [group(['terminal']), group(['review'])], [1.4, 1])
  ],
  [3, 1]
)

registry.registerMany([
  { id: 'default', area: 'layouts', title: 'Default', order: 0, data: DEFAULT_TREE },
  { id: 'focus', area: 'layouts', title: 'Focus', order: 10, data: FOCUS_TREE },
  { id: 'terminal-deck', area: 'layouts', title: 'Terminal deck', order: 20, data: TERMINAL_TREE },
  { id: 'quad', area: 'layouts', title: 'Quad', order: 30, data: QUAD_TREE }
])

declareDefaultTree(DEFAULT_TREE)

// Bundled plugins load AFTER core, so a same-id contribution from a plugin
// deliberately overrides the core default (last writer wins). Third-party
// runtime plugins will flow through the same discovery seam.
discoverBundledPlugins()

// Plugin panes join the tree by their `placement` hint the moment they
// register — incl. runtime plugins arriving seconds after boot.
watchContributedPanes()

// Session + route (page) tiles: persisted splits register panes docked beside
// main. A popped-out Browser and the HUD have no layout tree — registering
// tiles there would still run, and preview-tile watching would try to dock
// into a tree this window never renders (and, in the HUD, paint a webview
// into the transparent overlay).
if (!isBrowserWindow() && !isHudWindow()) {
  watchSessionTiles()
  startUnrestoredTileTitleBackfill()
  watchRouteTiles()
  watchPreviewTiles()
}

// Composer pop-out state is keyed by layout zone, so drop entries for zones the
// user has since closed or merged away — otherwise a long-lived install keeps a
// row for every split it has ever had.
$layoutTree.subscribe(tree => {
  if (tree) {
    pruneComposerPopoutZones(groupLeafIds(tree))
  }
})

// Mirror sidebar pins into the backend keep-flag so the auto-archive sweep
// never hides a pinned chat (and pre-existing pins migrate transparently).
watchSessionPins()

// Release unread-write guards once a list page confirms the value we wrote.
watchUnreadWriteGuard()

// The main tab reads as its SESSION (the loaded title, "New session" on a
// fresh draft) — a stack of main + tiles is then just a row of session names.
// register() replaces same-id in place; the render fn is the shared constant
// above, so the pane content never remounts.
const syncWorkspaceTitle = () => {
  const selected = $selectedStoredSessionId.get()
  const stored = selected ? $sessions.get().find(s => sessionMatchesStoredId(s, selected)) : null

  registry.register({
    id: 'workspace',
    area: 'panes',
    // The placeholder, not the draft's live name — `tabTitle` below renders
    // that. Keeping it here would re-register the pane on every keystroke.
    // A bot chat reads as its BOT: every canonical Bot Chat is stored under
    // the same name, which told two open bots apart by nothing (#99152).
    title: workspaceOwnerTitle(
      stored ? storedSessionTitle(stored) : NEW_SESSION_TITLE,
      selected ? $botChatScopes.get()[selected] : undefined
    ),
    data: {
      // The tab's status dot — the SAME primitive the sidebar row and session
      // tiles render, so the main tab never disagrees with its sidebar row. A
      // fresh draft has no session to key by, which IS its status: the dot
      // resolves to `draft` and marks the tab rather than leaving a hole.
      tabLead: () => <SessionStatusDot session={stored} storedSessionId={selected} />,
      // A draft's name lives in its composer, not in any session row, so the
      // label subscribes to it directly — typing renames the tab without
      // re-registering the pane.
      tabTitle: stored ? undefined : () => <SessionDraftTitle scope={selected} />,
      // Pages aren't tab-able: the main zone's bar stands down while one shows.
      headerVeto: $workspaceIsPage.get(),
      placement: 'main',
      minWidth: '22vw',
      tabDrag: workspaceTabDrag,
      tabWrap: wrapWorkspaceTab,
      uncloseable: true
    },
    render: renderWorkspacePane
  })
}

$selectedStoredSessionId.listen(syncWorkspaceTitle)
$sessions.listen(syncWorkspaceTitle)
$botChatScopes.listen(syncWorkspaceTitle)
$workspaceOwnerLabels.listen(syncWorkspaceTitle)
$workspaceIsPage.listen(syncWorkspaceTitle)

// Layout reset collapses every session tile into main as a tab (after the
// workspace) instead of re-scattering them — pre-placed before adoption.
registerLayoutResetHandler(stackSessionTilesIntoMain)

// ---------------------------------------------------------------------------
// Titlebar chrome toggles -> tree. The TitlebarControls buttons keep their
// store semantics ($sidebarOpen / $fileBrowserOpen / $panesFlipped); the tree
// reacts — a hidden pane's zone collapses (content stays mounted), the flip
// toggle mirrors the root row.
// ---------------------------------------------------------------------------

// HIDE-STYLE PANES (files, review, preview): the binding lives in the tree
// store — bindPaneVisibility — alongside bindToolPaneCollapse, so both are
// testable against the real function instead of a copy.

// TOOL PANELS (terminal, logs): the binding lives in the tree store —
// bindToolPaneCollapse — so the boot rule it encodes is testable against the
// real function instead of a copy. See its docblock for the semantics.

// SIDES have one source of truth: the TREE. The legacy $panesFlipped flag is
// DERIVED from where the sessions zone actually sits (TitlebarControls maps
// its left/right buttons through it), so dragging sessions across — or
// applying a mirrored preset — remaps the buttons automatically. The flip
// action (⌘\ / titlebar) mirrors the tree only when they disagree.
const sessionsOnRight = () => {
  const tree = $layoutTree.get()

  if (!tree) {
    return null
  }

  const order = allPaneIds(tree)
  const sessions = order.indexOf('sessions')
  const main = order.indexOf('workspace')

  return sessions >= 0 && main >= 0 ? sessions > main : null
}

$layoutTree.subscribe(() => {
  const flipped = sessionsOnRight()

  if (flipped !== null && flipped !== $panesFlipped.get()) {
    $panesFlipped.set(flipped)
  }
})

$panesFlipped.listen(flipped => {
  const current = sessionsOnRight()

  if (current !== null && current !== flipped) {
    mirrorLayoutTree()
  }
})

// POSITIONAL side toggles (titlebar buttons, ⌘B / ⌘J): $sidebarOpen ≙ the
// LEFT side of the main zone, $fileBrowserOpen ≙ the RIGHT — everything on
// that side hides together, whatever panes have been rearranged there.
bindTreeSideVisibility('left', $sidebarOpen, setSidebarOpen)
bindTreeSideVisibility('right', $fileBrowserOpen, setFileBrowserOpen)

// Workspace-scoped surfaces: the file tree and git diff only mean something
// inside a project. A detached chat (no cwd) hides them — their zones
// collapse and the chat absorbs the width; picking a project brings them
// back. The terminal is NOT workspace-gated: unlike the old shell (where it
// rode the rail's row and vanished with it), its zone stands on its own.
const $hasWorkspace = computed($currentCwd, cwd => Boolean(cwd.trim()))

// The tree pane's own presence tracks ⌘J directly, not just the column's
// collapse — otherwise a pane revealed into that shared column would drag the
// tree along with it.
//
// Both get a CLOSER and an OPENER. The closer keeps ⌘J/⌘G truthful when the
// pane is closed from the tab menu; the opener is its mirror, so bringing the
// pane back through the tree (the toggle's reveal path, the rail, a preset)
// writes the store too. Without the opener the boolean went stale the moment
// anything but the toggle showed the pane — the divergence this whole change
// is about.
bindPaneVisibility(
  'files',
  computed([$hasWorkspace, $fileBrowserOpen], (workspace, open) => workspace && open),
  () => setFileBrowserOpen(false),
  () => setFileBrowserOpen(true)
)
// ⌘G — the review sidebar appears/disappears (and comes to the front).
bindPaneVisibility(
  'review',
  computed([$reviewOpen, $hasWorkspace], (open, workspace) => open && workspace),
  closeReview,
  () => openReview($reviewScopeCwd.get(), $reviewScopeTarget.get())
)
// ⌃` / statusbar toggle — the terminal COLLAPSES to a rail (tab stays), not
// hides; PTYs stay alive while collapsed (see PersistentTerminal).
bindToolPaneCollapse(
  'terminal',
  $terminalTakeover,
  () => setTerminalTakeover(false),
  () => setTerminalTakeover(true)
)
// ⌘K door onto the same pane the keybind and statusbar pill flip — was a
// one-way "open" row under Go to, so it never showed on/off and couldn't hide.
// Reads the TREE like every other pane toggle: `$terminalTakeover` stays true
// behind a stacked sibling tab or a minimized zone, which would light the row
// "on" for a terminal that isn't on screen.
registry.register(
  paletteToggle({
    id: 'view.showTerminal',
    label: 'Toggle terminal',
    action: 'view.showTerminal',
    icon: Terminal,
    keywords: ['terminal', 'shell', 'console', 'pty'],
    get: () => isPaneVisible('terminal'),
    set: () => togglePaneVisible('terminal')
  })
)

// Logs are ⌘K-ONLY chrome: the pane contribution EXISTS only while $logsOpen
// is on. Off (the default) keeps logs out of the registry and the tree
// entirely — no secondary tab riding the terminal strip, no preset or
// adoption path that resurrects it. Session-only on purpose (not persisted):
// a fresh boot never re-opens logs automatically. The palette toggle is the
// single door in; tab ✕ / ⌘W / the toggle itself remove it again.
const $logsOpen = atom(false)

let unregisterLogsPane: (() => void) | null = null

const syncLogsPane = (open: boolean) => {
  if (open) {
    unregisterLogsPane ??= registry.register({
      id: 'logs',
      area: 'panes',
      title: 'logs',
      // Same tool-panel sizing rule as the terminal above — no minHeight, so
      // the sash floors it at COLLAPSED_ZONE_PX and folds the zone to its rail
      // rather than leaving a sliver. dock: its OWN zone beside the terminal —
      // never a tab in the terminal's strip.
      data: {
        placement: 'bottom',
        dock: { pane: 'terminal', pos: 'right' },
        height: '20vh',
        maxHeight: '80vh'
      },
      render: () => idle(<LogsPane />)
    })
    // Summoning logs is explicit intent — front it (un-dismisses if a ✕ close
    // left a dismissal record behind).
    revealTreePane('logs')
  } else {
    unregisterLogsPane?.()
    unregisterLogsPane = null

    // No dismissal record — the next toggle-on must re-adopt cleanly. Also
    // sweeps 'logs' out of persisted trees from before it was summon-only.
    // Guarded: removePane rebuilds the tree even for an absent pane, and a
    // no-op boot sweep would commit (and persist) a fresh identical tree.
    const tree = $layoutTree.get()

    if (tree && allPaneIds(tree).includes('logs')) {
      removeTreePane('logs')
    }
  }
}

// Tool-panel tab semantics (✕ / ⌘W route through the store) so the palette
// toggle stays truthful either way.
markCollapsePane('logs')
registerPaneCloser('logs', () => $logsOpen.set(false))
registerPaneOpener('logs', () => $logsOpen.set(true))
syncLogsPane($logsOpen.get())
$logsOpen.listen(syncLogsPane)

registry.register(
  paletteToggle({
    id: 'logs.toggle',
    label: 'Toggle logs',
    icon: FileText,
    keywords: ['logs', 'agent log', 'tail', 'debug'],
    // On-screen, not the store's boolean. Summon-only keeps the two in step
    // while logs sits in its own zone, but the user can still drag it into the
    // terminal's strip or minimize its zone — and then `$logsOpen` reads true
    // with nothing visible, so the row would show "on" and its press would
    // spend itself re-asserting a value it already held.
    get: () => isPaneVisible('logs'),
    set: () => togglePaneVisible('logs')
  })
)

// Hide-only chrome tabs (sessions / Bots) get a ⌘K toggle each — the palette
// door onto the same show/hide the zone menu offers. Auto-registered from the
// panes area so a plugin's hideOnly pane (Bots registers at plugin load, after
// this module runs) gets its row for free; disposers keep it in step when a
// plugin unloads. Registry writes during a subscriber callback are safe (the
// registry snapshots per-area and re-notifies), and re-registering the same
// palette id replaces the row instead of stacking duplicates.
{
  const stripTabToggles = new Map<string, () => void>()

  const syncStripTabToggles = () => {
    const hideOnlyPanes = registry
      .getArea('panes')
      .filter(c => (c.data as { hideOnly?: boolean } | undefined)?.hideOnly)

    const wanted = new Set(hideOnlyPanes.map(c => c.id))

    for (const [paneId, dispose] of stripTabToggles) {
      if (!wanted.has(paneId)) {
        dispose()
        stripTabToggles.delete(paneId)
      }
    }

    for (const pane of hideOnlyPanes) {
      if (stripTabToggles.has(pane.id)) {
        continue
      }

      const title = String(pane.title ?? pane.id)

      stripTabToggles.set(
        pane.id,
        registry.register(
          paletteToggle({
            id: `strip-tab.${pane.id}`,
            label: translateNow('zones.toggleStripTab', title),
            icon: LayoutDashboard,
            keywords: [title.toLowerCase(), 'tab', 'pane', 'sidebar', 'show', 'hide'],
            // On-screen truth, same contract as the logs toggle above.
            get: () => isPaneVisible(pane.id),
            set: visible => {
              if (visible) {
                revealTreePane(pane.id)
              } else {
                setStripTabHidden(pane.id, true)
              }
            }
          })
        )
      )
    }
  }

  syncStripTabToggles()
  registry.subscribeArea('panes', syncStripTabToggles)
}

// YOLO (dangerous-command approval bypass) is a status-bar zap and a /yolo
// command; ⌘K is the third door onto the SAME store function, so a user who
// lives in the palette never has to hunt for the pill.
registry.register(
  paletteToggle({
    id: 'session.yolo',
    label: 'Toggle yolo',
    icon: Zap,
    keywords: ['yolo', 'approvals', 'auto-approve', 'bypass', 'dangerous', 'commands'],
    get: () => $yoloActive.get(),
    set: enabled => void setYoloEnabled(enabled).catch(() => undefined)
  })
)

// Sessions/files Close = collapse their SIDE (⌘B/⌘J truthful, titlebar button
// flips back) — but only while the pane actually lives in that root side
// column. Dragged next to main, a side collapse can't hide it (the collapse
// skips main-bearing children), so Close falls back to dismissal there —
// otherwise ⌘W/Close silently no-op.
registerPaneCloser('sessions', () =>
  paneRootSide('sessions') === 'left' ? setSidebarOpen(false) : dismissTreePane('sessions')
)
registerPaneCloser('files', () =>
  paneRootSide('files') === 'right' ? setFileBrowserOpen(false) : dismissTreePane('files')
)

// ---------------------------------------------------------------------------

interface TitlebarSlotProps {
  area: 'titleBar.center' | 'titleBar.left' | 'titleBar.right'
  className: string
  style?: CSSProperties
}

function TitlebarSlot({ area, className, style }: TitlebarSlotProps) {
  const items = useContributions(area)

  if (items.length === 0) {
    return null
  }

  return (
    <div className={className} style={style}>
      <Slot area={area} />
    </div>
  )
}

export function ContribController() {
  const sidebarOpen = useStore($sidebarOpen)
  const statusbarVisible = useStore($statusbarVisible)

  // HUD mode is the SAME app with its frame removed: the wiring (gateway,
  // sessions, streams, submit) mounts identically, and only the shell around
  // the chat surface differs. Branching here rather than at the window entry
  // is what keeps the HUD's composer the real composer.
  if (isHudWindow()) {
    return (
      <ContribWiring>
        <AppContextMenu />
        <HudShell />
      </ContribWiring>
    )
  }

  if (isBrowserWindow()) {
    return (
      <ContribWiring>
        <BrowserPopoutShell />
      </ContribWiring>
    )
  }

  return (
    <SidebarProvider
      className="h-screen min-h-0 flex-col bg-background"
      onOpenChange={setSidebarOpen}
      open={sidebarOpen}
      style={{ '--sidebar-width': '100%' } as CSSProperties}
    >
      <ContribWiring>
        <AppContextMenu />
        <div
          className="flex h-screen min-h-0 w-screen flex-col bg-(--ui-bg-chrome) text-(--ui-text-primary)"
          // Window-glass hook: this div and the sidebar-wrapper above it are
          // the app shell's two full-window opaque painters; the
          // [data-hermes-glass] rules in styles.css clear them so the tint
          // painted by <body> is the only thing between the page and the
          // vibrancy material.
          data-contrib-shell=""
          style={{ '--titlebar-height': '0px' } as CSSProperties}
        >
          {/* Title bar: fixed chrome outside the grid, composable via slots.
              Layout contract (no contribution can break it):
                - a full-bar DRAG BASE underneath (pointer-events-none, like
                  AppShell's drag strips) — everywhere without content drags
                  the window;
                - each slot region is width-fit, no-drag, pointer-events-auto,
                  so every contribution is clickable by construction;
                - LEFT/RIGHT slots align to the MAIN PANE's geometry via the
                  tree-published --workspace-left/right vars (pure CSS, no rect
                  threading), clamped to clear the REAL TitlebarControls
                  clusters (fixed, z-70); center is truly window-centered. */}
          <div className="relative flex h-[34px] shrink-0 items-center bg-(--ui-sidebar-surface-background) text-xs">
            {/* Drag strips, AppShell-style: cut to AVOID the fixed control
                clusters instead of overlapping them — Electron's no-drag
                carve-out of fixed/transformed elements is unreliable, so a
                full-bar drag base kills their clicks. In-flow slot content
                still carves via its own no-drag wrapper (the same pattern as
                the app's session-title button). */}
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-y-0 left-0 w-(--titlebar-controls-left,14px) [-webkit-app-region:drag]"
            />
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-y-0 left-[calc(var(--titlebar-controls-left,14px)+(var(--titlebar-control-size,24px)*2)+0.75rem)] right-[calc(var(--titlebar-tools-right,0.75rem)+var(--titlebar-tools-width,5.5rem)+0.75rem)] [-webkit-app-region:drag]"
            />
            <TitlebarSlot
              area="titleBar.left"
              className="pointer-events-auto absolute z-10 flex w-max items-center gap-2 [-webkit-app-region:no-drag]"
              style={{
                left: 'max(calc(var(--workspace-left, 0px) + 0.5rem), calc(var(--titlebar-controls-left, 14px) + 2 * var(--titlebar-control-size, 24px) + 1rem))'
              }}
            />
            <TitlebarSlot
              area="titleBar.center"
              className="pointer-events-auto absolute left-1/2 top-1/2 z-10 flex w-max -translate-x-1/2 -translate-y-1/2 items-center gap-2 [-webkit-app-region:no-drag]"
            />
            <TitlebarSlot
              area="titleBar.right"
              className="pointer-events-auto absolute z-10 flex w-max items-center gap-2 [-webkit-app-region:no-drag]"
              style={{
                right:
                  // Five static cluster buttons: four systemTools plus the
                  // always-present right-sidebar toggle (titlebar-controls.tsx).
                  // Keep in sync with wiring.tsx's SYSTEM_TOOL_COUNT.
                  'max(calc(var(--workspace-right, 0px) + 0.5rem), calc(var(--titlebar-tools-right, 0.75rem) + 5 * var(--titlebar-control-size, 24px) + 0.5rem))'
              }}
            />
          </div>

          <LayoutTreeRoot />

          {/* "Close running tab?" — the busy/input-blocked tile close gate. */}
          <SessionTileCloseConfirm />

          {/* The REAL statusbar (model pill, command center, agents, …) with
              statusBar.left/right contributions merged in. Unmounted — not
              just hidden — while toggled off, so its 15s status poll and the
              per-turn readouts stop with it. */}
          {statusbarVisible && <WiredPane part="statusbar" />}
        </div>
      </ContribWiring>
    </SidebarProvider>
  )
}

// Referenced type kept for plugin authors' reference (payload shape of
// statusBar.* contributions).
export type { StatusbarItem }
