import { useState, useEffect, useRef, useMemo, useCallback, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { LocalTask, type Visibility } from './api';
import { useUrlState } from './hooks/useUrlState';
import { useToast } from './hooks/useToast';
import { useFileBrowser } from './hooks/useFileBrowser';
import { useFilePreview } from './hooks/useFilePreview';
import { useTaskOperations } from './hooks/useTaskOperations';
import { useLocalTaskList } from './hooks/useTaskList';
import { useLocalTask, useLocalTaskComments } from './hooks/useTaskDetail';
import { useSubtaskActivities } from './hooks/useSubtaskActivities';
import { useBranding } from './hooks/useBranding';
import { SwipeableTabs } from './components/mobile/SwipeableTabs';
import { useLocalStorageState } from './hooks/useLocalStorageState';
import { useTaskNotifications } from './hooks/useTaskNotifications';
import { DEFAULT_NOTIFY_EVENTS, type NotifyEventSettings } from './lib/notifications';
import { COLUMN_LIST, MOBILE_TAB_LIST, type MobileTabId, type PageId } from './lib/urlState';
import { confirmDiscardUnsaved } from './lib/unsavedGuard';
import { useBackdropClose } from './lib/useBackdropClose';
import { TopBar } from './components/layout/TopBar';
import { NavDrawer } from './components/layout/NavDrawer';
import { useEdgeSwipe } from './hooks/useEdgeSwipe';
import { visibleNavItemsFor } from './components/layout/TopBar';
import { useVisibleDetailTabs } from './components/detail/detailTabs';
import { ResizeHandle } from './components/layout/ResizeHandle';
import { TaskListPanel } from './components/list/TaskListPanel';
import { ChatPane } from './components/chat/ChatPane';
import { LocalDetailPanel } from './components/detail/DetailPanel';
import { CreateTaskDialog } from './components/create/CreateTaskDialog';
import { FilePreview } from './components/files/FilePreview';
import { OutputPreviewProvider } from './lib/output-preview-context';
import { stripOutputPrefix } from './lib/output-path-detect';
import { EmptyState } from './components/shared/EmptyState';
import { SkeletonChatPane } from './components/shared/Skeleton';
import { ChatPetOverlay } from './components/pets/ChatPetOverlay';
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 { UserFolderTab } from './components/userfolder/UserFolderTab';
import { UsagePage } from './components/usage/UsagePage';
import { HelpPage } from './pages/HelpPage';
import { TaskListWithSidePanel } from './components/dashboard/TaskListWithSidePanel';
import type { ConsoleStatus } from './lib/ssh-console-types';
import { CommandPalette } from './components/command/CommandPalette';
import { shouldOpenForKeyEvent, type CommandContext } from './lib/command-palette';
import { setThemePref } from './lib/theme';
import { shouldAutoFocus } from './lib/live-workspace';

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() {
  // 共有ページ: /ui/shared/:token — 認証不要
  const sharedMatch = window.location.pathname.match(/^\/ui\/shared\/([^/]+)/);
  if (sharedMatch) {
    return <SharedView token={sharedMatch[1]} />;
  }

  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 <AppInner isAdmin={isAdmin} authEnabled={authEnabled} user={user} />;
}

function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnabled: boolean; user: AuthUser | null }) {
  const { t } = useTranslation('layout');
  // Apply branding (document.title + --brand-primary CSS var)
  const branding = useBranding();
  const { urlState, setUrlState, pushUrlState } = useUrlState();
  const { status, search, sort, detailTab, mobileTab, taskId: localTaskId } = urlState;
  const dashboardWidget = urlState.dashboardWidget ?? 'worker-status';
  const setDashboardWidget = (slug: string) =>
    setUrlState(prev => ({ ...prev, dashboardWidget: slug }));
  // 認証なしで users ページにアクセスした場合は tasks にフォールバック
  const page = (urlState.page === 'users' && !authEnabled) ? 'tasks' : urlState.page;

  // UI state
  const [detailWidth, setDetailWidth] = useLocalStorageState<'normal' | 'focused'>(
    'ui.detailMode',
    'normal',
  );
  // focused 時の Chat 列幅 (px)。null は default (30vw) を意味する。
  const [focusedChatPx, setFocusedChatPx] = useLocalStorageState<number | null>(
    'ui.focusedChatPx',
    null,
  );
  // Live workspace: auto-enter focused layout while a browser/ssh tab is open.
  // A manual toggle/exit wins for the current view via `override`, which is
  // KEYED to the current (tab,task): when the key changes the stale override is
  // ignored at derivation time (no effect), so re-entering a live tab
  // re-auto-focuses immediately with no stale-frame flicker.
  const [override, setOverride] = useState<{ key: string; value: boolean } | null>(null);
  const overrideKey = `${detailTab}:${localTaskId ?? ''}`;
  // Key-match at derivation time = the leaving frame is immediately correct
  // (no stale flash). The effect then clears the stale override so RETURNING to
  // the same (tab,task) later re-auto-focuses instead of re-applying it.
  const activeOverride = override && override.key === overrideKey ? override.value : null;
  const isFocused =
    activeOverride ?? (shouldAutoFocus(detailTab, localTaskId != null) || detailWidth === 'focused');

  useEffect(() => {
    setOverride((o) => (o && o.key === overrideKey ? o : null));
  }, [overrideKey]);
  const [tabletDetailOpen, setTabletDetailOpen] = useState(false);
  const tabletDetailBackdrop = useBackdropClose(() => setTabletDetailOpen(false));
  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);
  // Detail tabs (with live Browser/SSH gating) for the tablet chat header.
  const visibleDetailTabs = useVisibleDetailTabs(localTaskId);
  const openDetailTab = useCallback((id: string) => {
    setUrlState(prev => ({ ...prev, detailTab: id as typeof detailTab }));
    setTabletDetailOpen(true);
  }, [setUrlState]);

  const openNavDrawer = () => {
    setTabletDetailOpen(false);
    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);

  const panelOpen = localTaskId !== null;

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

  // URL sync
  useEffect(() => { 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();

  // ⌘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;
      setUrlState((prev) => ({ ...prev, page: 'tasks', taskId: 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, setUrlState, 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) => {
      setUrlState(prev => ({ ...prev, page: 'tasks', 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') {
        setUrlState(prev => ({ ...prev, page: 'tasks', taskId: data.taskId }));
      }
    };
    navigator.serviceWorker.addEventListener('message', handler);
    return () => navigator.serviceWorker.removeEventListener('message', handler);
  }, [setUrlState]);

  const localTaskQuery = useLocalTask(localTaskId, panelOpen);
  const localCommentsQuery = useLocalTaskComments(localTaskId, panelOpen);
  const localTasks = localTasksQuery.data ?? [];
  const localTask = localTaskQuery.data ?? null;
  const localComments = localCommentsQuery.data ?? [];
  // Both queries must finish before mounting ChatPane — otherwise the task
  // detail resolves first and ChatPane briefly renders with comments=[],
  // which trips the "メッセージはまだありません" empty-state. data===undefined
  // means not yet loaded; once loaded (even with zero comments) it's at
  // worst [].
  const chatReady = localTask !== null && localCommentsQuery.data !== undefined;
  const hasSubtasks = (localTask?.subtasks?.length ?? 0) > 0;
  const { data: subtaskActivities } = useSubtaskActivities(localTaskId, hasSubtasks);

  // SSH console status (for conditional mobile SSH tab)
  const { data: consoleStatus } = useQuery<ConsoleStatus>({
    queryKey: ['console-status', localTaskId],
    queryFn: async () => {
      const r = await fetch(`/api/local/tasks/${localTaskId}/console/status`);
      return r.ok ? r.json() : { active: false };
    },
    enabled: !!localTaskId,
    refetchInterval: 5000,
  });
  const showSshMobileTab = consoleStatus?.active === true;

  // File browser
  const fileBrowser = useFileBrowser(localTaskId);

  // File preview
  const { previewState, previewLocalFile, previewSubtaskFile, closePreview } = useFilePreview(showToast);

  // Task operations
  const { handleCreateTask, handleComment, handleDelete, handleCancel } = useTaskOperations({
    taskId: localTaskId,
    showToast,
    setUrlState,
    setShowCreateDialog,
  });

  // Close tablet overlay when task changes
  useEffect(() => { setTabletDetailOpen(false); }, [localTaskId]);

  // Counts for TopBar
  const localColumns = COLUMN_LIST.reduce((acc, s) => {
    acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s);
    return acc;
  }, {} as Record<string, LocalTask[]>);

  // File preview handlers (bind taskId and section)
  const handleLocalFilePreview = (filePath: string, name: string) => {
    if (localTaskId) previewLocalFile(localTaskId, fileBrowser.section, filePath, name);
  };
  const handleSubtaskFilePreview = (taskId: number, jobId: string, category: string, filePath: string) => {
    previewSubtaskFile(taskId, jobId, category, filePath);
  };

  // Output path link click handler for the OutputPreviewProvider that
  // wraps the tasks page. Matches paths look like `output/sub/foo.md`
  // — strip the `output/` prefix and pass the relative path to
  // previewLocalFile with section pinned to 'output' (ignoring the
  // current FileBrowser section, which may be 'logs' or 'input').
  const handleOutputPathLinkClick = (matchedPath: string) => {
    if (!localTaskId) return;
    const relative = stripOutputPrefix(matchedPath);
    const displayName = relative.includes('/') ? relative.substring(relative.lastIndexOf('/') + 1) : relative;
    previewLocalFile(localTaskId, 'output', relative, displayName);
  };

  // TaskListPanel shared props
  const taskListProps = {
    localTasks,
    selectedStatus: status,
    sortMode: sort,
    searchQuery: search,
    activeTaskId: localTaskId,
    // Owner scope (自分/他のユーザ). Only active when auth is on — in no-auth mode
    // every task is owned by 'local' and the toggle would be meaningless.
    scope: urlState.scope,
    onScopeChange: (scope: 'mine' | 'others') => setUrlState(prev => ({ ...prev, scope })),
    currentUserId: user?.id ?? null,
    scopeEnabled: authEnabled && !!user,
    onStatusChange: (s: string) => setUrlState(prev => ({ ...prev, status: s as typeof status })),
    onSortChange: (s: string) => setUrlState(prev => ({ ...prev, sort: s as typeof sort })),
    onSearchChange: (q: string) => setUrlState(prev => ({ ...prev, search: q })),
    onSelectTask: (id: number) => setUrlState(prev => ({ ...prev, taskId: id, detailTab: 'overview' as const })),
    onOpenCreate: () => setShowCreateDialog(true),
  };

  // Detail panel shared props
  const detailPanelProps = (overrides?: { detailTab?: string; showWidthToggle?: boolean; onTabChange?: (t: string) => void; onClose?: () => void }) => ({
    task: localTask,
    taskId: localTaskId!,
    section: fileBrowser.section,
    currentPath: fileBrowser.currentPath,
    entries: fileBrowser.entries,
    pathSegments: fileBrowser.pathSegments,
    loading: localTaskQuery.isLoading,
    detailTab: overrides?.detailTab ?? detailTab,
    detailWidth: (isFocused ? 'focused' : 'normal') as 'normal' | 'focused',
    showWidthToggle: overrides?.showWidthToggle ?? true,
    onTabChange: overrides?.onTabChange ?? (t => setUrlState(prev => ({ ...prev, detailTab: t }))),
    onWidthToggle: () => {
      const next = !isFocused;
      setOverride({ key: overrideKey, value: next });
      setDetailWidth(next ? 'focused' : 'normal');
    },
    onClose: overrides?.onClose ?? (() => setUrlState(prev => ({ ...prev, taskId: null, detailTab: 'overview' }))),
    onDelete: handleDelete,
    onSectionChange: fileBrowser.setSection,
    onNavigate: fileBrowser.setCurrentPath,
    onPreview: handleLocalFilePreview,
    onRefresh: fileBrowser.refresh,
    isRefreshing: fileBrowser.isRefreshing,
    onViewFullLog: () => handleLocalFilePreview('activity.log', 'activity.log'),
    subtaskActivities,
    onSubtaskFilePreview: handleSubtaskFilePreview,
    shareToken: localTask?.shareToken ?? null,
  });

  // Layout calculation
  const sidebarWidth = 'clamp(240px, 22vw, 280px)';
  const detailPanelWidth = 'clamp(280px, 26vw, 440px)';  // normal mode 時のみ使用
  const RAIL_PX = 48;
  const HANDLE_PX = 4;
  const MIN_CHAT_PX = 280;
  const MIN_WS_PX = 280;
  const RESERVED_RIGHT = RAIL_PX + HANDLE_PX + MIN_WS_PX;  // = 332

  // focused 用 grid: rail | chat (variable) | handle | workspace
  const focusedGridCols = panelOpen
    ? `${RAIL_PX}px clamp(${MIN_CHAT_PX}px, var(--chat-w, 30vw), calc(100% - ${RESERVED_RIGHT}px)) ${HANDLE_PX}px minmax(${MIN_WS_PX}px, 1fr)`
    : `${RAIL_PX}px minmax(0, 1fr)`;
  // normal 用 grid (現状を維持)
  const normalGridCols = panelOpen
    ? `${sidebarWidth} minmax(280px, 1fr) ${detailPanelWidth}`
    : `${sidebarWidth} minmax(0, 1fr)`;
  const gridStyle: React.CSSProperties = isFocused
    ? {
        gridTemplateColumns: focusedGridCols,
        ['--chat-w' as string]: focusedChatPx !== null ? `${focusedChatPx}px` : '30vw',
      }
    : {
        gridTemplateColumns: normalGridCols,
      };

  // Dynamic mobile tab list: always show Browser, conditionally show SSH
  const mobileVisibleTabs: Array<{ id: MobileTabId; label: string }> = [
    { id: 'chat', label: 'Chat' },
    { id: 'overview', label: 'Overview' },
    { id: 'activity', label: 'Progress' },
    { id: 'files', label: 'Files' },
    { id: 'trace', label: 'Trace' },
    { id: 'browser', label: 'Browser' },
    ...(showSshMobileTab ? [{ id: 'ssh' as MobileTabId, label: 'SSH' }] : []),
  ];
  const mobileVisibleTabIds = mobileVisibleTabs.map(t => t.id);

  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 === '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 === 'userfolder' && <div className="flex-1 min-h-0 overflow-hidden"><UserFolderTab showToast={showToast} /></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>}

      {page === 'tasks' && <OutputPreviewProvider openOutputPath={handleOutputPathLinkClick}><div className="flex-1 min-h-0 overflow-hidden">
        {/* モバイル: 単一カラム (< sm = 640px) */}
        <div className="block sm:hidden h-full">
          {!panelOpen ? (
            <div className="p-2 h-full">
              <div className="bg-canvas border border-hairline rounded-md h-full overflow-hidden">
                <TaskListWithSidePanel
                  upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>}
                  activeWidgetSlug={dashboardWidget}
                  onActiveWidgetSlugChange={setDashboardWidget}
                />
              </div>
            </div>
          ) : (
            <MobileDetailFlow>
              <div className="flex-shrink-0 flex border-b border-hairline bg-canvas px-2 pt-[env(safe-area-inset-top)]">
                {mobileVisibleTabs.map(({ id, label }) => (
                  <button
                    key={id}
                    onClick={() => setUrlState(prev => ({ ...prev, mobileTab: id }))}
                    className={`flex-1 py-3 text-xs border-b-2 active:bg-surface-2 active:scale-[0.97] transition-[transform,color,background-color,border-color] duration-100 ${
                      mobileTab === id
                        ? 'text-slate-900 border-accent font-semibold'
                        : 'text-slate-500 border-transparent hover:text-slate-800 font-medium'
                    }`}
                  >
                    {label}
                  </button>
                ))}
                <button
                  aria-label={t('shell.close')}
                  onClick={() => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId }))}
                  className="px-3 py-3 text-slate-400 hover:text-slate-800 active:scale-[0.92] active:text-slate-700 transition-[transform,color] duration-100"
                >
                  <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round">
                    <path d="M4 4l8 8M12 4l-8 8" />
                  </svg>
                </button>
              </div>
              <div className="flex-1 min-h-0 overflow-hidden">
                <SwipeableTabs
                  tabs={mobileVisibleTabIds}
                  activeTab={mobileTab}
                  onTabChange={(id) => setUrlState(prev => ({ ...prev, mobileTab: id }))}
                  onSwipeBackFromFirst={() => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId }))}
                  renderTab={(id, preview) => (
                    // While dragging, browser (noVNC iframe) / ssh (WebSocket)
                    // peek as a light placeholder; the real panel mounts on commit.
                    preview && (id === 'browser' || id === 'ssh')
                      ? <div className="h-full w-full flex items-center justify-center bg-canvas text-slate-400 text-sm font-medium">{id === 'browser' ? t('shell.browser') : 'SSH'}</div>
                    : id === 'chat'
                      ? (chatReady
                          ? <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} />
                          : <SkeletonChatPane />)
                      : (localTaskId
                          ? <LocalDetailPanel
                              {...detailPanelProps({
                                detailTab: id === 'overview' ? 'overview'
                                  : id === 'activity' ? 'activity'
                                  : id === 'trace' ? 'trace'
                                  : id === 'browser' ? 'browser'
                                  : id === 'ssh' ? 'ssh'
                                  : 'files',
                                showWidthToggle: false,
                                onTabChange: t => setUrlState(prev => ({ ...prev, mobileTab: t as MobileTabId })),
                                onClose: () => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId })),
                              })}
                            />
                          : null)
                  )}
                />
              </div>
              {/* Mobile-only pet overlay. Anchored to the MobileDetailFlow
                  wrapper (which has `relative`) so the pet stays visible
                  across all tabs, not just Chat. Tablet+ uses the
                  ChatPane-internal instance instead. */}
              {localTask && (
                <ChatPetOverlay
                  taskId={localTask.id}
                  taskStatus={localTask.latestJob?.status ?? null}
                  currentActivity={localTask.latestJob?.currentActivity ?? null}
                  workerId={localTask.latestJob?.workerId ?? null}
                  lastBackendId={localTask.latestJob?.lastBackendId ?? null}
                  className="sm:hidden"
                />
              )}
            </MobileDetailFlow>
          )}
        </div>

        {/* タブレット: 2カラム (sm 〜 xl)。3列デスクトップは横幅が足りないと詳細列が
            狭まりタブが折り返すため、切替を xl(1280px) まで上げて中間帯はこの
            2列+詳細オーバーレイで運用する。 */}
        <div className="hidden sm:grid xl:hidden gap-2 p-2 h-full" style={{ gridTemplateColumns: 'clamp(220px, 30vw, 280px) minmax(0, 1fr)' }}>
          <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
            <TaskListWithSidePanel
              upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>}
              activeWidgetSlug={dashboardWidget}
              onActiveWidgetSlugChange={setDashboardWidget}
            />
          </div>
          <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
            {chatReady ? (
              <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} detailTabs={visibleDetailTabs} activeDetailTab={detailTab} onSelectDetailTab={openDetailTab} />
            ) : panelOpen ? (
              <SkeletonChatPane />
            ) : (
              <EmptyState title={t('emptyState.title')} description={t('emptyState.desc')} onCreateTask={() => setShowCreateDialog(true)} />
            )}
          </div>
        </div>

        {/* デスクトップ: >= xl (1280px). normal=3 列、focused=rail/chat/handle/ws=4 列 */}
        <div
          className="hidden xl:grid gap-2 p-2 h-full"
          data-focused-grid={isFocused ? '1' : undefined}
          style={gridStyle}
        >
          {/* col 1: list or rail. wrapper が bg/border を保持。 */}
          <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
            <TaskListWithSidePanel
              upper={
                isFocused
                  ? <TaskListPanel
                      {...taskListProps}
                      mode="rail"
                      onExitFocused={() => { setOverride({ key: overrideKey, value: false }); setDetailWidth('normal'); }}
                    />
                  : <div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} mode="list" /></div>
              }
              activeWidgetSlug={dashboardWidget}
              onActiveWidgetSlugChange={setDashboardWidget}
              defaultCollapsed={isFocused}
            />
          </div>
          {/* col 2: Chat */}
          <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
            {chatReady ? (
              <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} />
            ) : panelOpen ? (
              <SkeletonChatPane />
            ) : (
              <EmptyState title={t('emptyState.title')} description={t('emptyState.descCenter')} onCreateTask={() => setShowCreateDialog(true)} />
            )}
          </div>
          {/* col 3: Resize handle (focused + panelOpen 時のみ) */}
          {isFocused && panelOpen && (
            <ResizeHandle
              onResize={(px) => {
                const grid = document.querySelector<HTMLElement>('[data-focused-grid="1"]');
                if (grid) grid.style.setProperty('--chat-w', `${px}px`);
              }}
              onResizeEnd={(px) => setFocusedChatPx(px)}
              onReset={() => setFocusedChatPx(null)}
              railPx={RAIL_PX}
              minChatPx={MIN_CHAT_PX}
              minWorkspacePx={MIN_WS_PX}
              handlePx={HANDLE_PX}
            />
          )}
          {/* col 4: Workspace (detail) */}
          {panelOpen && (
            <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
              {localTaskId && <LocalDetailPanel {...detailPanelProps()} />}
            </div>
          )}
        </div>
      </div>

      {/* Tablet: detail overlay */}
      {tabletDetailOpen && panelOpen && (
        <div className="hidden sm:block xl:hidden fixed inset-0 bg-black/40 z-40" {...tabletDetailBackdrop}>
          <div className="absolute right-0 top-0 bottom-0 bg-canvas shadow-2xl flex flex-col overflow-hidden" style={{ width: 'min(480px, 90vw)' }} onClick={e => e.stopPropagation()}>
            {localTaskId && (
              <LocalDetailPanel
                {...detailPanelProps({
                  showWidthToggle: false,
                  onClose: () => setTabletDetailOpen(false),
                })}
              />
            )}
          </div>
        </div>
      )}
      </OutputPreviewProvider>}

      {showCreateDialog && (
        <CreateTaskDialog
          onClose={() => { setShowCreateDialog(false); setCreateInitialPiece(null); }}
          onSubmit={handleCreateTask}
          initialPiece={createInitialPiece ?? undefined}
        />
      )}
      {previewState && (
        <FilePreview
          name={previewState.name}
          content={previewState.content}
          imageSrc={previewState.imageSrc}
          markdownImageBaseUrl={previewState.markdownImageBaseUrl}
          onClose={closePreview}
          taskId={previewState.taskId}
          section={previewState.section}
          filePath={previewState.filePath}
          editable={previewState.editable}
          trustedHtmlUrl={user || !authEnabled ? previewState.trustedHtmlUrl : 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>
  );
}

/**
 * Mobile detail wrapper that adds horizontal swipe navigation between
 * the Chat / Overview / Progress / Files / Trace tabs. Tap-to-switch
 * via the tab bar still works (the swipe handler ignores touches that
 * start on form controls / buttons / anchors).
 */
function MobileDetailFlow({ children }: { children: ReactNode }) {
  // The horizontal tab swipe now lives in <SwipeableTabs> (the content area)
  // so it can yield to native scroll inside wide content and follow the finger.
  // This wrapper only provides `relative` for the app-level mobile pet overlay.
  return (
    <div className="relative flex flex-col h-full">
      {children}
    </div>
  );
}
