import { useEffect, useState } from 'react';
import { Database, Mail, Shield, Server, SlidersHorizontal, KeyRound, FolderGit2, Palette, HardDrive } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Spinner } from '@/components/ui/spinner';
import { notify } from '@/lib/notify';
import { api } from '../../api/client';
import { useAdminGuard } from '../../layouts/AdminLayout';
import { invalidateForumLimitsCache } from '../../hooks/useForumLimits';
import { DEFAULT_BRANDING, seedSiteBrandingCache } from '../../hooks/useSiteBranding';
import { clearAllFeedCache } from '../../utils/feedCache';
import { normalizeAsideWidgets, resolveAsideWidgets, mergeForumLimitsWithAsideWidgets, resolveSavedAsideWidgets } from '../../utils/asideWidgets';
import { normalizeFeedSortTabs } from '../../utils/feedSortTabs';
import AsideWidgetList from '../../components/admin/AsideWidgetList';
import FeedSortTabList from '../../components/admin/FeedSortTabList';
import type { AdminSettings, ForumLimits, MailConfig, OIDCConfig, OAuthClient, GiteaSyncConfig, StorageConfig, SiteBranding, AsideWidget } from '../../api/types';
import { DEFAULT_ASIDE_WIDGETS, DEFAULT_FEED_SORT_TABS } from '../../api/types';

type TabId = 'branding' | 'limits' | 'mail' | 'oidc' | 'gitea' | 'storage' | 'filter' | 'system';

type NumberLimitKey = {
  [K in keyof ForumLimits]: ForumLimits[K] extends number ? K : never;
}[keyof ForumLimits];

type SettingRow = {
  key: NumberLimitKey;
  label: string;
  unit?: string;
  hint?: string;
  min?: number;
};

type SettingSection = {
  id: string;
  title: string;
  summary: string;
  rows: SettingRow[];
};

const SETTING_SECTIONS: SettingSection[] = [
  {
    id: 'rule',
    title: '编辑规则',
    summary: '控制普通用户修改自己帖子 / 评论的时限（0 = 不限）',
    rows: [
      { key: 'post_edit_window_hours', label: '帖子可编辑时限', unit: '小时', hint: '0 = 不限', min: 0 },
      { key: 'comment_edit_window_minutes', label: '评论可编辑时限', unit: '分钟', hint: '0 = 不限', min: 0 },
    ],
  },
  {
    id: 'rate',
    title: '操作限流',
    summary: '同一用户或 IP 在窗口期内的最大请求次数',
    rows: [
      { key: 'rate_limit_window_sec', label: '限流窗口', unit: '秒', min: 10 },
      { key: 'rate_limit_post', label: '发帖', unit: '次', min: 1 },
      { key: 'rate_limit_comment', label: '评论', unit: '次', min: 1 },
      { key: 'rate_limit_register', label: '注册', unit: '次', min: 1 },
      { key: 'rate_limit_login', label: '登录', unit: '次', min: 1 },
    ],
  },
  {
    id: 'content',
    title: '内容长度',
    summary: '发帖与评论的字数上限，服务端强制校验',
    rows: [
      { key: 'post_title_max', label: '帖子标题', unit: '字', min: 1 },
      { key: 'post_tags_max', label: '帖子标签', unit: '字', hint: '0 = 不限', min: 0 },
      { key: 'post_content_max', label: '帖子正文', unit: '字', hint: '0 = 不限', min: 0 },
      { key: 'comment_max', label: '评论内容', unit: '字', min: 1 },
    ],
  },
  {
    id: 'search',
    title: '搜索与列表',
    summary: '关键词长度与首页每页条数',
    rows: [
      { key: 'search_keyword_min', label: '关键词最短', unit: '字', min: 0 },
      { key: 'search_keyword_max', label: '关键词最长', unit: '字', min: 1 },
      { key: 'page_size_default', label: '每页显示条数', unit: '条', hint: '首页列表分页大小', min: 1 },
    ],
  },
  {
    id: 'user',
    title: '用户账号',
    summary: '注册、改密、头像与签名限制',
    rows: [
      { key: 'password_min_len', label: '密码最短', unit: '位', min: 4 },
      { key: 'avatar_max_mb', label: '头像上限', unit: 'MB', min: 1 },
      { key: 'signature_max', label: '签名上限', unit: '字', min: 0 },
    ],
  },
];

type BoolLimitKey =
  | 'open_posts_in_new_tab'
  | 'open_content_links_in_new_tab';

const NAV_TOGGLES: { key: BoolLimitKey; label: string; hint: string }[] = [
  {
    key: 'open_posts_in_new_tab',
    label: '打开帖子时新开标签页',
    hint: '首页、热门、收藏等入口打开帖子详情',
  },
  {
    key: 'open_content_links_in_new_tab',
    label: '帖子正文链接新开标签页',
    hint: '正文内的外链与站内链接均在新标签打开',
  },
];

const TABS: { id: TabId; label: string; icon: typeof SlidersHorizontal }[] = [
  { id: 'branding', label: '站点与呈现', icon: Palette },
  { id: 'limits', label: '论坛限制', icon: SlidersHorizontal },
  { id: 'mail', label: '邮件服务', icon: Mail },
  { id: 'oidc', label: 'OIDC / SSO', icon: KeyRound },
  { id: 'gitea', label: 'Gitea 同步', icon: FolderGit2 },
  { id: 'storage', label: '对象存储', icon: HardDrive },
  { id: 'filter', label: '敏感词', icon: Shield },
  { id: 'system', label: '系统维护', icon: Server },
];

const EMPTY_MAIL: MailConfig = {
  enabled: false,
  host: '',
  port: 465,
  username: '',
  from: '',
  from_name: '姜十三论坛',
  encryption: 'ssl',
  has_password: false,
};

const EMPTY_OIDC: OIDCConfig = {
  enabled: false,
  root_url: '',
  ready: false,
  group_claim: 'groups',
  admin_group: 'gitea-admin',
  user_group: 'gitea-users',
  client_count: 0,
};

/** 就绪需：已启用 + 已持久化 ROOT_URL + 至少一个启用中的应用 */
function oidcStatusLabel(oidc: OIDCConfig, appCount: number): string {
  if (oidc.ready) return '已就绪';
  if (!oidc.enabled) return '未启用';
  const reasons: string[] = [];
  // discovery_url 仅在服务端已保存 ROOT_URL 时返回；表单里填写但未点保存时仍为空
  if (!oidc.discovery_url) reasons.push('保存 ROOT_URL');
  if (appCount < 1 && (oidc.client_count ?? 0) < 1) reasons.push('至少一个应用');
  if (reasons.length === 0) reasons.push('点击「保存全局设置」刷新状态');
  return `未就绪（需${reasons.join('、')}）`;
}

const EMPTY_GITEA: GiteaSyncConfig = {
  enabled: false,
  base_url: '',
  has_token: false,
  sync_interval_min: 60,
  ready: false,
  repo_count: 0,
};

const EMPTY_STORAGE: StorageConfig = {
  type: 'local',
  endpoint: '',
  region: 'us-east-1',
  bucket: '',
  access_key: '',
  public_base_url: '',
  prefix: '',
  force_path_style: true,
  has_secret_key: false,
  ready: true,
  image_delivery: 'webp',
};

function giteaStatusLabel(gitea: GiteaSyncConfig): string {
  if (gitea.ready) return `已就绪 · ${gitea.repo_count} 个仓库`;
  if (!gitea.enabled) return '未启用';
  const reasons: string[] = [];
  if (!gitea.base_url.trim()) reasons.push('BASE_URL');
  if (!gitea.has_token) reasons.push('Token');
  if (reasons.length === 0) reasons.push('保存后生效');
  return `未就绪（需${reasons.join('、')}）`;
}

function storageStatusLabel(storage: StorageConfig): string {
  if (storage.type === 'local') return '本地磁盘';
  if (storage.ready) return 'S3 已就绪';
  const reasons: string[] = [];
  if (!storage.endpoint.trim()) reasons.push('Endpoint');
  if (!storage.bucket.trim()) reasons.push('Bucket');
  if (!storage.access_key.trim()) reasons.push('Access Key');
  if (!storage.has_secret_key) reasons.push('Secret Key');
  if (!storage.public_base_url.trim()) reasons.push('公开访问地址');
  if (reasons.length === 0) reasons.push('保存后生效');
  return `未就绪（需${reasons.join('、')}）`;
}

function SettingTable({
  sections,
  limits,
  onChange,
}: {
  sections: SettingSection[];
  limits: ForumLimits;
  onChange: (key: NumberLimitKey, value: string) => void;
}) {
  return (
    <div className="admin-settings-sections">
      {sections.map(section => (
        <section key={section.id} className="admin-settings-section" id={`settings-${section.id}`}>
          <div className="admin-settings-section-head">
            <h3>{section.title}</h3>
            <p>{section.summary}</p>
          </div>
          <div className="admin-settings-table" role="group" aria-label={section.title}>
            {section.rows.map(row => (
              <div key={row.key} className="admin-settings-row">
                <label htmlFor={`limit-${row.key}`} className="admin-settings-row-label">
                  {row.label}
                </label>
                <div className="admin-settings-row-input">
                  <Input
                    id={`limit-${row.key}`}
                    type="number"
                    min={row.min ?? 0}
                    value={limits[row.key]}
                    onChange={e => onChange(row.key, e.target.value)}
                    className="admin-settings-input"
                  />
                  {row.unit && <span className="admin-settings-unit">{row.unit}</span>}
                </div>
                <span className="admin-settings-row-hint">{row.hint ?? ''}</span>
              </div>
            ))}
          </div>
        </section>
      ))}
    </div>
  );
}

export default function AdminSettingsPage() {
  const { ready } = useAdminGuard();
  const [settings, setSettings] = useState<AdminSettings | null>(null);
  const [limits, setLimits] = useState<ForumLimits | null>(null);
  const [asideWidgets, setAsideWidgets] = useState<AsideWidget[]>(DEFAULT_ASIDE_WIDGETS);
  const [branding, setBranding] = useState<SiteBranding>(DEFAULT_BRANDING);
  const [mail, setMail] = useState<MailConfig>(EMPTY_MAIL);
  const [oidc, setOidc] = useState<OIDCConfig>(EMPTY_OIDC);
  const [gitea, setGitea] = useState<GiteaSyncConfig>(EMPTY_GITEA);
  const [storage, setStorage] = useState<StorageConfig>(EMPTY_STORAGE);
  const [oauthClients, setOauthClients] = useState<OAuthClient[]>([]);
  const [clientForm, setClientForm] = useState({
    client_id: 'gitea',
    name: 'Gitea',
    redirect_uris: 'https://git.iioio.com/user/oauth2/jiang13/callback',
    enabled: true,
  });
  const [editingClientId, setEditingClientId] = useState<number | null>(null);
  const [revealedSecret, setRevealedSecret] = useState<string | null>(null);
  const [testTo, setTestTo] = useState('');
  const [filterWords, setFilterWords] = useState('');
  const [activeTab, setActiveTab] = useState<TabId>('branding');
  const [loading, setLoading] = useState(true);
  const [backing, setBacking] = useState(false);
  const [savingBranding, setSavingBranding] = useState(false);
  const [uploadingBrand, setUploadingBrand] = useState<'logo' | 'favicon' | 'og_image' | null>(null);
  const [savingForum, setSavingForum] = useState(false);
  const [savingMail, setSavingMail] = useState(false);
  const [savingOidc, setSavingOidc] = useState(false);
  const [savingGitea, setSavingGitea] = useState(false);
  const [savingStorage, setSavingStorage] = useState(false);
  const [syncingGitea, setSyncingGitea] = useState(false);
  const [savingClient, setSavingClient] = useState(false);
  const [testingMail, setTestingMail] = useState(false);
  const [savingFilter, setSavingFilter] = useState(false);

  useEffect(() => {
    if (!ready) return;
    api.adminSettings()
      .then(s => {
        setSettings(s);
        const loadedAsideWidgets = normalizeAsideWidgets(
          resolveAsideWidgets({
            aside_widgets: s.limits?.aside_widgets,
            aside_show_tag_cloud: s.limits?.aside_show_tag_cloud ?? false,
            aside_show_recent_comments: s.limits?.aside_show_recent_comments ?? false,
            aside_show_friend_links: s.limits?.aside_show_friend_links ?? true,
          }),
        );
        setAsideWidgets(loadedAsideWidgets);
        const baseLimits: ForumLimits = {
          open_posts_in_new_tab: true,
          open_content_links_in_new_tab: true,
          aside_show_tag_cloud: false,
          aside_show_recent_comments: false,
          aside_show_friend_links: true,
          aside_show_showcase: false,
          aside_widgets: loadedAsideWidgets,
          nav_show_friend_links: true,
          footer_show_friend_links: true,
          nav_show_showcase: false,
          footer_show_showcase: false,
          feed_list_style: 'title',
          feed_sort_tabs: normalizeFeedSortTabs(s.limits?.feed_sort_tabs ?? DEFAULT_FEED_SORT_TABS),
          permalink_enabled: false,
          permalink_ext: 'html',
          ...s.limits,
        };
        setLimits(mergeForumLimitsWithAsideWidgets(baseLimits, loadedAsideWidgets));
        setBranding({ ...DEFAULT_BRANDING, ...(s.branding ?? {}) });
        setMail({ ...EMPTY_MAIL, ...s.mail, password: '' });
        setOidc({ ...EMPTY_OIDC, ...(s.oidc ?? {}) });
        setGitea({ ...EMPTY_GITEA, ...(s.gitea ?? {}), token: '' });
        setStorage({ ...EMPTY_STORAGE, ...(s.storage ?? {}), secret_key: '' });
        setOauthClients(s.oauth_clients ?? []);
        setFilterWords(s.filter_words);
        if (s.mail?.from) setTestTo(s.mail.from);
      })
      .finally(() => setLoading(false));
  }, [ready]);

  const handleLimitChange = (key: NumberLimitKey, value: string) => {
    const n = parseInt(value, 10);
    if (Number.isNaN(n)) return;
    setLimits(prev => prev ? { ...prev, [key]: n } : prev);
  };

  const handleBoolLimitChange = (key: BoolLimitKey, checked: boolean) => {
    setLimits(prev => prev ? { ...prev, [key]: checked } : prev);
  };

  const handleAsideWidgetsChange = (widgets: AsideWidget[]) => {
    const normalized = normalizeAsideWidgets(widgets);
    setAsideWidgets(normalized);
    setLimits(prev => prev ? mergeForumLimitsWithAsideWidgets(prev, normalized) : prev);
  };

  const applySavedForumLimits = (savedWidgets: AsideWidget[], response: ForumLimits): ForumLimits => {
    const nextWidgets = resolveSavedAsideWidgets(savedWidgets, response.aside_widgets);
    const merged = mergeForumLimitsWithAsideWidgets(response, nextWidgets);
    setAsideWidgets(nextWidgets);
    setLimits(merged);
    return merged;
  };

  const applyBranding = (next: SiteBranding) => {
    setBranding({ ...DEFAULT_BRANDING, ...next });
    setSettings(s => s ? { ...s, branding: next } : s);
    seedSiteBrandingCache(next);
  };

  const handleSaveBranding = async () => {
    if (!limits) return;
    setSavingBranding(true);
    try {
      const r = await api.adminUpdateBranding(branding);
      applyBranding(r.branding);
      const forumPayload = mergeForumLimitsWithAsideWidgets(limits, asideWidgets);
      // 伪静态、右侧栏、浏览与链接、列表呈现同属站点呈现，一并保存
      const forum = await api.adminUpdateForumSettings(forumPayload);
      applySavedForumLimits(asideWidgets, forum.limits);
      clearAllFeedCache();
      notify.success('站点设置已保存');
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingBranding(false);
    }
  };

  const handleUploadBrandAsset = async (kind: 'logo' | 'favicon' | 'og_image', file: File | undefined) => {
    if (!file) return;
    setUploadingBrand(kind);
    try {
      const r = await api.adminUploadBrandingAsset(kind, file);
      notify.success(r.message);
      applyBranding(r.branding);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '上传失败');
    } finally {
      setUploadingBrand(null);
    }
  };

  const handleClearBrandAsset = async (kind: 'logo' | 'favicon' | 'og_image') => {
    setUploadingBrand(kind);
    try {
      const r = await api.adminClearBrandingAsset(kind);
      notify.success(r.message);
      applyBranding(r.branding);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '清除失败');
    } finally {
      setUploadingBrand(null);
    }
  };

  const handleSaveForumSettings = async () => {
    if (!limits) return;
    setSavingForum(true);
    try {
      const forumPayload = mergeForumLimitsWithAsideWidgets(limits, asideWidgets);
      const r = await api.adminUpdateForumSettings(forumPayload);
      notify.success(r.message);
      invalidateForumLimitsCache();
      clearAllFeedCache();
      const merged = applySavedForumLimits(asideWidgets, r.limits);
      setSettings(s => s ? { ...s, limits: merged } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingForum(false);
    }
  };

  const handleSaveMailSettings = async () => {
    setSavingMail(true);
    try {
      const payload: MailConfig = {
        ...mail,
        password: mail.password?.trim() ? mail.password : undefined,
      };
      const r = await api.adminUpdateMailSettings(payload);
      notify.success(r.message);
      setMail({ ...r.mail, password: '' });
      setSettings(s => s ? { ...s, mail: r.mail } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingMail(false);
    }
  };

  const handleSaveOidcSettings = async () => {
    setSavingOidc(true);
    try {
      const r = await api.adminUpdateOIDCSettings(oidc);
      notify.success(r.message);
      setOidc({ ...EMPTY_OIDC, ...r.oidc });
      setSettings(s => s ? { ...s, oidc: r.oidc } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingOidc(false);
    }
  };

  const handleSaveGiteaSettings = async () => {
    setSavingGitea(true);
    try {
      const payload: GiteaSyncConfig = {
        ...gitea,
        token: gitea.token?.trim() ? gitea.token : undefined,
      };
      const r = await api.adminUpdateGiteaSettings(payload);
      notify.success(r.message);
      setGitea({ ...EMPTY_GITEA, ...r.gitea, token: '' });
      setSettings(s => s ? { ...s, gitea: r.gitea } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingGitea(false);
    }
  };

  const handleSaveStorageSettings = async () => {
    setSavingStorage(true);
    try {
      const payload: StorageConfig = {
        ...storage,
        secret_key: storage.secret_key?.trim() ? storage.secret_key : undefined,
      };
      const r = await api.adminUpdateStorageSettings(payload);
      notify.success(r.message);
      setStorage({ ...EMPTY_STORAGE, ...r.storage, secret_key: '' });
      setSettings(s => s ? { ...s, storage: r.storage } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingStorage(false);
    }
  };

  const handleSyncGitea = async () => {
    setSyncingGitea(true);
    try {
      const r = await api.adminSyncGitea();
      notify.success(r.message);
      setGitea({ ...EMPTY_GITEA, ...r.gitea, token: '' });
      setSettings(s => s ? { ...s, gitea: r.gitea } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '同步失败');
    } finally {
      setSyncingGitea(false);
    }
  };

  const refreshClients = (clients: OAuthClient[], nextOidc?: OIDCConfig) => {
    setOauthClients(clients);
    if (nextOidc) {
      setOidc({ ...EMPTY_OIDC, ...nextOidc });
      setSettings(s => s ? { ...s, oidc: nextOidc, oauth_clients: clients } : s);
    } else {
      setSettings(s => s ? { ...s, oauth_clients: clients } : s);
    }
  };

  const resetClientForm = () => {
    setEditingClientId(null);
    setClientForm({
      client_id: 'gitea',
      name: 'Gitea',
      redirect_uris: 'https://git.iioio.com/user/oauth2/jiang13/callback',
      enabled: true,
    });
  };

  const handleSaveOAuthClient = async () => {
    setSavingClient(true);
    try {
      if (editingClientId) {
        const r = await api.adminUpdateOAuthClient(editingClientId, {
          name: clientForm.name,
          redirect_uris: clientForm.redirect_uris,
          enabled: clientForm.enabled,
        });
        notify.success(r.message);
        if (r.client.client_secret) setRevealedSecret(r.client.client_secret);
        const list = await api.adminListOAuthClients();
        refreshClients(list.clients, r.oidc);
        resetClientForm();
      } else {
        const r = await api.adminCreateOAuthClient({
          client_id: clientForm.client_id,
          name: clientForm.name,
          redirect_uris: clientForm.redirect_uris,
          enabled: clientForm.enabled,
        });
        notify.success(r.message);
        if (r.client.client_secret) setRevealedSecret(r.client.client_secret);
        const list = await api.adminListOAuthClients();
        refreshClients(list.clients, r.oidc);
        resetClientForm();
      }
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingClient(false);
    }
  };

  const handleRotateSecret = async (id: number) => {
    if (!window.confirm('确定轮换密钥？旧密钥将立即失效。')) return;
    setSavingClient(true);
    try {
      const row = oauthClients.find(c => c.id === id);
      if (!row) return;
      const r = await api.adminUpdateOAuthClient(id, {
        name: row.name,
        redirect_uris: row.redirect_uris,
        enabled: row.enabled,
        rotate_secret: true,
      });
      notify.success(r.message);
      if (r.client.client_secret) setRevealedSecret(r.client.client_secret);
      const list = await api.adminListOAuthClients();
      refreshClients(list.clients, r.oidc);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '轮换失败');
    } finally {
      setSavingClient(false);
    }
  };

  const handleDeleteOAuthClient = async (id: number) => {
    if (!window.confirm('确定删除该 OAuth 应用？')) return;
    try {
      const r = await api.adminDeleteOAuthClient(id);
      notify.success(r.message);
      const list = await api.adminListOAuthClients();
      refreshClients(list.clients, r.oidc);
      if (editingClientId === id) resetClientForm();
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '删除失败');
    }
  };

  const handleTestMail = async () => {
    if (!testTo.trim()) {
      notify.error('请填写测试收件邮箱');
      return;
    }
    setTestingMail(true);
    try {
      const r = await api.adminTestMail(testTo.trim());
      notify.success(r.message);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '发送失败');
    } finally {
      setTestingMail(false);
    }
  };

  const handleSaveFilterWords = async () => {
    setSavingFilter(true);
    try {
      const r = await api.adminUpdateFilterWords(filterWords);
      notify.success(r.message);
      setSettings(s => s ? { ...s, filter_words: filterWords, filter_word_count: r.word_count } : s);
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '保存失败');
    } finally {
      setSavingFilter(false);
    }
  };

  const handleBackup = async () => {
    setBacking(true);
    try {
      const r = await api.adminBackup();
      notify.success(r.message);
      window.location.href = r.download;
    } catch (e: unknown) {
      notify.error(e instanceof Error ? e.message : '备份失败');
    } finally {
      setBacking(false);
    }
  };

  if (!ready || loading) {
    return <div className="flex justify-center py-16"><Spinner size="lg" /></div>;
  }
  if (!settings || !limits) return null;

  return (
    <div className="admin-settings-page">
      <header className="admin-page-head">
        <h1>系统设置</h1>
        <p>管理论坛运行规则、邮件、OIDC/SSO、敏感词与数据维护</p>
      </header>

      <nav className="admin-settings-tabs" aria-label="设置分类">
        {TABS.map(({ id, label, icon: Icon }) => (
          <button
            key={id}
            type="button"
            className={`admin-settings-tab${activeTab === id ? ' active' : ''}`}
            onClick={() => setActiveTab(id)}
            aria-selected={activeTab === id}
          >
            <Icon size={15} />
            {label}
          </button>
        ))}
      </nav>

      {activeTab === 'branding' && limits && (
        <div className="admin-settings-panel admin-mail-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>站点与呈现</span>
              <span className="admin-settings-card-badge">{branding.name}</span>
            </div>
            <div className="admin-card-body admin-mail-body admin-brand-sections">
              <div className="admin-brand-preview">
                {branding.logo ? (
                  <img src={branding.logo} alt="" className="admin-brand-preview-logo" />
                ) : (
                  <span className="admin-brand-preview-mark">{branding.logo_mark}</span>
                )}
                <div>
                  <strong>{branding.name}</strong>
                  {branding.slogan && <p className="admin-mail-field-hint" style={{ marginTop: 4 }}>{branding.slogan}</p>}
                </div>
              </div>

              <section className="admin-settings-section" id="settings-brand-identity">
                <div className="admin-settings-section-head">
                  <h3>品牌标识</h3>
                  <p>名称、标语、简介与字标；简介用于右侧栏顶部展示与搜索引擎 description</p>
                </div>
                <div className="admin-mail-grid">
                  <div className="admin-mail-field">
                    <label htmlFor="brand-name">论坛名称</label>
                    <Input
                      id="brand-name"
                      value={branding.name}
                      onChange={e => setBranding(b => ({ ...b, name: e.target.value }))}
                      placeholder="姜十三论坛"
                      maxLength={64}
                    />
                  </div>
                  <div className="admin-mail-field">
                    <label htmlFor="brand-mark">字标（无 Logo 时显示）</label>
                    <Input
                      id="brand-mark"
                      value={branding.logo_mark}
                      onChange={e => setBranding(b => ({ ...b, logo_mark: e.target.value.slice(0, 2) }))}
                      placeholder="姜"
                      maxLength={2}
                    />
                    <span className="admin-mail-field-hint">建议 1 个汉字或字母</span>
                  </div>
                  <div className="admin-mail-field admin-mail-field--span2">
                    <label htmlFor="brand-slogan">标语</label>
                    <Input
                      id="brand-slogan"
                      value={branding.slogan}
                      onChange={e => setBranding(b => ({ ...b, slogan: e.target.value }))}
                      placeholder="拾三一隅，自在交流"
                      maxLength={200}
                    />
                    <span className="admin-mail-field-hint">短句，出现在浏览器标题与页脚</span>
                  </div>
                  <div className="admin-mail-field admin-mail-field--span2">
                    <label htmlFor="brand-description">站点简介</label>
                    <Textarea
                      id="brand-description"
                      value={branding.description ?? ''}
                      onChange={e => setBranding(b => ({ ...b, description: e.target.value }))}
                      placeholder="一两段话介绍本站定位与内容，便于搜索引擎与访客理解"
                      maxLength={500}
                      rows={3}
                    />
                    <span className="admin-mail-field-hint">
                      用于右侧栏顶部论坛简介与 SEO description；未填写时回退到标语（建议 80–160 字）
                    </span>
                  </div>
                  <div className="admin-mail-field admin-mail-field--span2">
                    <label htmlFor="brand-keywords">SEO 关键词</label>
                    <Input
                      id="brand-keywords"
                      value={branding.keywords ?? ''}
                      onChange={e => setBranding(b => ({ ...b, keywords: e.target.value }))}
                      placeholder="论坛,社区,技术交流"
                      maxLength={200}
                    />
                    <span className="admin-mail-field-hint">
                      用于 meta keywords；用逗号分隔，最多 20 个（支持中文逗号）
                    </span>
                  </div>
                </div>
              </section>

              <section className="admin-settings-section" id="settings-brand-assets">
                <div className="admin-settings-section-head">
                  <h3>视觉资源</h3>
                  <p>站点 Logo、浏览器标签图标与默认社交分享图</p>
                </div>
                <div className="admin-mail-grid">
                  <div className="admin-mail-field">
                    <label htmlFor="brand-logo-file">站点 Logo</label>
                    <div className="admin-brand-upload-row">
                      <Input
                        id="brand-logo-file"
                        type="file"
                        accept="image/png,image/jpeg,image/gif,image/webp"
                        onChange={e => {
                          const f = e.target.files?.[0];
                          void handleUploadBrandAsset('logo', f);
                          e.target.value = '';
                        }}
                      />
                      {branding.logo && (
                        <Button
                          variant="outline"
                          size="sm"
                          loading={uploadingBrand === 'logo'}
                          onClick={() => void handleClearBrandAsset('logo')}
                        >
                          清除
                        </Button>
                      )}
                    </div>
                    <span className="admin-mail-field-hint">
                      {uploadingBrand === 'logo' ? '上传中…' : '保留原图并生成 WebP，最大 2MB'}
                    </span>
                  </div>
                  <div className="admin-mail-field">
                    <label htmlFor="brand-favicon-file">Favicon</label>
                    <div className="admin-brand-upload-row">
                      <Input
                        id="brand-favicon-file"
                        type="file"
                        accept="image/png,image/jpeg,image/gif,image/webp,image/x-icon,image/vnd.microsoft.icon"
                        onChange={e => {
                          const f = e.target.files?.[0];
                          void handleUploadBrandAsset('favicon', f);
                          e.target.value = '';
                        }}
                      />
                      {branding.favicon && (
                        <Button
                          variant="outline"
                          size="sm"
                          loading={uploadingBrand === 'favicon'}
                          onClick={() => void handleClearBrandAsset('favicon')}
                        >
                          清除
                        </Button>
                      )}
                    </div>
                    <span className="admin-mail-field-hint">
                      {branding.favicon ? `当前：${branding.favicon}` : '浏览器标签图标'}
                    </span>
                  </div>
                  <div className="admin-mail-field admin-mail-field--span2">
                    <label htmlFor="brand-og-image-file">默认社交分享图（OG Image）</label>
                    <div className="admin-brand-upload-row">
                      <Input
                        id="brand-og-image-file"
                        type="file"
                        accept="image/png,image/jpeg,image/gif,image/webp"
                        onChange={e => {
                          const f = e.target.files?.[0];
                          void handleUploadBrandAsset('og_image', f);
                          e.target.value = '';
                        }}
                      />
                      {branding.og_image && (
                        <Button
                          variant="outline"
                          size="sm"
                          loading={uploadingBrand === 'og_image'}
                          onClick={() => void handleClearBrandAsset('og_image')}
                        >
                          清除
                        </Button>
                      )}
                    </div>
                    <span className="admin-mail-field-hint">
                      {uploadingBrand === 'og_image'
                        ? '上传中…'
                        : branding.og_image
                          ? `当前：${branding.og_image}；建议 1200×630，用于微信/社交预览；未设置时回退 Logo`
                          : '建议 1200×630，用于微信/社交预览；未设置时回退 Logo'}
                    </span>
                  </div>
                </div>
              </section>

              <section className="admin-settings-section" id="settings-brand-footer">
                <div className="admin-settings-section-head">
                  <h3>页脚信息</h3>
                  <p>备案号显示在站点底部；友情链接请在「社区 → 友情链接」管理</p>
                </div>
                <div className="admin-mail-grid">
                  <div className="admin-mail-field">
                    <label htmlFor="brand-icp">ICP 备案号</label>
                    <Input
                      id="brand-icp"
                      value={branding.icp_beian ?? ''}
                      onChange={e => setBranding(b => ({ ...b, icp_beian: e.target.value }))}
                      placeholder="京ICP备xxxxxxxx号"
                      maxLength={64}
                    />
                  </div>
                  <div className="admin-mail-field">
                    <label htmlFor="brand-icp-url">ICP 跳转链接</label>
                    <Input
                      id="brand-icp-url"
                      value={branding.icp_beian_url ?? ''}
                      onChange={e => setBranding(b => ({ ...b, icp_beian_url: e.target.value }))}
                      placeholder="https://beian.miit.gov.cn/"
                      maxLength={512}
                    />
                    <span className="admin-mail-field-hint">留空则用工信部默认页</span>
                  </div>
                </div>
              </section>

              <section className="admin-settings-section" id="settings-aside">
                <div className="admin-settings-section-head">
                  <h3>右侧栏组件</h3>
                  <p>「站点简介」固定在最上方；以下模块可拖拽排序，保存后同步影响桌面右侧栏与手机「社区动态」抽屉</p>
                </div>
                <AsideWidgetList
                  widgets={asideWidgets}
                  onChange={handleAsideWidgetsChange}
                />
              </section>

              <section className="admin-settings-section" id="settings-nav">
                <div className="admin-settings-section-head">
                  <h3>浏览与链接</h3>
                  <p>控制打开帖子与正文链接时是否新开浏览器标签页</p>
                </div>
                <div className="admin-settings-table" role="group" aria-label="浏览与链接">
                  {NAV_TOGGLES.map(row => (
                    <div key={row.key} className="admin-settings-row">
                      <span className="admin-settings-row-label" id={`limit-label-${row.key}`}>
                        {row.label}
                      </span>
                      <div className="admin-settings-row-input">
                        <button
                          type="button"
                          id={`limit-${row.key}`}
                          role="switch"
                          aria-checked={!!limits[row.key]}
                          aria-labelledby={`limit-label-${row.key}`}
                          className={`admin-settings-switch${limits[row.key] ? ' is-on' : ''}`}
                          onClick={() => handleBoolLimitChange(row.key, !limits[row.key])}
                        >
                          <span className="admin-settings-switch-ui" aria-hidden />
                        </button>
                      </div>
                      <span className="admin-settings-row-hint">{row.hint}</span>
                    </div>
                  ))}
                </div>
              </section>

              <section className="admin-settings-section" id="settings-feed-list">
                <div className="admin-settings-section-head">
                  <h3>列表呈现</h3>
                  <p>首页及帖子列表的信息密度、缩略图与排序标签</p>
                </div>
                <div className="admin-settings-table" role="group" aria-label="列表呈现">
                  <div className="admin-settings-row">
                    <span className="admin-settings-row-label" id="limit-label-feed_list_style">
                      帖子列表样式
                    </span>
                    <div className="admin-settings-row-input admin-settings-row-input--stack">
                      <div className="admin-permalink-presets">
                        {([
                          { value: 'title' as const, label: '仅标题' },
                          { value: 'excerpt' as const, label: '标题+摘要' },
                          { value: 'thumbnail' as const, label: '标题+摘要+缩略图' },
                        ]).map(opt => (
                          <button
                            key={opt.value}
                            type="button"
                            className={`admin-permalink-chip${limits.feed_list_style === opt.value ? ' is-active' : ''}`}
                            onClick={() => setLimits(prev => prev ? { ...prev, feed_list_style: opt.value } : prev)}
                          >
                            {opt.label}
                          </button>
                        ))}
                      </div>
                    </div>
                    <span className="admin-settings-row-hint">
                      仅标题最紧凑；摘要模式增加一行预览；缩略图模式在有站内配图时右侧显示封面
                    </span>
                  </div>
                </div>
                <div className="admin-settings-subsection">
                  <h4 className="admin-settings-subsection-title">首页排序标签</h4>
                  <p className="admin-settings-subsection-desc">
                    拖拽调整顺序；在「显示名称」框中改首页文案；右侧开关控制启停。第一个启用项为默认排序。「最新」按发帖与评论的最近时间混排
                  </p>
                  <FeedSortTabList
                    tabs={normalizeFeedSortTabs(limits.feed_sort_tabs ?? DEFAULT_FEED_SORT_TABS)}
                    onChange={tabs => setLimits(prev => prev ? { ...prev, feed_sort_tabs: tabs } : prev)}
                  />
                </div>
              </section>

              <section className="admin-settings-section" id="settings-permalink">
                <div className="admin-settings-section-head">
                  <h3>伪静态 URL</h3>
                  <p>为帖子 / 用户生成带后缀的规范链接，非规范路径会 301 跳转</p>
                </div>
                <div className="admin-settings-table" role="group" aria-label="伪静态">
                  <div className="admin-settings-row">
                    <span className="admin-settings-row-label" id="limit-label-permalink_enabled">
                      启用伪静态
                    </span>
                    <div className="admin-settings-row-input">
                      <button
                        type="button"
                        id="limit-permalink_enabled"
                        role="switch"
                        aria-checked={!!limits.permalink_enabled}
                        aria-labelledby="limit-label-permalink_enabled"
                        className={`admin-settings-switch${limits.permalink_enabled ? ' is-on' : ''}`}
                        onClick={() => setLimits(prev => prev ? { ...prev, permalink_enabled: !prev.permalink_enabled } : prev)}
                      >
                        <span className="admin-settings-switch-ui" aria-hidden />
                      </button>
                    </div>
                    <span className="admin-settings-row-hint">关闭：/post/123 · 开启：/post/123.后缀</span>
                  </div>
                  <div className="admin-settings-row">
                    <span className="admin-settings-row-label" id="limit-label-permalink_ext">
                      URL 后缀
                    </span>
                    <div className="admin-settings-row-input admin-settings-row-input--stack">
                      <div className="admin-permalink-presets">
                        {(['html', 'htm', 'shtml'] as const).map(ext => (
                          <button
                            key={ext}
                            type="button"
                            className={`admin-permalink-chip${limits.permalink_ext === ext ? ' is-active' : ''}`}
                            disabled={!limits.permalink_enabled}
                            onClick={() => setLimits(prev => prev ? { ...prev, permalink_ext: ext } : prev)}
                          >
                            .{ext}
                          </button>
                        ))}
                      </div>
                      <Input
                        id="limit-permalink_ext"
                        value={limits.permalink_ext}
                        disabled={!limits.permalink_enabled}
                        placeholder="html"
                        aria-labelledby="limit-label-permalink_ext"
                        onChange={e => {
                          const v = e.target.value.replace(/^\./, '').toLowerCase();
                          setLimits(prev => prev ? { ...prev, permalink_ext: v } : prev);
                        }}
                      />
                    </div>
                    <span className="admin-settings-row-hint">
                      预览：
                      <code className="admin-permalink-preview">
                        /post/123{limits.permalink_enabled ? `.${(limits.permalink_ext || 'html').replace(/^\./, '')}` : ''}
                      </code>
                    </span>
                  </div>
                </div>
              </section>
            </div>
          </div>
          <div className="admin-settings-bar">
            <p>保存后立即影响顶栏、页脚、列表样式、链接打开方式、伪静态链接与浏览器标题</p>
            <Button onClick={handleSaveBranding} loading={savingBranding}>
              保存站点设置
            </Button>
          </div>
        </div>
      )}

      {activeTab === 'limits' && (
        <div className="admin-settings-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>论坛限制</span>
              <span className="admin-settings-card-badge">共 {SETTING_SECTIONS.length} 组</span>
            </div>
            <div className="admin-card-body">
              <SettingTable sections={SETTING_SECTIONS} limits={limits} onChange={handleLimitChange} />
            </div>
          </div>
          <div className="admin-settings-bar">
            <p>修改后请点击保存，新规则立即对全部用户生效</p>
            <Button onClick={handleSaveForumSettings} loading={savingForum}>
              保存论坛限制
            </Button>
          </div>
        </div>
      )}

      {activeTab === 'mail' && (
        <div className="admin-settings-panel admin-mail-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>SMTP 邮件配置</span>
              <span className={`admin-mail-status${mail.enabled ? ' is-on' : ''}`}>
                <span className="admin-mail-status-dot" aria-hidden />
                {mail.enabled ? '已启用' : '未启用'}
              </span>
            </div>
            <div className="admin-card-body admin-mail-body">
              <label className="admin-mail-switch" htmlFor="mail-enabled">
                <input
                  id="mail-enabled"
                  type="checkbox"
                  checked={mail.enabled}
                  onChange={e => setMail(m => ({ ...m, enabled: e.target.checked }))}
                />
                <span className="admin-mail-switch-ui" aria-hidden />
                <span className="admin-mail-switch-copy">
                  <strong>启用邮件服务</strong>
                  <small>开启并填写完整后，新用户注册需邮箱验证码；首个管理员可在未配置时直接注册</small>
                </span>
              </label>

              <div className="admin-mail-grid">
                <div className="admin-mail-field admin-mail-field--span2">
                  <label htmlFor="mail-host">SMTP 主机</label>
                  <Input
                    id="mail-host"
                    value={mail.host}
                    onChange={e => setMail(m => ({ ...m, host: e.target.value }))}
                    placeholder="smtp.example.com"
                    autoComplete="off"
                  />
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-port">端口</label>
                  <Input
                    id="mail-port"
                    type="number"
                    min={1}
                    value={mail.port}
                    onChange={e => setMail(m => ({ ...m, port: parseInt(e.target.value, 10) || 0 }))}
                  />
                  <span className="admin-mail-field-hint">常用 465 / 587</span>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-encryption">加密方式</label>
                  <select
                    id="mail-encryption"
                    className="admin-mail-select"
                    value={mail.encryption}
                    onChange={e => setMail(m => ({
                      ...m,
                      encryption: e.target.value as MailConfig['encryption'],
                    }))}
                  >
                    <option value="ssl">SSL / TLS（465）</option>
                    <option value="starttls">STARTTLS（587）</option>
                    <option value="none">无加密</option>
                  </select>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-username">用户名</label>
                  <Input
                    id="mail-username"
                    value={mail.username}
                    onChange={e => setMail(m => ({ ...m, username: e.target.value }))}
                    placeholder="SMTP 登录账号"
                    autoComplete="off"
                  />
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-password">密码</label>
                  <Input
                    id="mail-password"
                    type="password"
                    value={mail.password || ''}
                    onChange={e => setMail(m => ({ ...m, password: e.target.value }))}
                    placeholder={mail.has_password ? '已设置，留空不改' : '密码或授权码'}
                    autoComplete="new-password"
                  />
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-from">发件地址</label>
                  <Input
                    id="mail-from"
                    type="email"
                    value={mail.from}
                    onChange={e => setMail(m => ({ ...m, from: e.target.value }))}
                    placeholder="noreply@example.com"
                  />
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="mail-from-name">发件人名称</label>
                  <Input
                    id="mail-from-name"
                    value={mail.from_name}
                    onChange={e => setMail(m => ({ ...m, from_name: e.target.value }))}
                    placeholder="姜十三论坛"
                  />
                </div>
              </div>

              <div className="admin-mail-test">
                <div className="admin-mail-test-head">
                  <strong>发送测试</strong>
                  <span>请先保存配置，再向指定邮箱发一封测试信</span>
                </div>
                <div className="admin-mail-test-row">
                  <Input
                    id="mail-test-to"
                    type="email"
                    value={testTo}
                    onChange={e => setTestTo(e.target.value)}
                    placeholder="your@email.com"
                    aria-label="测试收件邮箱"
                  />
                  <Button variant="outline" onClick={handleTestMail} loading={testingMail}>
                    发送测试
                  </Button>
                </div>
              </div>
            </div>
          </div>

          <div className="admin-settings-bar">
            <p>保存后立即生效，注册页将按配置要求邮箱验证码</p>
            <Button onClick={handleSaveMailSettings} loading={savingMail}>
              保存邮件设置
            </Button>
          </div>
        </div>
      )}

      {activeTab === 'oidc' && (
        <div className="admin-settings-panel admin-mail-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>OIDC Provider（全局）</span>
              <span className={`admin-mail-status${oidc.ready ? ' is-on' : ''}`}>
                <span className="admin-mail-status-dot" aria-hidden />
                {oidcStatusLabel(oidc, oauthClients.length)}
              </span>
            </div>
            <div className="admin-card-body admin-mail-body">
              <label className="admin-mail-switch" htmlFor="oidc-enabled">
                <input
                  id="oidc-enabled"
                  type="checkbox"
                  checked={oidc.enabled}
                  onChange={e => setOidc(o => ({ ...o, enabled: e.target.checked }))}
                />
                <span className="admin-mail-switch-ui" aria-hidden />
                <span className="admin-mail-switch-copy">
                  <strong>启用 OIDC</strong>
                  <small>开启后，Gitea 等可将本论坛作为身份源；已登录用户跳转静默授权</small>
                </span>
              </label>

              <div className="admin-mail-grid">
                <div className="admin-mail-field admin-mail-field--span2">
                  <label htmlFor="oidc-root-url">对外公网地址（ROOT_URL）</label>
                  <Input
                    id="oidc-root-url"
                    value={oidc.root_url}
                    onChange={e => setOidc(o => ({ ...o, root_url: e.target.value }))}
                    placeholder="https://bbs.iioio.com"
                    autoComplete="off"
                  />
                  <span className="admin-mail-field-hint">无尾斜杠；作为 OIDC Issuer</span>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="oidc-group-claim">用户组 Claim 名</label>
                  <Input
                    id="oidc-group-claim"
                    value={oidc.group_claim}
                    onChange={e => setOidc(o => ({ ...o, group_claim: e.target.value }))}
                    placeholder="groups"
                  />
                  <span className="admin-mail-field-hint">Gitea「用户组 Claim 名称」填此项</span>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="oidc-user-group">普通用户组值</label>
                  <Input
                    id="oidc-user-group"
                    value={oidc.user_group}
                    onChange={e => setOidc(o => ({ ...o, user_group: e.target.value }))}
                    placeholder="gitea-users"
                  />
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="oidc-admin-group">管理员组值</label>
                  <Input
                    id="oidc-admin-group"
                    value={oidc.admin_group}
                    onChange={e => setOidc(o => ({ ...o, admin_group: e.target.value }))}
                    placeholder="gitea-admin"
                  />
                  <span className="admin-mail-field-hint">Gitea「管理员用户组」填此项</span>
                </div>
              </div>

              {oidc.discovery_url && (
                <div className="admin-mail-test">
                  <div className="admin-mail-test-head">
                    <strong>给 Gitea 填写</strong>
                    <span>Provider 选 OpenID Connect；可选填 groups 映射管理员</span>
                  </div>
                  <code className="admin-oidc-url">{oidc.discovery_url}</code>
                  {oidc.logout_url && <code className="admin-oidc-url">登出：{oidc.logout_url}</code>}
                </div>
              )}
            </div>
          </div>

          <div className="admin-settings-bar">
            <p>全局配置保存后立即生效；应用凭证在下方管理（密钥 bcrypt 存储）</p>
            <Button onClick={handleSaveOidcSettings} loading={savingOidc}>
              保存全局设置
            </Button>
          </div>

          <div className="admin-card admin-settings-card" style={{ marginTop: 16 }}>
            <div className="admin-card-head">
              <span>OAuth 应用</span>
              <span className="admin-settings-card-badge">{oauthClients.length} 个</span>
            </div>
            <div className="admin-card-body admin-mail-body">
              {revealedSecret && (
                <div className="admin-mail-test">
                  <div className="admin-mail-test-head">
                    <strong>客户端密钥（仅显示一次）</strong>
                    <span>请立即复制到 Gitea，离开后无法再查看明文</span>
                  </div>
                  <code className="admin-oidc-url">{revealedSecret}</code>
                  <Button variant="outline" className="mt-2" onClick={() => setRevealedSecret(null)}>已保存，关闭提示</Button>
                </div>
              )}

              {oauthClients.length > 0 && (
                <div className="admin-oauth-list">
                  {oauthClients.map(c => (
                    <div key={c.id} className="admin-oauth-row">
                      <div>
                        <strong>{c.name}</strong>
                        <div className="admin-mail-field-hint">
                          {c.client_id} · {c.enabled ? '启用' : '停用'} · {c.redirect_uris}
                        </div>
                      </div>
                      <div className="admin-oauth-actions">
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() => {
                            setEditingClientId(c.id);
                            setClientForm({
                              client_id: c.client_id,
                              name: c.name,
                              redirect_uris: c.redirect_uris,
                              enabled: c.enabled,
                            });
                          }}
                        >
                          编辑
                        </Button>
                        <Button variant="outline" size="sm" onClick={() => handleRotateSecret(c.id)} loading={savingClient}>
                          轮换密钥
                        </Button>
                        <Button variant="outline" size="sm" onClick={() => handleDeleteOAuthClient(c.id)}>
                          删除
                        </Button>
                      </div>
                    </div>
                  ))}
                </div>
              )}

              <div className="admin-mail-grid" style={{ marginTop: 12 }}>
                {!editingClientId && (
                  <div className="admin-mail-field">
                    <label htmlFor="oauth-client-id">客户端 ID</label>
                    <Input
                      id="oauth-client-id"
                      value={clientForm.client_id}
                      onChange={e => setClientForm(f => ({ ...f, client_id: e.target.value }))}
                      placeholder="gitea"
                    />
                  </div>
                )}
                <div className="admin-mail-field">
                  <label htmlFor="oauth-client-name">显示名称</label>
                  <Input
                    id="oauth-client-name"
                    value={clientForm.name}
                    onChange={e => setClientForm(f => ({ ...f, name: e.target.value }))}
                    placeholder="Gitea"
                  />
                </div>
                <div className="admin-mail-field admin-mail-field--span2">
                  <label htmlFor="oauth-redirect">回调地址</label>
                  <Textarea
                    id="oauth-redirect"
                    value={clientForm.redirect_uris}
                    onChange={e => setClientForm(f => ({ ...f, redirect_uris: e.target.value }))}
                    rows={2}
                    placeholder="https://git.iioio.com/user/oauth2/jiang13/callback"
                  />
                </div>
                <label className="admin-mail-switch" htmlFor="oauth-enabled">
                  <input
                    id="oauth-enabled"
                    type="checkbox"
                    checked={clientForm.enabled}
                    onChange={e => setClientForm(f => ({ ...f, enabled: e.target.checked }))}
                  />
                  <span className="admin-mail-switch-ui" aria-hidden />
                  <span className="admin-mail-switch-copy">
                    <strong>启用此应用</strong>
                  </span>
                </label>
              </div>

              <div className="admin-settings-bar" style={{ marginTop: 8 }}>
                <p>{editingClientId ? `正在编辑 #${editingClientId}` : '新建时自动生成密钥（仅显示一次）'}</p>
                <div className="admin-settings-bar-actions">
                  {editingClientId && (
                    <Button variant="outline" onClick={resetClientForm}>取消编辑</Button>
                  )}
                  <Button onClick={handleSaveOAuthClient} loading={savingClient}>
                    {editingClientId ? '保存应用' : '创建应用'}
                  </Button>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}

      {activeTab === 'gitea' && (
        <div className="admin-settings-panel admin-mail-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>Gitea 仓库同步</span>
              <span className={`admin-mail-status${gitea.ready ? ' is-on' : ''}`}>
                <span className="admin-mail-status-dot" aria-hidden />
                {giteaStatusLabel(gitea)}
              </span>
            </div>
            <div className="admin-card-body admin-mail-body">
              <label className="admin-mail-switch" htmlFor="gitea-enabled">
                <input
                  id="gitea-enabled"
                  type="checkbox"
                  checked={gitea.enabled}
                  onChange={e => setGitea(g => ({ ...g, enabled: e.target.checked }))}
                />
                <span className="admin-mail-switch-ui" aria-hidden />
                <span className="admin-mail-switch-copy">
                  <strong>启用同步</strong>
                  <small>按论坛用户名匹配 Gitea login，拉取公开仓库供「开源码桶」页展示</small>
                </span>
              </label>

              <div className="admin-mail-grid">
                <div className="admin-mail-field admin-mail-field--span2">
                  <label htmlFor="gitea-base-url">Gitea 地址（BASE_URL）</label>
                  <Input
                    id="gitea-base-url"
                    value={gitea.base_url}
                    onChange={e => setGitea(g => ({ ...g, base_url: e.target.value }))}
                    placeholder="https://git.iioio.com"
                    autoComplete="off"
                  />
                  <span className="admin-mail-field-hint">无尾斜杠</span>
                </div>
                <div className="admin-mail-field admin-mail-field--span2">
                  <label htmlFor="gitea-token">Access Token</label>
                  <Input
                    id="gitea-token"
                    type="password"
                    value={gitea.token ?? ''}
                    onChange={e => setGitea(g => ({ ...g, token: e.target.value }))}
                    placeholder={gitea.has_token ? '已配置，留空则保持不变' : '只读 Token'}
                    autoComplete="new-password"
                  />
                  <span className="admin-mail-field-hint">需能列出用户公开仓库</span>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="gitea-interval">同步间隔</label>
                  <Input
                    id="gitea-interval"
                    type="number"
                    min={5}
                    max={1440}
                    value={gitea.sync_interval_min}
                    onChange={e => setGitea(g => ({
                      ...g,
                      sync_interval_min: Number.parseInt(e.target.value, 10) || 60,
                    }))}
                  />
                  <span className="admin-mail-field-hint">分钟，最小 5</span>
                </div>
              </div>
            </div>
          </div>

          <div className="admin-settings-bar">
            <p>保存后按间隔自动同步；也可立即手动拉取</p>
            <div className="admin-settings-bar-actions">
              <Button variant="outline" onClick={handleSyncGitea} loading={syncingGitea} disabled={!gitea.ready}>
                立即同步
              </Button>
              <Button onClick={handleSaveGiteaSettings} loading={savingGitea}>
                保存同步设置
              </Button>
            </div>
          </div>
        </div>
      )}

      {activeTab === 'storage' && (
        <div className="admin-settings-panel admin-mail-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>上传对象存储</span>
              <span className={`admin-mail-status${storage.ready ? ' is-on' : ''}`}>
                <span className="admin-mail-status-dot" aria-hidden />
                {storageStatusLabel(storage)}
              </span>
            </div>
            <div className="admin-card-body admin-mail-body">
              <div className="admin-mail-grid">
                <div className="admin-mail-field">
                  <label htmlFor="storage-type">存储类型</label>
                  <select
                    id="storage-type"
                    className="admin-mail-select"
                    value={storage.type}
                    onChange={e => setStorage(s => ({
                      ...s,
                      type: e.target.value === 's3' ? 's3' : 'local',
                    }))}
                  >
                    <option value="local">本地磁盘（data/uploads）</option>
                    <option value="s3">S3 兼容（MinIO / OSS / 七牛等）</option>
                  </select>
                  <span className="admin-mail-field-hint">头像、帖子图、站点品牌图均走此后端</span>
                </div>
                <div className="admin-mail-field">
                  <label htmlFor="storage-image-delivery">图片展示方案</label>
                  <select
                    id="storage-image-delivery"
                    className="admin-mail-select"
                    value={storage.image_delivery || 'webp'}
                    onChange={e => setStorage(s => ({
                      ...s,
                      image_delivery: e.target.value === 'original' ? 'original' : 'webp',
                    }))}
                  >
                    <option value="webp">使用 WebP（省流量，仍保留原图）</option>
                    <option value="original">使用原图（体积更大）</option>
                  </select>
                  <span className="admin-mail-field-hint">
                    静态图上传后同时保存原图与 WebP；此项只影响新上传写入正文/头像的 URL。动图 GIF 始终保留原文件
                  </span>
                </div>
              </div>

              {storage.type === 's3' && (
                <>
                  <div className="admin-mail-grid">
                    <div className="admin-mail-field admin-mail-field--span2">
                      <label htmlFor="storage-endpoint">Endpoint</label>
                      <Input
                        id="storage-endpoint"
                        value={storage.endpoint}
                        onChange={e => setStorage(s => ({ ...s, endpoint: e.target.value }))}
                        placeholder="https://s3.example.com"
                        autoComplete="off"
                      />
                    </div>
                    <div className="admin-mail-field">
                      <label htmlFor="storage-region">Region</label>
                      <Input
                        id="storage-region"
                        value={storage.region}
                        onChange={e => setStorage(s => ({ ...s, region: e.target.value }))}
                        placeholder="us-east-1"
                        autoComplete="off"
                      />
                    </div>
                    <div className="admin-mail-field">
                      <label htmlFor="storage-bucket">Bucket</label>
                      <Input
                        id="storage-bucket"
                        value={storage.bucket}
                        onChange={e => setStorage(s => ({ ...s, bucket: e.target.value }))}
                        placeholder="jiang13"
                        autoComplete="off"
                      />
                    </div>
                    <div className="admin-mail-field">
                      <label htmlFor="storage-access-key">Access Key</label>
                      <Input
                        id="storage-access-key"
                        value={storage.access_key}
                        onChange={e => setStorage(s => ({ ...s, access_key: e.target.value }))}
                        autoComplete="off"
                      />
                    </div>
                    <div className="admin-mail-field">
                      <label htmlFor="storage-secret-key">Secret Key</label>
                      <Input
                        id="storage-secret-key"
                        type="password"
                        value={storage.secret_key ?? ''}
                        onChange={e => setStorage(s => ({ ...s, secret_key: e.target.value }))}
                        placeholder={storage.has_secret_key ? '已配置，留空则保持不变' : 'Secret Key'}
                        autoComplete="new-password"
                      />
                    </div>
                    <div className="admin-mail-field admin-mail-field--span2">
                      <label htmlFor="storage-public-base">公开访问地址</label>
                      <Input
                        id="storage-public-base"
                        value={storage.public_base_url}
                        onChange={e => setStorage(s => ({ ...s, public_base_url: e.target.value }))}
                        placeholder="https://cdn.example.com/forum"
                        autoComplete="off"
                      />
                      <span className="admin-mail-field-hint">无尾斜杠；上传后返回此前缀下的绝对 URL</span>
                    </div>
                    <div className="admin-mail-field">
                      <label htmlFor="storage-prefix">对象前缀（可选）</label>
                      <Input
                        id="storage-prefix"
                        value={storage.prefix}
                        onChange={e => setStorage(s => ({ ...s, prefix: e.target.value }))}
                        placeholder="forum/"
                        autoComplete="off"
                      />
                    </div>
                    <div className="admin-mail-field">
                      <label className="admin-mail-switch" htmlFor="storage-path-style" style={{ marginTop: 22 }}>
                        <input
                          id="storage-path-style"
                          type="checkbox"
                          checked={storage.force_path_style}
                          onChange={e => setStorage(s => ({ ...s, force_path_style: e.target.checked }))}
                        />
                        <span className="admin-mail-switch-ui" aria-hidden />
                        <span className="admin-mail-switch-copy">
                          <strong>Path-Style</strong>
                          <small>MinIO 等多为开启；AWS S3 官方多为关闭</small>
                        </span>
                      </label>
                    </div>
                  </div>
                  <p className="admin-mail-field-hint" style={{ marginTop: 8 }}>
                    请确保 Bucket 已配置公开读或 CDN 回源可读；本程序不代设 ACL。
                  </p>
                </>
              )}
            </div>
          </div>

          <div className="admin-settings-bar">
            <p>保存后立即生效，无需重启；切换存储后端或展示方案不会改写历史帖子里的图片 URL</p>
            <Button onClick={handleSaveStorageSettings} loading={savingStorage}>
              保存存储设置
            </Button>
          </div>
        </div>
      )}

      {activeTab === 'filter' && (
        <div className="admin-settings-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">
              <span>敏感词过滤</span>
              <span className="admin-settings-card-badge">{settings.filter_word_count} 个词条</span>
            </div>
            <div className="admin-card-body admin-settings-filter-body">
              <p className="admin-settings-filter-tip">
                每行一个词，<code>#</code> 开头为注释。保存后写入 <code>filter_words.txt</code> 并立即生效。
              </p>
              <Textarea
                rows={16}
                value={filterWords}
                onChange={e => setFilterWords(e.target.value)}
                className="admin-settings-filter-textarea"
                spellCheck={false}
                placeholder={'# 示例\n违禁词\n广告刷单'}
              />
            </div>
          </div>
          <div className="admin-settings-bar">
            <p>敏感词会在发帖、评论、昵称等文本中自动替换为 *</p>
            <Button onClick={handleSaveFilterWords} loading={savingFilter}>
              保存敏感词
            </Button>
          </div>
        </div>
      )}

      {activeTab === 'system' && (
        <div className="admin-settings-panel">
          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">运行信息</div>
            <div className="admin-card-body">
              <dl className="admin-settings-info">
                <div className="admin-settings-info-row">
                  <dt>数据目录</dt>
                  <dd><code>{settings.data_dir}</code></dd>
                </div>
                <div className="admin-settings-info-row">
                  <dt>数据库</dt>
                  <dd><code>{settings.db_path}</code></dd>
                </div>
                <div className="admin-settings-info-row">
                  <dt>敏感词文件</dt>
                  <dd><code>{settings.filter_path}</code></dd>
                </div>
                <div className="admin-settings-info-row">
                  <dt>监听端口</dt>
                  <dd>{settings.port}</dd>
                </div>
              </dl>
            </div>
          </div>

          <div className="admin-card admin-settings-card">
            <div className="admin-card-head">数据备份</div>
            <div className="admin-card-body admin-settings-backup-body">
              <p>导出当前主库 SQLite 副本，便于迁移或灾难恢复。不含 <code>monitor.db</code> 浏览量。</p>
              <p className="admin-settings-backup-name">
                文件名：<code>jiang13_backup_YYYYMMDD_HHMMSS.db</code>
              </p>
              <Button onClick={handleBackup} loading={backing}>
                <Database size={16} />
                立即备份并下载
              </Button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
