import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSetupState } from './hooks/useSetupState';
import { SetupWizard } from './components/setup/SetupWizard';
import { createLocalTask, fetchLocalTask, type CreateLocalTaskInput, type Visibility } from './api';
import { useUrlState } from './hooks/useUrlState';
import { useToast } from './hooks/useToast';
import { useLocalTaskList } from './hooks/useTaskList';
import { useBranding } from './hooks/useBranding';
import { useLocalStorageState } from './hooks/useLocalStorageState';
import { useTaskNotifications } from './hooks/useTaskNotifications';
import { DEFAULT_NOTIFY_EVENTS, type NotifyEventSettings } from './lib/notifications';
import { normalizeLegacyTasksUrl, parseLegacyTasksUrl, buildUiUrlStateSearch, type PageId } from './lib/urlState';
import { confirmDiscardUnsaved } from './lib/unsavedGuard';
import { TopBar } from './components/layout/TopBar';
import { NavDrawer } from './components/layout/NavDrawer';
import { useEdgeSwipe } from './hooks/useEdgeSwipe';
import { visibleNavItemsFor } from './components/layout/TopBar';
import { CreateTaskDialog } from './components/create/CreateTaskDialog';
import { SettingsPage } from './pages/SettingsPage';
import { PiecesPage } from './pages/PiecesPage';
import { SchedulesPage } from './pages/SchedulesPage';
import { UsersPage } from './pages/UsersPage';
import { AdminCaptchaPage } from './pages/AdminCaptchaPage';
import { SharedView } from './pages/SharedView';
import { SharedAppView } from './pages/SharedAppView';
import { JoinSpace } from './components/spaces/JoinSpace';
import { matchPublicRoute } from './lib/publicRoute';
import { UsagePage } from './components/usage/UsagePage';
import { HelpPage } from './pages/HelpPage';
import { SpacesPage } from './components/spaces/SpacesPage';
import { CrossSpaceCalendar } from './components/spaces/CrossSpaceCalendar';
import { useSpaces } from './hooks/useSpaces';
import { CommandPalette } from './components/command/CommandPalette';
import { shouldOpenForKeyEvent, type CommandContext } from './lib/command-palette';
import { setThemePref } from './lib/theme';

export interface AuthUser {
  id: string;
  email: string;
  name: string | null;
  avatarUrl: string | null;
  role: 'admin' | 'user';
  orgIds?: string[];
  defaultVisibility?: Visibility;
  defaultVisibilityOrgId?: string | null;
}

type AuthMode =
  | { mode: 'disabled' }
  | { mode: 'loading' }
  | { mode: 'authenticated'; user: AuthUser }
  | { mode: 'unauthenticated' };

export function useAuthState(): AuthMode {
  const { data, error, isLoading } = useQuery({
    queryKey: ['auth', 'me'],
    queryFn: async () => {
      const res = await fetch('/api/auth/me');
      if (res.status === 404) return { mode: 'disabled' as const };
      if (res.status === 401) return { mode: 'unauthenticated' as const };
      if (!res.ok) throw new Error('Auth check failed');
      const user = await res.json();
      return { mode: 'authenticated' as const, user };
    },
    retry: false,
    staleTime: 5 * 60 * 1000,
  });

  if (isLoading) return { mode: 'loading' };
  if (error || !data) return { mode: 'disabled' };
  return data;
}

export function App() {
  // 認証ゲートの外側で扱う公開ルートをパスマッチ（純関数）で判定する。
  const publicRoute = matchPublicRoute(window.location.pathname);
  if (publicRoute) {
    // 共有タスク: /ui/shared/:token — 認証不要
    if (publicRoute.kind === 'shared') return <SharedView token={publicRoute.token} />;
    // 公開アプリ: /ui/app/:token — 認証不要・read-only
    if (publicRoute.kind === 'app') return <SharedAppView token={publicRoute.token} />;
    // 招待リンク: /ui/invite/:token — 認証は必要だが、未ログイン時は JoinSpace 自身が
    // ログイン導線（returnTo 付き）を出すので、ここでは認証ゲートを通さず直接表示する。
    if (publicRoute.kind === 'invite') return <JoinSpace token={publicRoute.token} />;
  }

  return <AuthenticatedApp />;
}

function AuthenticatedApp() {
  const auth = useAuthState();

  // Redirect to login if unauthenticated
  if (auth.mode === 'unauthenticated') {
    window.location.href = '/auth/login';
    return null;
  }

  // Show loading spinner while checking auth
  if (auth.mode === 'loading') {
    return (
      <div className="h-dvh flex items-center justify-center bg-slate-50">
        <div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
      </div>
    );
  }

  const isAdmin = auth.mode === 'authenticated' ? auth.user.role === 'admin' : true;
  const authEnabled = auth.mode !== 'disabled';
  const user = auth.mode === 'authenticated' ? auth.user : null;

  return <SetupGate isAdmin={isAdmin} authEnabled={authEnabled} user={user} />;
}

// First-run gate: when the runtime has no usable LLM, show the full-screen
// setup wizard instead of the app. Fail-open — if the status check errors, fall
// through to the app rather than blocking it.
function SetupGate({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnabled: boolean; user: AuthUser | null }) {
  const { data: setup, isLoading } = useSetupState();
  const queryClient = useQueryClient();

  if (isLoading) {
    return (
      <div className="h-dvh flex items-center justify-center bg-slate-50">
        <div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
      </div>
    );
  }

  if (setup?.needsSetup) {
    const canConfigure = !setup.authActive || isAdmin;
    if (canConfigure) {
      return (
        <SetupWizard
          status={setup}
          onComplete={() => {
            queryClient.invalidateQueries({ queryKey: ['setup'] });
            queryClient.invalidateQueries({ queryKey: ['auth'] });
            queryClient.invalidateQueries({ queryKey: ['config'] });
            queryClient.invalidateQueries({ queryKey: ['workers'] });
          }}
        />
      );
    }
    return (
      <div className="h-dvh flex items-center justify-center bg-slate-50 px-4">
        <div className="max-w-md text-center">
          <h1 className="text-lg font-semibold text-slate-800 mb-2">Setup needed</h1>
          <p className="text-sm text-slate-600">
            This server has no language model configured yet. Ask an administrator to finish setup from Settings.
          </p>
        </div>
      </div>
    );
  }

  return <AppInner isAdmin={isAdmin} authEnabled={authEnabled} user={user} />;
}

function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnabled: boolean; user: AuthUser | null }) {
  const { t } = useTranslation('layout');
  const { t: tList } = useTranslation('list');
  const queryClient = useQueryClient();
  // Apply branding (document.title + --brand-primary CSS var)
  const branding = useBranding();
  const { urlState, setUrlState, pushUrlState, replaceUrlSearch } = useUrlState();
  // Latest urlState for reads inside async callbacks (closures capture a stale
  // value otherwise). Updated every render; refs are safe to write during render.
  const urlStateRef = useRef(urlState);
  urlStateRef.current = urlState;
  // 認証なしで users ページにアクセスした場合は spaces にフォールバック
  const page = (urlState.page === 'users' && !authEnabled) ? 'spaces' : urlState.page;

  // 個人ワークスペースの解決。spaces 一覧から「自分が所有する kind === 'personal'」
  // を拾う。バックエンドは既に個人スペースを所有者のみ可視に絞るが、二重の防御と
  // して owner 一致も明示チェックする（admin が他ユーザーの個人スペースを誤って
  // 自分のものとして拾わないように）。認証なしモードでは synthetic user が唯一の
  // 個人スペースを所有するので user=null でも従来通り解決する。
  // 一覧が空/ローディング中なら personalSpaceId は null（graceful skeleton）。
  const { data: spaces } = useSpaces();
  const personalSpaceId =
    spaces?.find(
      s => s.kind === 'personal' && (user == null || s.ownerId === user.id),
    )?.id ?? null;

  // Legacy `?page=tasks` deep links (the Tasks tab was removed in M2) are
  // normalized onto the workspace model once spaces have loaded. We wait for the
  // personal workspace to resolve so the rewrite lands on the right rail, then
  // replaceState (no history entry) and let useUrlState re-read. Runs at most
  // once per legacy URL: after the rewrite, page!=='tasks' so the guard exits.
  //
  // M3: a task deep-link must land on the workspace that actually OWNS the task,
  // not the personal one. We resolve the owning spaceId via a single-task fetch
  // (id→spaceId) and only then rewrite. Spaceless rows (spaceId === null) and
  // fetch failures fall back to the personal workspace. A bare `?page=tasks`
  // (no task) has no task to resolve and always targets personal.
  const legacyTasksRedirectDone = useRef(false);
  useEffect(() => {
    if (legacyTasksRedirectDone.current) return;
    if (typeof window === 'undefined') return;
    // Wait until spaces resolve so the personal workspace id is known.
    if (spaces === undefined) return;
    const rawSearch = window.location.search.replace(/^\?/, '');
    // parseLegacyTasksUrl decides what counts as legacy (explicit page=tasks OR
    // a bare ?task=ID deep link) and returns null otherwise.
    const parsed = parseLegacyTasksUrl(rawSearch);
    if (parsed == null) return;
    legacyTasksRedirectDone.current = true;

    // Replace synchronously onto the personal workspace (the Tasks page always
    // lived there) in a single history-clean step. No async gap, so there is no
    // window where the URL stays legacy while a fetch is in flight.
    replaceUrlSearch(normalizeLegacyTasksUrl(rawSearch, personalSpaceId)!);

    // If the deep-linked task actually belongs to a different (case) workspace,
    // correct the rail afterwards. This is a normal navigation that composes
    // with user actions: we only apply it if the user is still on that chat, so
    // a slow fetch can't yank them back from somewhere they navigated to.
    if (parsed.legacyTaskId != null) {
      void (async () => {
        try {
          const task = await fetchLocalTask(parsed.legacyTaskId!);
          if (!task.spaceId || task.spaceId === personalSpaceId) return;
          // Correct the workspace with another history-clean replace — but only
          // if the URL still shows this chat (no page param = on spaces, same
          // chat id). If the user navigated away during the fetch, leave them be.
          const cur = new URLSearchParams(window.location.search);
          if (cur.get('page') == null && cur.get('chat') === String(parsed.legacyTaskId)) {
            cur.set('space', task.spaceId);
            replaceUrlSearch(cur.toString());
          }
        } catch {
          // Unknown/forbidden task: the personal-workspace landing already opened the chat.
        }
      })();
    }
  }, [spaces, personalSpaceId, replaceUrlSearch]);

  // Auto-open the personal workspace on startup so the workspaces page never
  // lands the user on an empty rail (the Tasks tab used to be the default
  // landing). One-shot: a ref guards it so a later in-session deselect (mobile
  // "back to list") is NOT immediately re-selected. We only act when no space /
  // chat is targeted, the legacy redirect didn't already pick one, and the
  // personal workspace has resolved. A fresh reload is a new "startup" and may
  // re-open personal — that matches "起動時は個人ワークスペースが開きます".
  const personalSpaceAutoOpened = useRef(false);
  useEffect(() => {
    if (personalSpaceAutoOpened.current) return;
    if (page !== 'spaces') return;
    if (urlState.spaceId || urlState.spaceTaskId) return;
    if (!personalSpaceId) return;
    personalSpaceAutoOpened.current = true;
    setUrlState(prev => ({ ...prev, spaceId: personalSpaceId }));
  }, [page, urlState.spaceId, urlState.spaceTaskId, personalSpaceId, setUrlState]);

  // UI state
  const [navDrawerOpen, setNavDrawerOpen] = useState(false);
  const hamburgerRef = useRef<HTMLButtonElement>(null);
  // compactMode is measured by TopBar (actual fit) and reported up here so the
  // nav drawer / edge-swipe stay in sync with whether the hamburger is shown.
  const [compactMode, setCompactMode] = useState(false);
  const visibleNav = visibleNavItemsFor(isAdmin, authEnabled);

  const openNavDrawer = () => {
    setNavDrawerOpen(true);
  };

  // Shared navigation handler used by both TopBar and NavDrawer.
  // Guards against discarding unsaved edits before switching pages.
  const handleNavigatePage = useCallback((p: PageId) => {
    if (p === page) {
      setNavDrawerOpen(false);
      return;
    }
    if (!confirmDiscardUnsaved()) return;
    setUrlState(prev => ({ ...prev, page: p }));
    setNavDrawerOpen(false);
  }, [page, setUrlState]);

  const edgeSwipe = useEdgeSwipe({
    enabled: compactMode && !navDrawerOpen,
    onOpen: openNavDrawer,
  });

  useEffect(() => {
    if (!compactMode) setNavDrawerOpen(false);
  }, [compactMode]);

  const [showCreateDialog, setShowCreateDialog] = useState(false);
  /**
   * When set, CreateTaskDialog opens with the given piece preselected (and a
   * help-themed placeholder). Used by HelpPage "AI に聞く" so the user lands
   * directly in the help assistant. Cleared on dialog close.
   */
  const [createInitialPiece, setCreateInitialPiece] = useState<string | null>(null);

  // Toast
  const { toast, showToast } = useToast();

  // URL sync. While a legacy Tasks deep link is still pending normalization,
  // skip the push: otherwise this would pushState the parsed fallback (page=tasks
  // → spaces) and pollute history *before* the legacy redirect's replaceState
  // runs, leaving the back button pointing at the broken pre-redirect URL.
  useEffect(() => {
    // Don't sync stale legacy state. `urlState.taskId` is the Tasks-page field;
    // post-M2 nothing sets it in-session, so it is non-null only right after
    // readUiUrlState parsed a legacy `?task=`/`?page=tasks` URL. While it is set
    // we're mid-redirect: the legacy effect replaceState's the URL onto
    // space/chat, but within that same flush urlState is still the pre-replace
    // value. Pushing it would re-serialize `task=...` and undo the redirect (and
    // pollute history). Suppressing on taskId covers the whole window — first
    // render before spaces load, and the post-replace flush — and never misfires
    // in-session because taskId is dead there.
    // Hold the sync while a legacy redirect is pending so it never pushes the
    // pre-redirect URL into history (and never undoes the redirect's replace):
    //  - taskId != null: the post-replace stale flush — the redirect replaced
    //    the URL but urlState (which still carries the dead Tasks-page taskId)
    //    hasn't caught up yet. taskId is never set in-session post-M2, so this
    //    only ever matches a just-parsed legacy URL.
    //  - parseLegacyTasksUrl on the live URL: the first-render window before the
    //    redirect runs, covering every legacy form including bare/invalid
    //    `?task=` and `?page=tasks` (same detector the redirect uses).
    if (urlState.taskId != null) return;
    if (typeof window !== 'undefined'
      && parseLegacyTasksUrl(window.location.search.replace(/^\?/, '')) != null) return;
    pushUrlState(urlState);
  }, [urlState, pushUrlState]);

  // Data queries — split per concern so each tab fetches what it needs.
  // Overview/Chat render as soon as task + comments arrive, without waiting
  // for the (potentially large) activity.log. ProgressTab fetches that on
  // its own when mounted.
  const localTasksQuery = useLocalTaskList();

  // Open a task by id inside the workspace (spaces) model. The Tasks tab was
  // removed in M2, so every "open this task" entry point (calendar, OS
  // notifications, ⌘K) resolves the task's owning workspace and lands the user
  // there with the chat selected. Tasks with no space (legacy spaceless rows,
  // M3) fall back to the personal workspace so they still surface somewhere.
  //
  // Tracks the most recent open request so a slow single-task fetch can't
  // override a newer navigation (user clicks task A then B while A resolves).
  const openTaskSeq = useRef(0);
  const handleOpenTaskInSpace = useCallback((id: number, spaceIdHint?: string | null) => {
    const seq = ++openTaskSeq.current;
    // Select the chat immediately so the UI responds; only the WORKSPACE rail
    // may need an async resolve below.
    const select = (spaceId: string | undefined) =>
      setUrlState(prev => ({
        ...prev,
        page: 'spaces',
        spaceId: spaceId ?? prev.spaceId,
        spaceTaskId: id > 0 ? id : undefined,
      }));

    // spaceIdHint lets callers that already know the owning workspace (e.g. a
    // just-created task, before the list query refetches) skip the lookup and
    // avoid a wrong personal-workspace fallback.
    if (spaceIdHint !== undefined) {
      // null hint = spaceless row → personal fallback (no fetch needed).
      select(spaceIdHint ?? personalSpaceId ?? undefined);
      return;
    }
    // The list cache is the next-best source: a present row authoritatively
    // gives spaceId (null → personal), so no fetch is needed.
    const cached = localTasksQuery.data?.find(t => t.id === id);
    if (cached) {
      select(cached.spaceId ?? personalSpaceId ?? undefined);
      return;
    }

    // Neither the hint nor the list cache knows the owning workspace (e.g. an OS
    // notification / ⌘K / calendar open before the list query loads). Resolve
    // the real spaceId via a single-task fetch so a non-personal task doesn't
    // mis-land on the personal rail. We select the chat now (so the UI isn't
    // blocked) and CORRECT the rail once the fetch resolves. Spaceless rows and
    // failures fall back to personal.
    select(personalSpaceId ?? undefined);
    if (id <= 0) return;
    void (async () => {
      try {
        const task = await fetchLocalTask(id);
        if (openTaskSeq.current !== seq) return; // superseded by a newer open
        const resolved = task.spaceId ?? personalSpaceId ?? undefined;
        // Correct the rail with a REPLACE (no new history entry, so Back never
        // lands on the bogus "personal + chat" intermediate) and only if the
        // user is STILL on this chat. Judge that from current STATE, not the URL:
        // the initial select() only updated React state and the URL sync runs
        // later, so window.location can still be stale when the fetch resolves.
        if (resolved !== undefined) {
          const st = urlStateRef.current;
          if (st.page === 'spaces' && st.spaceTaskId === id && st.spaceId !== resolved) {
            replaceUrlSearch(buildUiUrlStateSearch({ ...st, spaceId: resolved || undefined }));
          }
        }
      } catch {
        // Personal fallback already applied above.
      }
    })();
  }, [localTasksQuery.data, personalSpaceId, setUrlState, replaceUrlSearch]);

  // ⌘K command palette
  const [cmdkOpen, setCmdkOpen] = useState(false);

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const el = document.activeElement as Element | null;
      const inTerminal = !!el?.closest('.xterm');
      // Monaco / contenteditable bind ⌘K/Ctrl-K as chord prefixes — don't steal.
      const inEditor = !!el?.closest('.monaco-editor, [contenteditable="true"]');
      if (shouldOpenForKeyEvent(e, { inTerminal, inEditor })) {
        e.preventDefault();
        setCmdkOpen((o) => !o);
      }
    };
    window.addEventListener('keydown', onKey, true);
    return () => window.removeEventListener('keydown', onKey, true);
  }, []);

  const cmdkCtx: CommandContext = useMemo(() => ({
    navigatePage: handleNavigatePage,
    openTask: (id: number) => {
      // Opening a task can change page (e.g. from settings) → run the same
      // unsaved-changes guard handleNavigatePage uses, so we don't silently
      // discard an in-progress edit.
      if (!confirmDiscardUnsaved()) return;
      handleOpenTaskInSpace(id);
    },
    setTheme: setThemePref,
    navItems: visibleNav.map((n) => ({ id: n.id, label: t(n.labelKey) })),
    tasks: (localTasksQuery.data ?? []).map((t) => ({ id: t.id, title: t.title })),
  }), [handleNavigatePage, handleOpenTaskInSpace, visibleNav, localTasksQuery.data]);

  // ブラウザ通知設定 (localStorage) — 設定 UI は NotificationsForm が管理
  const [notifyEnabled] = useLocalStorageState<boolean>('notify.enabled', true);
  const [notifyEvents] = useLocalStorageState<NotifyEventSettings>(
    'notify.events',
    DEFAULT_NOTIFY_EVENTS,
  );

  useTaskNotifications({
    tasks: localTasksQuery.data,
    currentUserId: user?.id ?? null,
    enabled: notifyEnabled,
    events: notifyEvents,
    onNotificationClick: (taskId) => {
      handleOpenTaskInSpace(taskId);
    },
  });

  // V2: SW posts `open-task` when the user clicks an OS notification and the
  // SW focuses (or opens) this tab. We route it through the same URL state
  // transition as V1's onclick handler.
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;
    const handler = (e: MessageEvent) => {
      const data = e.data;
      if (!data || typeof data !== 'object') return;
      if (data.type === 'open-task' && typeof data.taskId === 'number') {
        handleOpenTaskInSpace(data.taskId);
      }
    };
    navigator.serviceWorker.addEventListener('message', handler);
    return () => navigator.serviceWorker.removeEventListener('message', handler);
  }, [handleOpenTaskInSpace]);

  const localTasks = localTasksQuery.data ?? [];

  // Create a task from the global dialog (Help assistant "AI に聞く"). The
  // Tasks tab was removed in M2, so the created task opens inside its
  // workspace (personal by default). Space-internal "new chat" has its own
  // flow in SpaceDetail; this path only serves the few non-space entry points.
  const handleGlobalCreateTask = useCallback(async (
    input: CreateLocalTaskInput,
    attachments: Array<{ name: string; contentBase64: string }>,
  ) => {
    try {
      const created = await createLocalTask({ ...input, attachments });
      setShowCreateDialog(false);
      setCreateInitialPiece(null);
      showToast(tList('toast.created', { id: created.task.id }));
      queryClient.invalidateQueries({ queryKey: ['localTasks'] });
      // Use the resolved workspace from the create response (the list query
      // hasn't refetched yet) so a non-personal task opens in its own space.
      handleOpenTaskInSpace(created.task.id, created.task.spaceId ?? input.spaceId ?? null);
    } catch {
      showToast(tList('toast.createFailed'), 'error');
    }
  }, [showToast, queryClient, handleOpenTaskInSpace, tList]);

  return (
    <div className="h-dvh flex flex-col overflow-hidden bg-slate-50 text-slate-900" {...edgeSwipe}>
      <TopBar
        currentPage={page}
        onNavigate={handleNavigatePage}
        isAdmin={isAdmin}
        authEnabled={authEnabled}
        user={user}
        appName={branding.appName}
        logoUrl={branding.logoUrl}
        onOpenDrawer={openNavDrawer}
        hamburgerButtonRef={hamburgerRef}
        navDrawerOpen={navDrawerOpen}
        onOpenCommandK={() => setCmdkOpen(true)}
        onCompactChange={setCompactMode}
      />

      <div role="status" aria-live="polite" aria-atomic="true" className="flex-shrink-0">
        {toast && (
          <div className={
            toast.variant === 'error'
              ? 'mx-4 mt-2 px-4 py-2.5 bg-red-50 dark:bg-red-500/15 border border-red-200 dark:border-red-500/30 rounded-xl text-[13px] text-red-800 dark:text-red-300'
              : 'mx-4 mt-2 px-4 py-2.5 bg-green-50 dark:bg-green-500/15 border border-green-200 dark:border-green-500/30 rounded-xl text-[13px] text-green-800 dark:text-green-300'
          }>
            {toast.message}
          </div>
        )}
      </div>

      {page === 'settings' && <div className="flex-1 min-h-0 overflow-hidden"><SettingsPage isAdmin={isAdmin} /></div>}
      {page === 'spaces' && <div className="flex-1 min-h-0 overflow-hidden"><SpacesPage spaceId={urlState.spaceId} spaceTaskId={urlState.spaceTaskId} chatFilter={{ search: urlState.spaceSearch, status: urlState.spaceStatus, sort: urlState.spaceSort, scope: urlState.spaceScope }} onChatFilterChange={(next) => setUrlState(prev => ({ ...prev, ...(next.search !== undefined ? { spaceSearch: next.search } : {}), ...(next.status !== undefined ? { spaceStatus: next.status } : {}), ...(next.sort !== undefined ? { spaceSort: next.sort } : {}), ...(next.scope !== undefined ? { spaceScope: next.scope } : {}) }))} onSelectSpace={(id) => setUrlState(prev => ({ ...prev, spaceId: id, spaceTaskId: undefined, spaceSearch: '', spaceStatus: 'all', spaceSort: 'updated', spaceScope: 'mine' }))} onSelectSpaceTask={(id) => setUrlState(prev => ({ ...prev, spaceTaskId: id > 0 ? id : undefined }))} onCreateTask={handleGlobalCreateTask} onOpenTask={handleOpenTaskInSpace} /></div>}
      {page === 'calendar' && <div className="flex-1 min-h-0 overflow-hidden"><CrossSpaceCalendar onOpenSpace={(id) => setUrlState(prev => ({ ...prev, page: 'spaces', spaceId: id, spaceTaskId: undefined }))} onOpenTask={handleOpenTaskInSpace} /></div>}
      {page === 'pieces' && <div className="flex-1 min-h-0 overflow-hidden"><PiecesPage showToast={showToast} isAdmin={isAdmin} /></div>}
      {page === 'schedules' && <div className="flex-1 min-h-0 overflow-hidden"><SchedulesPage showToast={showToast} /></div>}
      {page === 'users' && isAdmin && authEnabled && <div className="flex-1 min-h-0 overflow-hidden"><UsersPage /></div>}
      {page === 'captcha' && <div className="flex-1 min-h-0 overflow-hidden"><AdminCaptchaPage isAdmin={isAdmin} /></div>}
      {page === 'usage' && <div className="flex-1 min-h-0 overflow-hidden flex flex-col"><UsagePage /></div>}
      {page === 'help' && <div className="flex-1 min-h-0 overflow-hidden"><HelpPage isAdmin={isAdmin} onAskAi={() => { setCreateInitialPiece('help'); setShowCreateDialog(true); }} selectedId={urlState.help} onSelect={(id) => setUrlState(prev => ({ ...prev, help: id }))} /></div>}


      {showCreateDialog && (
        <CreateTaskDialog
          onClose={() => { setShowCreateDialog(false); setCreateInitialPiece(null); }}
          onSubmit={handleGlobalCreateTask}
          initialPiece={createInitialPiece ?? undefined}
        />
      )}
      {branding.footerText && (
        <footer className="flex-shrink-0 border-t border-slate-200 bg-canvas px-4 py-1.5 text-[10px] text-slate-500 text-center">
          {branding.footerText}
        </footer>
      )}
      <NavDrawer
        open={navDrawerOpen}
        onClose={() => setNavDrawerOpen(false)}
        visibleNav={visibleNav}
        currentPage={page}
        onNavigate={handleNavigatePage}
        appName={branding.appName}
        logoUrl={branding.logoUrl}
        returnFocusRef={hamburgerRef}
      />
      <CommandPalette open={cmdkOpen} onClose={() => setCmdkOpen(false)} ctx={cmdkCtx} />
    </div>
  );
}

