import { useState, useEffect, useRef, type ReactNode } from 'react';
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 { useSwipeNav } from './hooks/useSwipeNav';
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 { TopBar } from './components/layout/TopBar';
import { NavDrawer } from './components/layout/NavDrawer';
import { useEdgeSwipe } from './hooks/useEdgeSwipe';
import { visibleNavItemsFor, useCompactNav } from './components/layout/TopBar';
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 { HelpPage } from './pages/HelpPage';
import { TaskListWithSidePanel } from './components/dashboard/TaskListWithSidePanel';
import type { ConsoleStatus } from './lib/ssh-console-types';

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 }) {
  // 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,
  );
  const [tabletDetailOpen, setTabletDetailOpen] = useState(false);
  const [navDrawerOpen, setNavDrawerOpen] = useState(false);
  const hamburgerRef = useRef<HTMLButtonElement>(null);
  const compactMode = useCompactNav(isAdmin, authEnabled);
  const visibleNav = visibleNavItemsFor(isAdmin, authEnabled);

  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 = (p: PageId) => {
    if (p === page) {
      setNavDrawerOpen(false);
      return;
    }
    if (!confirmDiscardUnsaved()) return;
    setUrlState(prev => ({ ...prev, page: p }));
    setNavDrawerOpen(false);
  };

  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();

  // ブラウザ通知設定 (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,
    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,
    showWidthToggle: overrides?.showWidthToggle ?? true,
    onTabChange: overrides?.onTabChange ?? (t => setUrlState(prev => ({ ...prev, detailTab: t }))),
    onWidthToggle: () => setDetailWidth(prev => prev === 'focused' ? 'normal' : 'focused'),
    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 isFocused = detailWidth === 'focused';
  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}
      />

      <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 border border-red-200 rounded-xl text-[13px] text-red-800'
              : 'mx-4 mt-2 px-4 py-2.5 bg-green-50 border border-green-200 rounded-xl text-[13px] text-green-800'
          }>
            {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 === '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 mobileTab={mobileTab} onTabChange={(id) => setUrlState(prev => ({ ...prev, mobileTab: id }))} onSwipeRightFromEdge={() => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId }))} visibleTabs={mobileVisibleTabIds}>
              <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="閉じる"
                  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 key={mobileTab} className="flex-1 min-h-0 overflow-hidden animate-mobile-tab-swap">
                {mobileTab === 'chat' && (
                  chatReady ? (
                    <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} />
                  ) : (
                    <SkeletonChatPane />
                  )
                )}
                {mobileTab !== 'chat' && localTaskId && (
                  <LocalDetailPanel
                    {...detailPanelProps({
                      detailTab: mobileTab === 'overview' ? 'overview'
                        : mobileTab === 'activity' ? 'activity'
                        : mobileTab === 'trace' ? 'trace'
                        : mobileTab === 'browser' ? 'browser'
                        : mobileTab === 'ssh' ? 'ssh'
                        : 'files',
                      showWidthToggle: false,
                      onTabChange: t => setUrlState(prev => ({ ...prev, mobileTab: t as MobileTabId })),
                      onClose: () => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId })),
                    })}
                  />
                )}
              </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 〜 lg) */}
        <div className="hidden sm:grid lg: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} onOpenDetail={() => setTabletDetailOpen(true)} />
            ) : panelOpen ? (
              <SkeletonChatPane />
            ) : (
              <EmptyState title="スレッドを選択してください" description="左の一覧から選ぶと、会話、進捗、成果物を追えます。" onCreateTask={() => setShowCreateDialog(true)} />
            )}
          </div>
        </div>

        {/* デスクトップ: >= lg (1024px). normal=3 列、focused=rail/chat/handle/ws=4 列 */}
        <div
          className="hidden lg: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={() => 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="スレッドを選択してください" description="左の一覧から選ぶと、会話、進捗、成果物を中央で追えます。" 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 lg:hidden fixed inset-0 bg-black/40 z-40" onClick={() => setTabletDetailOpen(false)}>
          <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}
        />
      )}
      {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}
      />
    </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({
  mobileTab,
  onTabChange,
  onSwipeRightFromEdge,
  visibleTabs,
  children,
}: {
  mobileTab: MobileTabId;
  onTabChange: (tab: MobileTabId) => void;
  onSwipeRightFromEdge?: () => void;
  visibleTabs: MobileTabId[];
  children: ReactNode;
}) {
  const swipe = useSwipeNav({
    onSwipeLeft: () => {
      const idx = visibleTabs.indexOf(mobileTab);
      if (idx >= 0 && idx < visibleTabs.length - 1) {
        onTabChange(visibleTabs[idx + 1]);
      }
    },
    onSwipeRight: () => {
      const idx = visibleTabs.indexOf(mobileTab);
      if (idx > 0) {
        onTabChange(visibleTabs[idx - 1]);
      } else if (idx === 0) {
        onSwipeRightFromEdge?.();
      }
    },
  });
  // `relative` is required so the app-level mobile pet overlay (rendered
  // inside this wrapper) can anchor with position: absolute.
  return (
    <div className="relative flex flex-col h-full" {...swipe}>
      {children}
    </div>
  );
}
