import { useState, useEffect, useCallback, useRef, useMemo, Suspense, startTransition } from 'react';
import { Outlet, useNavigate, useSearchParams, useLocation } from 'react-router-dom';
import { Menu, Moon, Sun, Search, Plus, PanelRight, X, Mail, SlidersHorizontal } from 'lucide-react';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAuth } from '../hooks/useAuth';
import { useTheme, useMediaQuery } from '../hooks/useTheme';
import { useOverlayA11y, moveTabIndex } from '../hooks/useOverlayA11y';
import { api } from '../api/client';
import type { Board, RecentComment, RecentUser, ForumStats, TagCount, User } from '../api/types';
import type { PostHeading } from '../utils/postHeadings';
import { getCachedBoards, getCachedStats, getCachedRecentComments, getCachedRecentUsers, getCachedTags, hasCachedAside, setCachedBoards, setCachedStats, setCachedRecentComments, setCachedRecentUsers, setCachedTags } from '../utils/layoutCache';
import Sidebar, { isNeutralSidebarRoute } from '../components/Sidebar';
import RightPanel from '../components/RightPanel';
import BackToTop from '../components/BackToTop';
import { useForumLimits } from '../hooks/useForumLimits';
import { resolveAsideWidgets } from '../utils/asideWidgets';
import { buildHomeUrl, parseFeedSort } from '../components/FeedSortBar';
import { navigateFeed, PAGE_FORCE_REFRESH_EVENT } from '../utils/feedCache';
import { PAGE_SOFT_REFRESH_COMMIT_EVENT } from '../utils/softRefresh';
import { prefetchRoute, wasColdBootEnsured } from '../utils/prefetchRoute';
import {
  transitionTo,
} from '../utils/spaTransition';
import PostSearchPanel from '../components/search/PostSearchPanel';
import {
  POST_SEARCH_OPEN_EVENT,
  usePostSearch,
} from '../hooks/usePostSearch';
import { cn } from '@/lib/utils';
import { getBoardThemeIndex } from '../utils/boardTheme';
import { loginPath } from '../utils/authRedirect';
import { openForumPost } from '../utils/openPost';
import { refetchSiteBranding, useSiteBranding } from '../hooks/useSiteBranding';
import { useMonitorPageview } from '../hooks/useMonitorPageview';
import SiteBrandMark from '../components/SiteBrandMark';
import SiteFooter from '../components/SiteFooter';
import { userPath } from '../utils/userPath';
import { parsePermalinkID } from '../utils/permalink';
import { ensureSitePagesLoaded } from '../hooks/useSitePages';
import { endHomeHydrate, isHomeHydrating } from '../utils/homeHydrate';
import { getBootUnread } from '../utils/authBoot';

export default function MainLayout() {
  const { user, loading: authLoading, logout } = useAuth();
  const { theme, toggle } = useTheme();
  const { branding } = useSiteBranding();
  useMonitorPageview();
  const mqMobile = useMediaQuery('(max-width: 768px)');
  const hideAside = useMediaQuery('(max-width: 1100px)');
  /** hydrate 首帧强制桌面布局（SSR 为桌面三栏），随后再跟 matchMedia */
  const [forceDesktop, setForceDesktop] = useState(() => isHomeHydrating());
  const isMobile = forceDesktop ? false : mqMobile;
  const nav = useNavigate();
  const loc = useLocation();
  const [params] = useSearchParams();
  const isCompose = loc.pathname.startsWith('/compose') || /\/post\/\d+\/edit$/.test(loc.pathname);

  useEffect(() => {
    if (!forceDesktop) return;
    endHomeHydrate();
    startTransition(() => setForceDesktop(false));
  }, [forceDesktop]);

  const [boards, setBoards] = useState<Board[]>(() => getCachedBoards());
  const [stats, setStats] = useState<ForumStats | null>(() => getCachedStats());
  const [recentComments, setRecentComments] = useState<RecentComment[]>(() => getCachedRecentComments());
  const [recentUsers, setRecentUsers] = useState<RecentUser[]>(() => getCachedRecentUsers());
  const [unreadMessages, setUnreadMessages] = useState(() => getBootUnread());
  const [tags, setTags] = useState<TagCount[]>(() => getCachedTags());
  const [tagsLoading, setTagsLoading] = useState(() => getCachedTags().length === 0);
  const [postOutline, setPostOutline] = useState<{
    headings: PostHeading[];
    scrollRoot: HTMLElement | null;
    title?: string;
    author?: User | null;
    publishedAt?: string;
    viewCount?: number;
  } | null>(null);
  const [asideOpen, setAsideOpen] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [searchPanelOpen, setSearchPanelOpen] = useState(false);
  const searchInputRef = useRef<HTMLInputElement>(null);
  const [asideLoading, setAsideLoading] = useState(() => !hasCachedAside());
  const [boardsLoading, setBoardsLoading] = useState(() => getCachedBoards().length === 0);
  const [layoutRefreshTick, setLayoutRefreshTick] = useState(0);
  const asideEverLoaded = useRef(false);
  /** 冷启动门闩：main.tsx 已预热则可同步放行；否则等齐再呈现 */
  const [shellReady, setShellReady] = useState(() => isCompose || wasColdBootEnsured());
  const coldBootDone = useRef(isCompose || wasColdBootEnsured());
  const bootGen = useRef(0);
  const [boardId, setBoardId] = useState(() => {
    const m = loc.pathname.match(/^\/board\/(\d+(?:\.[A-Za-z0-9]{1,16})?)$/);
    if (m) return parsePermalinkID(m[1]) || 0;
    return Number(params.get('board')) || 0;
  });
  const [keywordDraft, setKeywordDraft] = useState(params.get('keyword') || '');
  const { limits: forumLimits } = useForumLimits();
  const feedSort = parseFeedSort(params.get('sort'), forumLimits.feed_sort_tabs);
  const postSearch = usePostSearch(forumLimits);
  const asideWidgets = useMemo(() => resolveAsideWidgets(forumLimits), [forumLimits]);
  const showTagCloud = asideWidgets.some(w => w.id === 'tag_cloud' && w.enabled);
  const showRecentComments = asideWidgets.some(w => w.id === 'recent_comments' && w.enabled);
  const showRecentUsers = asideWidgets.some(w => w.id === 'recent_users' && w.enabled);

  const asideDrawerRef = useRef<HTMLElement>(null);
  const asideCloseRef = useRef<HTMLButtonElement>(null);
  const sidebarDrawerRef = useRef<HTMLElement>(null);
  const sidebarCloseRef = useRef<HTMLButtonElement>(null);
  const boardBarRef = useRef<HTMLDivElement>(null);

  const closeAside = useCallback(() => setAsideOpen(false), []);
  const closeSidebar = useCallback(() => setSidebarOpen(false), []);
  const openAside = useCallback(() => {
    setSidebarOpen(false);
    setAsideOpen(true);
  }, []);
  const openSidebar = useCallback(() => {
    setAsideOpen(false);
    setSidebarOpen(true);
  }, []);

  useOverlayA11y(asideOpen && hideAside && !isCompose, closeAside, asideDrawerRef, {
    initialFocusRef: asideCloseRef,
  });
  useOverlayA11y(sidebarOpen && isMobile && !isCompose, closeSidebar, sidebarDrawerRef, {
    initialFocusRef: sidebarCloseRef,
  });

  useEffect(() => {
    const m = loc.pathname.match(/^\/board\/(\d+(?:\.[A-Za-z0-9]{1,16})?)$/);
    if (m) {
      setBoardId(parsePermalinkID(m[1]) || 0);
      return;
    }
    setBoardId(Number(params.get('board')) || 0);
  }, [loc.pathname, params]);
  useEffect(() => {
    setKeywordDraft(params.get('keyword') || '');
  }, [params]);
  useEffect(() => {
    setAsideOpen(false);
    setSidebarOpen(false);
  }, [loc.pathname, loc.search]);
  useEffect(() => {
    if (!/^\/post\/\d+/.test(loc.pathname)) setPostOutline(null);
  }, [loc.pathname]);
  useEffect(() => {
    if (!hideAside) setAsideOpen(false);
  }, [hideAside]);
  useEffect(() => {
    if (!isMobile) setSidebarOpen(false);
  }, [isMobile]);

  const openSearchPanel = useCallback(() => setSearchPanelOpen(true), []);

  useEffect(() => {
    const onOpen = () => setSearchPanelOpen(true);
    window.addEventListener(POST_SEARCH_OPEN_EVENT, onOpen);
    return () => window.removeEventListener(POST_SEARCH_OPEN_EVENT, onOpen);
  }, []);

  useEffect(() => {
    if (isCompose) return;
    const onKey = (e: KeyboardEvent) => {
      if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'k') return;
      const tag = (e.target as HTMLElement | null)?.tagName;
      const editable = (e.target as HTMLElement | null)?.isContentEditable;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || editable) return;
      e.preventDefault();
      if (isMobile) {
        setSearchPanelOpen(true);
      } else {
        searchInputRef.current?.focus();
        searchInputRef.current?.select();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [isCompose, isMobile]);
  useEffect(() => {
    if (!asideOpen && !sidebarOpen) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [asideOpen, sidebarOpen]);

  const refreshBoards = useCallback(() => {
    return Promise.all([
      api.boards().then(d => {
        const next = d.boards ?? [];
        setBoards(next);
        setCachedBoards(next);
        return next;
      }).catch(() => [] as Board[]),
      api.stats().then(next => {
        setStats(next);
        setCachedStats(next);
        return next;
      }).catch(() => null),
    ]).finally(() => {
      setBoardsLoading(false);
    });
  }, []);

  useEffect(() => {
    refreshBoards();
    const onRefresh = () => refreshBoards();
    window.addEventListener('boards-refresh', onRefresh);
    return () => window.removeEventListener('boards-refresh', onRefresh);
  }, [refreshBoards]);

  // 冷启动兜底：main 未预热时静默等齐（不打进度条）；站内已预热则保持打开
  useEffect(() => {
    if (isCompose) {
      coldBootDone.current = true;
      setShellReady(true);
      return;
    }
    if (coldBootDone.current || wasColdBootEnsured()) {
      coldBootDone.current = true;
      setShellReady(true);
      setBoardsLoading(false);
      setAsideLoading(false);
      setTagsLoading(false);
      asideEverLoaded.current = true;
      return;
    }

    const gen = ++bootGen.current;
    let cancelled = false;

    (async () => {
      try {
        const path = `${loc.pathname}${loc.search}`;
        const tasks: Promise<unknown>[] = [
          prefetchRoute(path, { force: false }),
          refreshBoards(),
          ensureSitePagesLoaded(),
        ];

        if (!hideAside) {
          if (showRecentComments) {
            tasks.push(
              api.recentComments().then((d) => {
                const next = Array.isArray(d.comments) ? d.comments : [];
                setRecentComments(next);
                setCachedRecentComments(next);
              }).catch(() => undefined),
            );
          }
          if (showRecentUsers) {
            tasks.push(
              api.recentUsers().then((d) => {
                const next = Array.isArray(d.users) ? d.users : [];
                setRecentUsers(next);
                setCachedRecentUsers(next);
              }).catch(() => undefined),
            );
          }
          if (showTagCloud) {
            tasks.push(
              api.tags(40).then((d) => {
                const next = Array.isArray(d.tags) ? d.tags : [];
                setTags(next);
                setCachedTags(next);
              }).catch(() => undefined),
            );
          }
        }

        await Promise.all(tasks);
      } catch {
        // 失败也放行
      } finally {
        if (!cancelled && bootGen.current === gen) {
          asideEverLoaded.current = true;
          setAsideLoading(false);
          setTagsLoading(false);
          setBoardsLoading(false);
          coldBootDone.current = true;
          setShellReady(true);
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [
    isCompose,
    loc.pathname,
    loc.search,
    hideAside,
    showRecentComments,
    showRecentUsers,
    showTagCloud,
    refreshBoards,
  ]);

  const refreshUnreadMessages = useCallback(() => {
    if (!user) {
      setUnreadMessages(0);
      return;
    }
    api.messageUnreadCount()
      .then((r) => setUnreadMessages(r.count || 0))
      .catch(() => setUnreadMessages(0));
  }, [user]);

  useEffect(() => {
    refreshUnreadMessages();
    const onRefresh = () => refreshUnreadMessages();
    window.addEventListener('messages-unread-refresh', onRefresh);
    const timer = window.setInterval(refreshUnreadMessages, 60_000);
    return () => {
      window.removeEventListener('messages-unread-refresh', onRefresh);
      window.clearInterval(timer);
    };
  }, [refreshUnreadMessages]);

  useEffect(() => {
    const syncFromCache = () => {
      // 仅当 cache 有内容时覆盖，避免空数组盖住已渲染的非空 UI
      const nextBoards = getCachedBoards();
      if (nextBoards.length > 0) setBoards(nextBoards);
      setBoardsLoading(false);
      const nextStats = getCachedStats();
      if (nextStats) setStats(nextStats);
      const nextComments = getCachedRecentComments();
      if (nextComments.length > 0 || hasCachedAside()) setRecentComments(nextComments);
      const nextUsers = getCachedRecentUsers();
      if (nextUsers.length > 0 || hasCachedAside()) setRecentUsers(nextUsers);
      const nextTags = getCachedTags();
      if (nextTags.length > 0) setTags(nextTags);
      setTagsLoading(false);
      setAsideLoading(false);
      asideEverLoaded.current = true;
      refreshUnreadMessages();
    };
    const onForce = () => {
      // 旧路径：仍可能被别处派发；尽量静默刷新壳层
      refreshBoards();
      refreshUnreadMessages();
      refetchSiteBranding();
      setLayoutRefreshTick(n => n + 1);
    };
    const onCommit = () => {
      // 软刷新：预热已写入 cache，同一拍同步进 state，不触发分批 loading
      syncFromCache();
    };
    window.addEventListener(PAGE_FORCE_REFRESH_EVENT, onForce);
    window.addEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit);
    return () => {
      window.removeEventListener(PAGE_FORCE_REFRESH_EVENT, onForce);
      window.removeEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit);
    };
  }, [refreshBoards, refreshUnreadMessages]);

  // 标签云：进页/离开发帖页时拉取；不跟 posts-refresh 联动（置顶/推荐等不改标签）
  useEffect(() => {
    if (isCompose || !showTagCloud) return;
    let cancelled = false;
    // 有 session 缓存则静默刷新（软刷新不卸空白）
    if (getCachedTags().length === 0) setTagsLoading(true);
    api.tags(40).then(d => {
      if (cancelled) return;
      const next = Array.isArray(d.tags) ? d.tags : [];
      setTags(next);
      setCachedTags(next);
    }).catch(() => {}).finally(() => {
      if (!cancelled) setTagsLoading(false);
    });
    return () => {
      cancelled = true;
    };
  }, [isCompose, showTagCloud, layoutRefreshTick]);

  const needAsideData = !isCompose && (!hideAside || asideOpen);
  const needRecentComments = needAsideData && showRecentComments;
  useEffect(() => {
    if (!needRecentComments) return;
    let cancelled = false;
    // 无缓存时才显示加载态，有缓存则静默刷新，避免抽屉高度跳动
    if (!asideEverLoaded.current && !hasCachedAside()) {
      setAsideLoading(true);
    }

    api.recentComments().then(d => {
      if (cancelled) return;
      const next = Array.isArray(d.comments) ? d.comments : [];
      setRecentComments(next);
      setCachedRecentComments(next);
    }).catch(() => {}).finally(() => {
      if (!cancelled) {
        asideEverLoaded.current = true;
        setAsideLoading(false);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [needRecentComments, layoutRefreshTick]);

  const needRecentUsers = needAsideData && showRecentUsers;
  useEffect(() => {
    if (!needRecentUsers) return;
    let cancelled = false;
    if (!asideEverLoaded.current && !hasCachedAside()) {
      setAsideLoading(true);
    }

    api.recentUsers().then(d => {
      if (cancelled) return;
      const next = Array.isArray(d.users) ? d.users : [];
      setRecentUsers(next);
      setCachedRecentUsers(next);
    }).catch(() => {}).finally(() => {
      if (!cancelled) {
        asideEverLoaded.current = true;
        setAsideLoading(false);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [needRecentUsers, layoutRefreshTick]);

  const doQuickSearch = () => {
    const { author, titleOnly, scopeBoardId } = postSearch.filters;
    postSearch.submitSearch({
      keyword: keywordDraft,
      author,
      titleOnly,
      scopeBoardId,
    }, { refreshIfSame: true });
  };

  const handleHeaderClear = () => {
    const hasUrlSearch = postSearch.filters.isFiltered;
    setKeywordDraft('');
    if (hasUrlSearch) postSearch.clearSearch();
  };

  const contextBoard = boards.find((b) => b.id === boardId);
  const searchPanelDraft = {
    keyword: keywordDraft,
    author: postSearch.filters.author,
    titleOnly: postSearch.filters.titleOnly,
    scopeBoardId: postSearch.filters.scopeBoardId,
  };

  const openPost = useCallback((id: number, opts?: { floor?: number }) => {
    setAsideOpen(false);
    openForumPost(nav, id, forumLimits.open_posts_in_new_tab, opts);
  }, [nav, forumLimits.open_posts_in_new_tab]);

  const userInitial = user?.nickname?.charAt(0) || '?';
  const isFeedHome = loc.pathname === '/' || /^\/board\/\d+/.test(loc.pathname);
  const outletKeyword = params.get('keyword') || '';
  const outletTag = params.get('tag') || '';
  const outletAuthor = params.get('author') || '';
  // 搜索/标签结果页不选中任何板块芯片（避免看起来仍停在「全部」）
  const mobileActiveBoard =
    isNeutralSidebarRoute(loc.pathname) || !!outletKeyword || !!outletTag || !!outletAuthor
      ? -1
      : boardId;

  const boardChipIds = useMemo(() => [0, ...boards.map(b => b.id)], [boards]);
  const activeChipIndex = Math.max(0, boardChipIds.indexOf(mobileActiveBoard === -1 ? 0 : mobileActiveBoard));

  const isPostDetail = /^\/post\/\d+/.test(loc.pathname) && !/\/edit$/.test(loc.pathname);
  const setPostOutlineSafe = useCallback((outline: {
    headings: PostHeading[];
    scrollRoot: HTMLElement | null;
    title?: string;
    author?: User | null;
    publishedAt?: string;
    viewCount?: number;
  } | null) => {
    setPostOutline(outline);
  }, []);
  const layoutCtx = useMemo<LayoutCtx>(() => ({
    boardId,
    keyword: outletKeyword,
    setBoardId,
    boards,
    boardsLoading,
    stats,
    refreshBoards,
    isMobile,
    setPostOutline: setPostOutlineSafe,
  }), [boardId, outletKeyword, boards, boardsLoading, stats, refreshBoards, isMobile, setPostOutlineSafe]);

  const selectBoardChip = (id: number) => {
    setBoardId(id);
    navigateFeed(nav, buildHomeUrl(id, feedSort, { permalink: forumLimits }));
  };

  const onBoardBarKeyDown = (e: React.KeyboardEvent) => {
    const next = moveTabIndex(e.key, activeChipIndex, boardChipIds.length);
    if (next == null) return;
    e.preventDefault();
    selectBoardChip(boardChipIds[next]);
    requestAnimationFrame(() => {
      const tabs = boardBarRef.current?.querySelectorAll<HTMLElement>('[role="tab"]');
      tabs?.[next]?.focus();
    });
  };

  return (
    <div className={cn('app-shell', forceDesktop && 'ssr-home')}>
      <div className="app-frame">
      <header className="app-header">
        <div className="header-inner">
          {!isCompose && (
            <button
              type="button"
              className="header-icon-btn header-menu-btn"
              onClick={openSidebar}
              aria-label="打开导航菜单"
              aria-expanded={sidebarOpen}
              aria-controls="sidebar-drawer"
              title="导航"
            >
              <Menu size={18} aria-hidden />
            </button>
          )}
          {/* 任意页点 Logo：回首页并强制刷新，不展示会话缓存 */}
          <button type="button" className="header-brand" onClick={() => navigateFeed(nav, '/', { refresh: true })}>
            <SiteBrandMark branding={branding} className="header-logo-mark" />
            <span className="header-logo-text">{branding.name}</span>
          </button>

          {!isCompose && isMobile && (
            <button
              type="button"
              className={cn(
                'header-search-mobile-pill',
                (keywordDraft || postSearch.filters.isFiltered) && 'header-search-mobile-pill--active',
              )}
              onClick={openSearchPanel}
              aria-label="搜索帖子"
            >
              <Search size={15} aria-hidden className="header-search-mobile-pill__icon" />
              <span className="header-search-mobile-pill__text">
                {keywordDraft
                  || postSearch.filters.keyword
                  || postSearch.filters.author
                  || '搜索帖子…'}
              </span>
              {postSearch.filters.hasAdvancedFilters && (
                <span className="header-search-mobile-pill__dot" aria-hidden />
              )}
            </button>
          )}

          {!isCompose && !isMobile && (
          <form
            className="header-search-wrap"
            role="search"
            onSubmit={e => {
              e.preventDefault();
              doQuickSearch();
            }}
          >
            <div className="header-search-row">
              <Search className="header-search-icon" size={16} aria-hidden />
              <input
                ref={searchInputRef}
                className="header-search-input"
                type="search"
                placeholder="搜索帖子…"
                aria-label="搜索帖子"
                value={keywordDraft}
                onChange={e => setKeywordDraft(e.target.value)}
                maxLength={forumLimits.search_keyword_max > 0 ? forumLimits.search_keyword_max : undefined}
                enterKeyHint="search"
              />
              <button
                type="button"
                className={cn(
                  'header-search-filter-btn',
                  postSearch.filters.hasAdvancedFilters && 'header-search-filter-btn--active',
                )}
                onClick={openSearchPanel}
                aria-label="搜索筛选"
                title="筛选"
              >
                <SlidersHorizontal size={15} aria-hidden />
                {postSearch.filters.hasAdvancedFilters && (
                  <span className="header-search-filter-btn__dot" aria-hidden />
                )}
              </button>
              {(keywordDraft || postSearch.filters.isFiltered) && (
                <button
                  type="button"
                  className="header-search-clear"
                  onClick={handleHeaderClear}
                  aria-label="清除搜索"
                >×</button>
              )}
              <kbd className="header-search-kbd" aria-hidden>Ctrl K</kbd>
            </div>
          </form>
          )}

          <div className="header-actions">
            {!isCompose && (
            <button
              type="button"
              className="header-compose-btn"
              onClick={() => user ? void transitionTo(nav, '/compose') : nav(loginPath('/compose'))}
              aria-label="发帖"
            >
              <Plus size={16} aria-hidden />
              <span className="header-compose-btn__label">发帖</span>
            </button>
            )}

            <div className="header-action-group">
              {/* 平板：侧栏收起时用按钮打开社区动态；手机改由导航抽屉入口 */}
              {!isCompose && hideAside && !isMobile && (
                <button
                  type="button"
                  className="header-icon-btn"
                  onClick={openAside}
                  aria-label={isPostDetail ? '打开作者与目录' : '打开社区动态'}
                  aria-expanded={asideOpen}
                  aria-controls="aside-drawer"
                  title={isPostDetail ? '作者与目录' : '社区动态'}
                >
                  <PanelRight size={18} aria-hidden />
                </button>
              )}

              <button
                  type="button"
                  className="header-icon-btn header-theme-btn"
                  onClick={toggle}
                  aria-label={theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
                  title={theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
                >
                  {theme === 'light' ? <Moon size={18} aria-hidden /> : <Sun size={18} aria-hidden />}
                </button>

              {authLoading ? (
                <span className="header-auth-slot header-auth-slot--loading" aria-hidden />
              ) : user ? (
                <>
                <button
                  type="button"
                  className="header-icon-btn header-msg-btn"
                  title={unreadMessages > 0 ? `${unreadMessages} 条未读消息` : '站内消息'}
                  aria-label={unreadMessages > 0 ? `站内消息，${unreadMessages} 条未读` : '站内消息'}
                  onClick={() => void transitionTo(nav, '/messages')}
                >
                  <Mail size={18} aria-hidden />
                  {unreadMessages > 0 && (
                    <span className="header-msg-badge">{unreadMessages > 99 ? '99+' : unreadMessages}</span>
                  )}
                </button>
                <DropdownMenu>
                  <DropdownMenuTrigger asChild>
                    <button type="button" className="header-user-btn" title={user.nickname} aria-label={`用户菜单：${user.nickname}`}>
                      {user.avatar
                        ? <img src={user.avatar} alt="" className="header-user-avatar" loading="lazy" decoding="async" />
                        : <span className="header-user-initial">{userInitial}</span>}
                    </button>
                  </DropdownMenuTrigger>
                  <DropdownMenuContent
                    align="end"
                    className="w-40"
                    onCloseAutoFocus={(e) => e.preventDefault()}
                  >
                    <DropdownMenuItem onClick={() => void transitionTo(nav, userPath(user.id))}>个人主页</DropdownMenuItem>
                    <DropdownMenuItem onClick={() => void transitionTo(nav, '/profile')}>
                      账号设置{typeof user.points === 'number' ? ` · ${user.points} 积分` : ''}
                    </DropdownMenuItem>
                    <DropdownMenuItem onClick={() => void transitionTo(nav, '/messages')}>
                      站内消息{unreadMessages > 0 ? ` (${unreadMessages})` : ''}
                    </DropdownMenuItem>
                    <DropdownMenuItem onClick={() => void transitionTo(nav, '/favorites')}>我的收藏</DropdownMenuItem>
                    {isMobile && (
                      <DropdownMenuItem onClick={toggle}>
                        {theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
                      </DropdownMenuItem>
                    )}
                    {user.role === 'admin' && (
                      <>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem onClick={() => nav('/admin/dashboard')}>管理后台</DropdownMenuItem>
                      </>
                    )}
                    <DropdownMenuSeparator />
                    <DropdownMenuItem onClick={() => logout().then(() => nav('/login'))}>
                      退出登录
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
                </>
              ) : (
                <button type="button" className="header-login-btn" onClick={() => nav(loginPath())}>
                  登录
                </button>
              )}
            </div>
          </div>
        </div>
      </header>

      {!isCompose && (
        <PostSearchPanel
          open={searchPanelOpen}
          onOpenChange={setSearchPanelOpen}
          draft={searchPanelDraft}
          contextBoardId={boardId}
          contextBoardName={contextBoard?.name}
          onSubmit={(input) => {
            const ok = postSearch.submitSearch(input);
            if (ok) setKeywordDraft((input.keyword ?? '').trim());
            return ok;
          }}
          onClear={postSearch.clearSearch}
        />
      )}

      <div className={`app-body${isCompose ? ' app-body--compose' : ''}`}>
        {!isCompose && shellReady && (
          <Sidebar
            boards={boards}
            activeBoard={boardId}
            onSelectBoard={setBoardId}
            boardsLoading={boardsLoading}
            
          />
        )}

        <div className={cn(
          'content-workspace',
          isCompose && 'content-workspace--compose',
          hideAside && !isCompose && 'content-workspace--aside-hidden',
        )}>
        <main
          className={cn(
            'main-content',
            isCompose && 'main-content--compose',
            // 手机 Feed：整栏滚动，板块条 / 排序栏可滚出视口，多露出帖子列表
            isMobile && !isCompose && isFeedHome && 'main-content--feed-mobile-scroll',
          )}
          aria-busy={!shellReady}
        >
          {shellReady && isMobile && !isCompose && isFeedHome && (
            <div
              ref={boardBarRef}
              className="mobile-board-bar"
              role="tablist"
              aria-label="板块"
              onKeyDown={onBoardBarKeyDown}
            >
              <button
                type="button"
                role="tab"
                tabIndex={activeChipIndex === 0 ? 0 : -1}
                aria-selected={mobileActiveBoard === 0}
                className={`board-chip ${mobileActiveBoard === 0 ? 'active' : ''}`}
                onClick={() => selectBoardChip(0)}
              >全部</button>
              {boards.map((b, i) => {
                const themeIdx = getBoardThemeIndex(b);
                const isActive = mobileActiveBoard === b.id;
                const idx = i + 1;
                return (
                  <button
                    key={b.id}
                    type="button"
                    role="tab"
                    tabIndex={activeChipIndex === idx ? 0 : -1}
                    aria-selected={isActive}
                    className={cn(
                      'board-chip',
                      isActive && 'active',
                      isActive && `board-chip--${themeIdx}`,
                    )}
                    onClick={() => selectBoardChip(b.id)}
                  >{b.name}</button>
                );
              })}
            </div>
          )}
          {shellReady ? (
            <Suspense fallback={null}>
              <Outlet context={layoutCtx} />
            </Suspense>
          ) : null}
        </main>

        {!isCompose && (
        <aside className="aside-panel" aria-busy={!shellReady}>
          {shellReady && (
          <RightPanel
            recentComments={recentComments}
            recentUsers={recentUsers}
            tags={tags}
            tagsLoading={tagsLoading}
            stats={stats}
            loading={asideLoading}
            asideWidgets={asideWidgets}
            onPostClick={openPost}
            
            postDetail={isPostDetail ? {
              author: postOutline?.author ?? null,
              publishedAt: postOutline?.publishedAt,
              viewCount: postOutline?.viewCount,
              headings: postOutline?.headings ?? [],
              scrollRoot: postOutline?.scrollRoot ?? null,
              outlineTitle: postOutline?.title,
            } : null}
          />
          )}
        </aside>
        )}
        </div>
      </div>

      {/* 桌面壳层贴底；手机端由各页 InFlowSiteFooter 随内容滚动 */}
      {!isCompose && !isMobile && <SiteFooter />}
      </div>

      {sidebarOpen && isMobile && !isCompose && (
        <div className="sidebar-drawer-root">
          <button
            type="button"
            className="aside-drawer-backdrop"
            aria-label="关闭导航菜单"
            tabIndex={-1}
            onClick={closeSidebar}
          />
          <aside
            id="sidebar-drawer"
            ref={sidebarDrawerRef}
            className="sidebar-drawer"
            role="dialog"
            aria-modal="true"
            aria-label="导航菜单"
          >
            <div className="aside-drawer-head">
              <span>导航</span>
              <button
                ref={sidebarCloseRef}
                type="button"
                className="header-icon-btn"
                aria-label="关闭"
                onClick={closeSidebar}
              >
                <X size={18} aria-hidden />
              </button>
            </div>
            <div className="aside-drawer-body sidebar-drawer-body">
              {shellReady && (
              <Sidebar
                boards={boards}
                activeBoard={boardId}
                onSelectBoard={setBoardId}
                boardsLoading={boardsLoading}
              />
              )}
              <div className="sidebar-drawer-extras">
                <button
                  type="button"
                  className="sidebar-drawer-extra-btn"
                  onClick={() => { closeSidebar(); openAside(); }}
                >
                  <PanelRight size={16} aria-hidden />
                  {isPostDetail ? '作者与目录' : '社区动态'}
                </button>
                <button
                  type="button"
                  className="sidebar-drawer-extra-btn"
                  onClick={toggle}
                >
                  {theme === 'light' ? <Moon size={16} aria-hidden /> : <Sun size={16} aria-hidden />}
                  {theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
                </button>
              </div>
            </div>
          </aside>
        </div>
      )}

      {asideOpen && hideAside && !isCompose && (
        <div className="aside-drawer-root">
          <button
            type="button"
            className="aside-drawer-backdrop"
            aria-label="关闭社区动态"
            tabIndex={-1}
            onClick={closeAside}
          />
          <aside
            id="aside-drawer"
            ref={asideDrawerRef}
            className="aside-drawer"
            role="dialog"
            aria-modal="true"
            aria-label={isPostDetail ? '作者与目录' : '社区动态'}
          >
            <div className="aside-drawer-head">
              <span>{isPostDetail ? '作者与目录' : '社区动态'}</span>
              <button
                ref={asideCloseRef}
                type="button"
                className="header-icon-btn"
                aria-label="关闭"
                onClick={closeAside}
              >
                <X size={18} aria-hidden />
              </button>
            </div>
            <div className="aside-drawer-body">
              <RightPanel
                recentComments={recentComments}
                recentUsers={recentUsers}
                tags={tags}
                tagsLoading={tagsLoading}
                stats={stats}
                loading={asideLoading}
                asideWidgets={asideWidgets}
                onPostClick={openPost}
                postDetail={isPostDetail ? {
                  author: postOutline?.author ?? null,
                  publishedAt: postOutline?.publishedAt,
                  viewCount: postOutline?.viewCount,
                  headings: postOutline?.headings ?? [],
                  scrollRoot: postOutline?.scrollRoot ?? null,
                  outlineTitle: postOutline?.title,
                } : null}
              />
            </div>
          </aside>
        </div>
      )}

      <BackToTop />
    </div>
  );
}

export type LayoutCtx = {
  boardId: number;
  keyword: string;
  setBoardId: (id: number) => void;
  boards: Board[];
  boardsLoading: boolean;
  stats: ForumStats | null;
  refreshBoards: () => void;
  isMobile: boolean;
  /** 详情页上报作者与目录，供右侧栏展示 */
  setPostOutline: (outline: {
    headings: PostHeading[];
    scrollRoot: HTMLElement | null;
    title?: string;
    author?: User | null;
    publishedAt?: string;
    viewCount?: number;
  } | null) => void;
};
