import {
  type ReactNode,
  useDeferredValue,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { getCustomWorldSceneRelativePositionLabel } from '../data/customWorldSceneGraph';
import {
  resolveCustomWorldCampSceneImage,
  resolveCustomWorldLandmarkImageMap,
} from '../data/customWorldVisuals';
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import {
  buildCustomWorldCreatorIntentFoundationText,
  normalizeCustomWorldCreatorIntent,
} from '../services/customWorldCreatorIntent';
import { AnimationState, Character, CustomWorldProfile } from '../types';
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { CharacterAnimator } from './CharacterAnimator';
import type { CustomWorldEditorTarget } from './CustomWorldEntityEditorModal';
import { CustomWorldNpcPortrait } from './CustomWorldNpcVisualEditor';

export type ResultTab = 'world' | 'playable' | 'story' | 'landmarks';

type PendingGeneratedEntity = {
  id: string;
  kind: 'playable' | 'story' | 'landmark';
  title: string;
  progress: number;
  phaseLabel: string;
};

type RecentGeneratedIds = Record<'playable' | 'story' | 'landmark', string[]>;

interface CustomWorldEntityCatalogProps {
  profile: CustomWorldProfile;
  previewCharacters: Character[];
  activeTab: ResultTab;
  onActiveTabChange: (tab: ResultTab) => void;
  onEditTarget: (target: CustomWorldEditorTarget) => void;
  onProfileChange: (profile: CustomWorldProfile) => void;
  onDeleteStoryNpcs?: (ids: string[]) => void;
  onDeleteLandmarks?: (ids: string[]) => void;
  createActionLabel?: string;
  onCreateAction?: () => void;
  createActionDisabled?: boolean;
  pendingGeneratedEntity?: PendingGeneratedEntity | null;
  recentGeneratedIds?: RecentGeneratedIds;
  readOnly?: boolean;
}

const RESULT_TABS: Array<{ id: ResultTab; label: string }> = [
  { id: 'world', label: '世界' },
  { id: 'playable', label: '可扮演角色' },
  { id: 'story', label: '场景角色' },
  { id: 'landmarks', label: '场景' },
];

function Section({
  title,
  subtitle,
  badge,
  actions,
  children,
}: {
  title: string;
  subtitle?: string;
  badge?: ReactNode;
  actions?: ReactNode;
  children: ReactNode;
}) {
  return (
    <div
      className="pixel-nine-slice pixel-panel"
      style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 14, paddingY: 12 })}
    >
      <div className="flex items-start justify-between gap-3">
        <div className="min-w-0">
          <div className="text-xs font-bold tracking-[0.16em] text-white">
            {title}
          </div>
          {subtitle ? (
            <div className="mt-1 text-xs leading-6 text-zinc-500">
              {subtitle}
            </div>
          ) : null}
        </div>
        <div className="flex items-center gap-2">
          {badge}
          {actions}
        </div>
      </div>
      <div className="mt-3">{children}</div>
    </div>
  );
}

function SmallButton({
  onClick,
  children,
  tone = 'default',
  disabled = false,
}: {
  onClick: () => void;
  children: ReactNode;
  tone?: 'default' | 'sky' | 'rose';
  disabled?: boolean;
}) {
  const toneClassName =
    tone === 'sky'
      ? 'border-sky-300/20 bg-sky-500/10 text-sky-100 hover:text-white'
      : tone === 'rose'
        ? 'border-rose-300/20 bg-rose-500/10 text-rose-100 hover:text-white'
        : 'border-white/10 bg-black/20 text-zinc-300 hover:text-white';

  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className={`rounded-full border px-3 py-1 text-[11px] transition-colors ${toneClassName} ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
    >
      {children}
    </button>
  );
}

function SearchBox({
  value,
  onChange,
  placeholder,
}: {
  value: string;
  onChange: (value: string) => void;
  placeholder: string;
}) {
  return (
    <div className="rounded-2xl border border-white/10 bg-black/20 px-3 py-2">
      <input
        value={value}
        onChange={(event) => onChange(event.target.value)}
        placeholder={placeholder}
        className="w-full bg-transparent text-sm text-zinc-100 outline-none placeholder:text-zinc-500"
      />
    </div>
  );
}

function ImageFrame({
  src,
  alt,
  fallbackLabel,
  tone = 'square',
}: {
  src?: string;
  alt: string;
  fallbackLabel: string;
  tone?: 'square' | 'landscape';
}) {
  return (
    <div
      className={`overflow-hidden rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.16),transparent_48%),linear-gradient(180deg,rgba(19,24,39,0.96),rgba(8,10,17,0.92))] ${tone === 'landscape' ? 'aspect-[16/9]' : 'aspect-square'}`}
    >
      {src ? (
        <img src={src} alt={alt} className="h-full w-full object-cover" />
      ) : (
        <div className="flex h-full w-full items-center justify-center px-4 text-center text-sm font-semibold tracking-[0.18em] text-zinc-400">
          {fallbackLabel}
        </div>
      )}
    </div>
  );
}

function EmptyState({ title }: { title: string }) {
  return (
    <div className="rounded-2xl border border-dashed border-white/12 bg-black/20 px-5 py-6 text-center">
      <div className="text-sm text-zinc-300">{title}</div>
    </div>
  );
}

function NewBadge() {
  return (
    <span className="rounded-full border border-amber-300/24 bg-amber-500/12 px-2.5 py-1 text-[10px] font-semibold text-amber-100">
      新
    </span>
  );
}

function PendingEntityCard({
  title,
  phaseLabel,
  progress,
}: {
  title: string;
  phaseLabel: string;
  progress: number;
}) {
  return (
    <div className="rounded-[1.35rem] border border-sky-300/18 bg-sky-500/10 px-4 py-4">
      <div className="flex items-start justify-between gap-3">
        <div>
          <div className="text-sm font-semibold text-white">{title}</div>
          <div className="mt-1 text-xs leading-6 text-sky-50/90">
            {phaseLabel}
          </div>
        </div>
        <div className="rounded-full border border-sky-300/20 bg-black/20 px-2.5 py-1 text-[10px] text-sky-100">
          {Math.round(progress)}%
        </div>
      </div>
      <div className="mt-3 h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/30">
        <div
          className="h-full bg-[linear-gradient(90deg,#38bdf8_0%,#67e8f9_100%)] transition-[width] duration-300"
          style={{ width: `${Math.max(6, Math.min(100, progress))}%` }}
        />
      </div>
    </div>
  );
}

function CatalogCard({
  title,
  description,
  media,
  badge,
  isSelectionMode,
  isSelected,
  onClick,
  layout = 'stacked',
  mediaClassName,
  disabled = false,
}: {
  title: string;
  description: string;
  media: ReactNode;
  badge?: ReactNode;
  isSelectionMode: boolean;
  isSelected: boolean;
  onClick: () => void;
  layout?: 'stacked' | 'compact';
  mediaClassName?: string;
  disabled?: boolean;
}) {
  const selectionBadge = isSelectionMode ? (
    <div
      className={`shrink-0 rounded-full border px-2.5 py-1 text-[10px] ${
        isSelected
          ? 'border-rose-300/25 bg-rose-500/14 text-rose-50'
          : 'border-white/10 bg-black/20 text-zinc-400'
      }`}
    >
      {isSelected ? '已选' : '选择'}
    </div>
  ) : null;

  if (layout === 'compact') {
    return (
      <button
        type="button"
        onClick={onClick}
        disabled={disabled}
        className={`w-full rounded-[1.3rem] border p-2.5 text-left transition-colors ${
          isSelected
            ? 'border-rose-300/35 bg-rose-500/10'
            : disabled
              ? 'border-white/10 bg-black/20'
              : 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-black/28'
        }`}
      >
        <div className="flex items-start gap-3">
          <div
            className={`shrink-0 overflow-hidden rounded-[1rem] border border-white/8 bg-black/25 ${mediaClassName ?? 'h-[4.75rem] w-[4.75rem]'}`}
          >
            {media}
          </div>
          <div className="min-w-0 flex-1">
            <div className="flex items-start justify-between gap-3">
              <div className="min-w-0 text-[15px] font-semibold leading-5 text-white">
                {title}
              </div>
              <div className="flex items-center gap-2">
                {badge}
                {selectionBadge}
              </div>
            </div>
            <div className="mt-1.5 text-sm leading-5 text-zinc-300">
              {description || '暂无描述'}
            </div>
          </div>
        </div>
      </button>
    );
  }

  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className={`w-full rounded-[1.4rem] border p-3 text-left transition-colors ${
        isSelected
          ? 'border-rose-300/35 bg-rose-500/10'
          : disabled
            ? 'border-white/10 bg-black/20'
            : 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-black/28'
      }`}
    >
      <div className="space-y-3">
        <div
          className={`overflow-hidden rounded-[1.1rem] border border-white/8 bg-black/25 ${mediaClassName ?? ''}`}
        >
          {media}
        </div>
        <div className="flex items-start justify-between gap-3">
          <div className="min-w-0 text-base font-semibold text-white">
            {title}
          </div>
          <div className="flex items-center gap-2">
            {badge}
            {selectionBadge}
          </div>
        </div>
        <div className="text-sm leading-6 text-zinc-300">
          {description || '暂无描述'}
        </div>
      </div>
    </button>
  );
}

function matchText(text: string, query: string) {
  return text.toLowerCase().includes(query.toLowerCase());
}

function getSearchPlaceholder(tab: ResultTab) {
  if (tab === 'playable') return '搜索角色名称、称号、标签';
  if (tab === 'story') return '搜索场景角色名称、身份、动机';
  if (tab === 'landmarks') return '搜索场景名称、描述、NPC、连接';
  return '搜索';
}

function compactTextList(values: Array<string | null | undefined>) {
  return values.map((value) => value?.trim() ?? '').filter(Boolean);
}

function buildOpeningSceneSearchText(
  profile: CustomWorldProfile,
  campScene: ReturnType<typeof resolveCustomWorldCampScene>,
) {
  return [
    campScene.name,
    campScene.description,
    campScene.dangerLevel,
    profile.playerGoal,
    profile.summary,
    '开局场景',
    '开局归处',
  ].join(' ');
}

function buildStructuredFoundationEntries(profile: CustomWorldProfile) {
  const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
  const relationshipSeed = creatorIntent?.keyCharacters[0];
  const relationshipText = relationshipSeed
    ? compactTextList([
        relationshipSeed.name,
        relationshipSeed.role,
        relationshipSeed.relationToPlayer
          ? `与玩家：${relationshipSeed.relationToPlayer}`
          : '',
        relationshipSeed.hiddenHook
          ? `暗线：${relationshipSeed.hiddenHook}`
          : '',
      ]).join(' · ')
    : '';
  const themeToneText = compactTextList([
    creatorIntent?.themeKeywords.join('、') || '',
    creatorIntent?.toneDirectives.join('、') || '',
  ]).join(' / ');
  const playerOpeningText = compactTextList([
    creatorIntent?.playerPremise || '',
    creatorIntent?.openingSituation || '',
  ]).join('；');

  return [
    {
      id: 'world-hook',
      label: '世界一句话',
      value:
        creatorIntent?.worldHook ||
        profile.anchorPack?.worldSummary ||
        profile.summary,
    },
    {
      id: 'player-opening',
      label: '玩家开局',
      value: playerOpeningText || profile.playerGoal,
    },
    {
      id: 'theme-tone',
      label: '主题气质',
      value: themeToneText || profile.tone,
    },
    {
      id: 'core-conflict',
      label: '核心冲突',
      value:
        creatorIntent?.coreConflicts.join('；') ||
        profile.coreConflicts.join('；') ||
        profile.summary,
    },
    {
      id: 'relationship-seed',
      label: '关键关系',
      value:
        relationshipText ||
        profile.playableNpcs[0]?.relationshipHooks.join('；') ||
        profile.storyNpcs[0]?.relationshipHooks.join('；') ||
        '待补充',
    },
    {
      id: 'iconic-elements',
      label: '标志元素',
      value:
        creatorIntent?.iconicElements.join('、') ||
        profile.anchorPack?.motifDirectives.join('、') ||
        '待补充',
    },
  ];
}

type CatalogRole =
  | CustomWorldProfile['playableNpcs'][number]
  | CustomWorldProfile['storyNpcs'][number];

type BulkDeleteTab = 'story' | 'landmarks';

function buildRoleSearchText(role: CatalogRole) {
  return [
    role.name,
    role.title,
    role.role,
    role.description,
    role.backstory,
    role.backstoryReveal.publicSummary,
    role.personality,
    role.motivation,
    role.combatStyle,
    ...role.backstoryReveal.chapters.flatMap((chapter) => [
      chapter.title,
      chapter.teaser,
      chapter.content,
      chapter.contextSnippet,
    ]),
    ...role.skills.flatMap((skill) => [skill.name, skill.summary, skill.style]),
    ...role.initialItems.flatMap((item) => [
      item.name,
      item.category,
      item.description,
      ...item.tags,
    ]),
    ...role.relationshipHooks,
    ...role.tags,
  ].join(' ');
}

function buildLandmarkSearchText(
  landmark: CustomWorldProfile['landmarks'][number],
  storyNpcById: Map<string, CustomWorldProfile['storyNpcs'][number]>,
  landmarkById: Map<string, CustomWorldProfile['landmarks'][number]>,
) {
  return [
    landmark.name,
    landmark.description,
    landmark.dangerLevel,
    ...landmark.sceneNpcIds.map((npcId) => storyNpcById.get(npcId)?.name ?? ''),
    ...landmark.connections.flatMap((connection) => [
      landmarkById.get(connection.targetLandmarkId)?.name ?? '',
      getCustomWorldSceneRelativePositionLabel(connection.relativePosition),
      connection.summary,
    ]),
  ].join(' ');
}

export function CustomWorldEntityCatalog({
  profile,
  previewCharacters,
  activeTab,
  onActiveTabChange,
  onEditTarget,
  onProfileChange,
  onDeleteStoryNpcs,
  onDeleteLandmarks,
  createActionLabel,
  onCreateAction,
  createActionDisabled = false,
  pendingGeneratedEntity = null,
  recentGeneratedIds = {
    playable: [],
    story: [],
    landmark: [],
  },
  readOnly = false,
}: CustomWorldEntityCatalogProps) {
  const scrollContainerRef = useRef<HTMLDivElement | null>(null);
  const [searchDraft, setSearchDraft] = useState('');
  const [bulkDeleteMode, setBulkDeleteMode] = useState<BulkDeleteTab | null>(
    null,
  );
  const [selectedBulkIds, setSelectedBulkIds] = useState<string[]>([]);
  const deferredSearch = useDeferredValue(searchDraft.trim());

  const storyNpcById = useMemo(
    () => new Map(profile.storyNpcs.map((npc) => [npc.id, npc])),
    [profile.storyNpcs],
  );
  const landmarkById = useMemo(
    () => new Map(profile.landmarks.map((landmark) => [landmark.id, landmark])),
    [profile.landmarks],
  );
  const landmarkImageById = useMemo(
    () => resolveCustomWorldLandmarkImageMap(profile),
    [profile],
  );
  const resolvedCampScene = useMemo(
    () => resolveCustomWorldCampScene(profile),
    [profile],
  );
  const resolvedCampImageSrc = useMemo(
    () => resolveCustomWorldCampSceneImage(profile),
    [profile],
  );
  const previewCharacterById = useMemo(
    () =>
      new Map(
        profile.playableNpcs.map((role, index) => [
          role.id,
          previewCharacters[index] ?? null,
        ]),
      ),
    [previewCharacters, profile.playableNpcs],
  );
  const recentPlayableIdSet = useMemo(
    () => new Set(recentGeneratedIds.playable),
    [recentGeneratedIds.playable],
  );
  const recentStoryIdSet = useMemo(
    () => new Set(recentGeneratedIds.story),
    [recentGeneratedIds.story],
  );
  const recentLandmarkIdSet = useMemo(
    () => new Set(recentGeneratedIds.landmark),
    [recentGeneratedIds.landmark],
  );

  const filteredPlayable = useMemo(
    () =>
      profile.playableNpcs.filter(
        (role) =>
          !deferredSearch ||
          matchText(buildRoleSearchText(role), deferredSearch),
      ),
    [deferredSearch, profile.playableNpcs],
  );

  const filteredStory = useMemo(
    () =>
      profile.storyNpcs.filter(
        (npc) =>
          !deferredSearch ||
          matchText(buildRoleSearchText(npc), deferredSearch),
      ),
    [deferredSearch, profile.storyNpcs],
  );

  const filteredLandmarks = useMemo(
    () =>
      profile.landmarks.filter(
        (landmark) =>
          !deferredSearch ||
          matchText(
            buildLandmarkSearchText(landmark, storyNpcById, landmarkById),
            deferredSearch,
          ),
      ),
    [deferredSearch, landmarkById, profile.landmarks, storyNpcById],
  );
  const structuredFoundationEntries = useMemo(
    () => buildStructuredFoundationEntries(profile),
    [profile],
  );
  const structuredFoundationSourceText = useMemo(
    () =>
      buildCustomWorldCreatorIntentFoundationText(profile.creatorIntent).trim() ||
      profile.settingText.trim(),
    [profile.creatorIntent, profile.settingText],
  );
  const normalizedCreatorIntent = useMemo(
    () => normalizeCustomWorldCreatorIntent(profile.creatorIntent),
    [profile.creatorIntent],
  );
  const filteredSceneEntries = useMemo(() => {
    const openingSceneEntry = {
      id: 'custom-world-opening-scene',
      kind: 'camp' as const,
      name: resolvedCampScene.name,
      description: resolvedCampScene.description,
      imageSrc: resolvedCampImageSrc,
      searchText: buildOpeningSceneSearchText(profile, resolvedCampScene),
    };
    const landmarkEntries = filteredLandmarks.map((landmark) => ({
      id: landmark.id,
      kind: 'landmark' as const,
      name: landmark.name,
      description: landmark.description,
      imageSrc: landmarkImageById.get(landmark.id) ?? landmark.imageSrc,
      searchText: buildLandmarkSearchText(landmark, storyNpcById, landmarkById),
    }));
    const recentEntries = landmarkEntries.filter((entry) =>
      recentLandmarkIdSet.has(entry.id),
    );
    const restEntries = landmarkEntries.filter(
      (entry) => !recentLandmarkIdSet.has(entry.id),
    );
    const allEntries = [...recentEntries, openingSceneEntry, ...restEntries];

    if (!deferredSearch) {
      return allEntries;
    }

    return allEntries.filter((entry) =>
      matchText(entry.searchText, deferredSearch),
    );
  }, [
    deferredSearch,
    filteredLandmarks,
    landmarkById,
    landmarkImageById,
    profile,
    recentLandmarkIdSet,
    resolvedCampImageSrc,
    resolvedCampScene,
    storyNpcById,
  ]);

  const lockedCharacterNames = useMemo(
    () =>
      new Set(
        normalizedCreatorIntent?.keyCharacters
          .filter((entry) => entry.locked)
          .map((entry) => entry.name.trim())
          .filter(Boolean) ?? [],
      ),
    [normalizedCreatorIntent],
  );

  const counts = {
    world: 1,
    playable:
      profile.playableNpcs.length +
      (pendingGeneratedEntity?.kind === 'playable' ? 1 : 0),
    story:
      profile.storyNpcs.length +
      (pendingGeneratedEntity?.kind === 'story' ? 1 : 0),
    landmarks:
      profile.landmarks.length +
      1 +
      (pendingGeneratedEntity?.kind === 'landmark' ? 1 : 0),
  } satisfies Record<ResultTab, number>;

  const bulkDeleteTab: BulkDeleteTab | null =
    activeTab === 'story' || activeTab === 'landmarks' ? activeTab : null;
  const isBulkDeleteMode =
    bulkDeleteTab !== null && bulkDeleteMode === bulkDeleteTab;

  useEffect(() => {
    if (bulkDeleteMode && bulkDeleteMode !== activeTab) {
      setBulkDeleteMode(null);
      setSelectedBulkIds([]);
    }
  }, [activeTab, bulkDeleteMode]);

  useEffect(() => {
    const container = scrollContainerRef.current;
    if (!container) return;
    if (typeof container.scrollTo === 'function') {
      container.scrollTo({ top: 0, behavior: 'auto' });
      return;
    }
    container.scrollTop = 0;
  }, [activeTab]);

  const removePlayable = (id: string, name: string) => {
    if (profile.playableNpcs.length <= 1) {
      window.alert('至少保留一个可扮演角色，才能正常进入自定义世界。');
      return;
    }
    if (!window.confirm(`确认删除可扮演角色「${name}」吗？`)) return;
    onProfileChange({
      ...profile,
      playableNpcs: profile.playableNpcs.filter((role) => role.id !== id),
    });
  };

  const startBulkDelete = (tab: BulkDeleteTab) => {
    setBulkDeleteMode(tab);
    setSelectedBulkIds([]);
  };

  const cancelBulkDelete = () => {
    setBulkDeleteMode(null);
    setSelectedBulkIds([]);
  };

  const toggleBulkSelected = (id: string) => {
    setSelectedBulkIds((current) =>
      current.includes(id)
        ? current.filter((entry) => entry !== id)
        : [...current, id],
    );
  };

  const confirmBulkDelete = () => {
    if (!bulkDeleteTab || selectedBulkIds.length === 0) {
      return;
    }

    const label = bulkDeleteTab === 'story' ? '场景角色' : '场景';
    const confirmed = window.confirm(
      `确认批量删除 ${selectedBulkIds.length} 个${label}吗？`,
    );
    if (!confirmed) {
      return;
    }

    if (bulkDeleteTab === 'story') {
      onDeleteStoryNpcs?.(selectedBulkIds);
    } else {
      onDeleteLandmarks?.(selectedBulkIds);
    }
    cancelBulkDelete();
  };

  return (
    <div
      ref={scrollContainerRef}
      className="h-full min-h-0 space-y-3 overflow-y-auto overscroll-contain pr-1 scrollbar-hide"
    >
      <div className="px-1 pb-1 text-center">
        <div className="text-[11px] font-bold tracking-[0.28em] text-zinc-500">
          世界档案
        </div>
        <div className="mt-2 text-3xl font-black text-white sm:text-[2.2rem]">
          {profile.name}
        </div>
        <div className="mt-2 text-sm tracking-[0.18em] text-zinc-400">
          {profile.subtitle}
        </div>
      </div>

      <div className="sticky top-0 z-10 -mx-1 space-y-3 bg-[linear-gradient(180deg,rgba(8,10,17,0.98)_0%,rgba(8,10,17,0.95)_75%,rgba(8,10,17,0)_100%)] px-1 pb-3 pt-1">
        <div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
          {RESULT_TABS.map((tab) => (
            <div key={tab.id}>
              <button
                type="button"
                onClick={() => onActiveTabChange(tab.id)}
                className={`rounded-full border px-3 py-2 text-left text-sm transition-colors ${activeTab === tab.id ? 'border-sky-300/25 bg-sky-500/12 text-white' : 'border-white/10 bg-black/20 text-zinc-300 hover:text-white'}`}
              >
                <div className="font-semibold">{tab.label}</div>
                <div className="mt-1 text-[10px] tracking-[0.16em] text-white/55">
                  {counts[tab.id]}
                </div>
              </button>
            </div>
          ))}
        </div>

        {activeTab !== 'world' ? (
          <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
            <div className="min-w-0 flex-1">
              <SearchBox
                value={searchDraft}
                onChange={setSearchDraft}
                placeholder={getSearchPlaceholder(activeTab)}
              />
            </div>
            <div className="flex flex-wrap items-center justify-end gap-2">
              {isBulkDeleteMode ? (
                <>
                  <div className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] text-zinc-300">
                    已选 {selectedBulkIds.length}
                  </div>
                  <SmallButton onClick={cancelBulkDelete}>取消</SmallButton>
                  <SmallButton onClick={confirmBulkDelete} tone="rose">
                    删除选中
                  </SmallButton>
                </>
              ) : (
                <>
                  {!readOnly && createActionLabel && onCreateAction ? (
                    <SmallButton
                      onClick={onCreateAction}
                      tone="sky"
                      disabled={createActionDisabled}
                    >
                      {createActionLabel}
                    </SmallButton>
                  ) : null}
                  {!readOnly &&
                  bulkDeleteTab &&
                  ((bulkDeleteTab === 'story' && onDeleteStoryNpcs) ||
                    (bulkDeleteTab === 'landmarks' && onDeleteLandmarks)) ? (
                    <SmallButton
                      onClick={() => startBulkDelete(bulkDeleteTab)}
                      tone="rose"
                    >
                      批量删除
                    </SmallButton>
                  ) : null}
                </>
              )}
            </div>
          </div>
        ) : null}
      </div>

      {activeTab === 'world' ? (
        <>
          <Section title="档案规模">
            <div className="grid grid-cols-3 gap-2 text-center text-[11px] text-zinc-300">
              <div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
                <div className="text-xl font-black text-white">
                  {profile.playableNpcs.length}
                </div>
                <div>可扮演角色</div>
              </div>
              <div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
                <div className="text-xl font-black text-white">
                  {profile.storyNpcs.length}
                </div>
                <div>场景角色</div>
              </div>
              <div className="rounded-xl border border-white/8 bg-black/20 px-2 py-3">
                <div className="text-xl font-black text-white">
                  {profile.landmarks.length + 1}
                </div>
                <div>场景</div>
              </div>
            </div>
          </Section>

          <Section
            title="世界概述"
            actions={
              readOnly ? (
                <SmallButton
                  onClick={() => onEditTarget({ kind: 'world' })}
                  tone="sky"
                >
                  查看详情
                </SmallButton>
              ) : (
                <SmallButton
                  onClick={() => onEditTarget({ kind: 'world' })}
                  tone="sky"
                >
                  编辑
                </SmallButton>
              )
            }
          >
            <div className="space-y-3 text-sm leading-7 text-zinc-300">
              <p>{profile.summary}</p>
              <div className="rounded-2xl border border-amber-300/12 bg-amber-500/8 px-3 py-3 text-amber-100">
                主线目标：{profile.playerGoal}
              </div>
              <div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3">
                世界基调：{profile.tone}
              </div>
            </div>
          </Section>

          <Section
            title="基本设定"
            actions={
              readOnly ? (
                <SmallButton
                  onClick={() => onEditTarget({ kind: 'world' })}
                  tone="sky"
                >
                  查看详情
                </SmallButton>
              ) : (
                <SmallButton
                  onClick={() => onEditTarget({ kind: 'world' })}
                  tone="sky"
                >
                  编辑
                </SmallButton>
              )
            }
          >
            <div className="space-y-3">
              <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
                解析字段
              </div>
              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                {structuredFoundationEntries.map((entry) => (
                  <div
                    key={entry.id}
                    className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4"
                  >
                    <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
                      {entry.label}
                    </div>
                    <div className="mt-2 text-sm leading-7 text-zinc-100">
                      {entry.value || '待补充'}
                    </div>
                  </div>
                ))}
              </div>
              {structuredFoundationSourceText ? (
                <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
                  <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
                    锚点原文
                  </div>
                  <div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-200">
                    {structuredFoundationSourceText}
                  </div>
                </div>
              ) : null}
            </div>
          </Section>
        </>
      ) : null}

      {activeTab === 'playable' ? (
        <div className="space-y-3">
          {pendingGeneratedEntity?.kind === 'playable' ? (
            <PendingEntityCard
              title={pendingGeneratedEntity.title}
              phaseLabel={pendingGeneratedEntity.phaseLabel}
              progress={pendingGeneratedEntity.progress}
            />
          ) : null}
          <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm text-zinc-300">
            {readOnly
              ? '当前是草稿结果预览，可先浏览角色结构，再回到工作区继续精修。'
              : '可扮演角色支持新增、删除与更换外观模板。'}
          </div>
          {filteredPlayable.length === 0 ? (
            <EmptyState title="当前没有符合搜索条件的可扮演角色。" />
          ) : (
            filteredPlayable.map((role) => {
              const previewCharacter =
                previewCharacterById.get(role.id) ?? null;

              return (
                <div key={role.id}>
                  <Section
                    title={role.name}
                    subtitle={role.title}
                    badge={recentPlayableIdSet.has(role.id) ? <NewBadge /> : null}
                    actions={
                      readOnly ? (
                        <SmallButton
                          onClick={() =>
                            onEditTarget({
                              kind: 'playable',
                              mode: 'edit',
                              id: role.id,
                            })
                          }
                          tone="sky"
                        >
                          查看详情
                        </SmallButton>
                      ) : (
                        <div className="flex items-center gap-2">
                          <SmallButton
                            onClick={() =>
                              onEditTarget({
                                kind: 'playable',
                                mode: 'edit',
                                id: role.id,
                              })
                            }
                            tone="sky"
                          >
                            编辑
                          </SmallButton>
                          <SmallButton
                            onClick={() => removePlayable(role.id, role.name)}
                            tone="rose"
                          >
                            删除
                          </SmallButton>
                        </div>
                      )
                    }
                  >
                    <div className="flex flex-col gap-3 sm:flex-row">
                      <div className="flex h-28 w-28 shrink-0 items-end justify-center overflow-hidden rounded-2xl border border-white/10 bg-black/35">
                        {previewCharacter ? (
                          <CharacterAnimator
                            state={AnimationState.RUN}
                            character={previewCharacter}
                            className="h-full w-full"
                            imageClassName="object-bottom"
                          />
                        ) : null}
                      </div>
                      <div className="min-w-0 flex-1">
                        {lockedCharacterNames.has(role.name.trim()) ? (
                          <div className="mb-2 inline-flex rounded-full border border-amber-300/18 bg-amber-500/10 px-2.5 py-1 text-[10px] text-amber-100">
                            创作者锁定角色
                          </div>
                        ) : null}
                        <div className="text-sm leading-6 text-zinc-300">
                          {role.description}
                        </div>
                        <div className="mt-2 text-xs leading-6 text-zinc-400">
                          {role.backstory}
                        </div>
                        <div className="mt-3 rounded-xl border border-sky-300/12 bg-sky-500/8 px-3 py-2 text-xs leading-6 text-sky-50/95">
                          公开背景：
                          {role.backstoryReveal.publicSummary || '未填写'}
                        </div>
                        <div className="mt-3 grid gap-2 sm:grid-cols-2">
                          <div className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
                            身份：{role.role}
                          </div>
                          <div className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
                            初始好感：{role.initialAffinity}
                          </div>
                          <div className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
                            性格：{role.personality}
                          </div>
                          <div className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
                            战斗：{role.combatStyle}
                          </div>
                        </div>
                        <div className="mt-3 rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300">
                          动机：{role.motivation}
                        </div>
                        <div className="mt-3 rounded-xl border border-white/8 bg-black/20 px-3 py-3">
                          <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">
                            好感背景章节
                          </div>
                          <div className="mt-2 space-y-2">
                            {role.backstoryReveal.chapters.map((chapter) => (
                              <div
                                key={`${role.id}-${chapter.id}`}
                                className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300"
                              >
                                {chapter.affinityRequired} 好感 ·{' '}
                                {chapter.title}：{chapter.teaser}
                              </div>
                            ))}
                          </div>
                        </div>
                        <div className="mt-3 rounded-xl border border-white/8 bg-black/20 px-3 py-3">
                          <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">
                            技能
                          </div>
                          <div className="mt-2 space-y-2">
                            {role.skills.map((skill) => (
                              <div
                                key={`${role.id}-${skill.id}`}
                                className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300"
                              >
                                {skill.name} · {skill.style}：{skill.summary}
                              </div>
                            ))}
                          </div>
                        </div>
                        <div className="mt-3 rounded-xl border border-white/8 bg-black/20 px-3 py-3">
                          <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-400">
                            初始物品
                          </div>
                          <div className="mt-2 space-y-2">
                            {role.initialItems.map((item) => (
                              <div
                                key={`${role.id}-${item.id}`}
                                className="rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-xs leading-6 text-zinc-300"
                              >
                                {item.name} x{item.quantity} · {item.category} ·{' '}
                                {item.rarity}：{item.description}
                              </div>
                            ))}
                          </div>
                        </div>
                        <div className="mt-3 flex flex-wrap gap-2">
                          {role.tags.map((tag) => (
                            <span
                              key={`${role.id}-${tag}`}
                              className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[10px] text-zinc-300"
                            >
                              {tag}
                            </span>
                          ))}
                        </div>
                      </div>
                    </div>
                  </Section>
                </div>
              );
            })
          )}
        </div>
      ) : null}

      {activeTab === 'story' ? (
        <div className="space-y-3">
          {pendingGeneratedEntity?.kind === 'story' ? (
            <PendingEntityCard
              title={pendingGeneratedEntity.title}
              phaseLabel={pendingGeneratedEntity.phaseLabel}
              progress={pendingGeneratedEntity.progress}
            />
          ) : null}
          {filteredStory.length === 0 ? (
            <EmptyState title="当前没有符合搜索条件的场景角色。" />
          ) : (
            filteredStory.map((npc) => (
              <div key={npc.id}>
                <CatalogCard
                  title={npc.name}
                  description={npc.description}
                  badge={recentStoryIdSet.has(npc.id) ? <NewBadge /> : null}
                  isSelectionMode={isBulkDeleteMode}
                  isSelected={selectedBulkIds.includes(npc.id)}
                  layout="compact"
                  mediaClassName="h-[4.75rem] w-[4.75rem] sm:h-[5.25rem] sm:w-[5.25rem]"
                  onClick={() =>
                    isBulkDeleteMode
                      ? toggleBulkSelected(npc.id)
                      : readOnly
                        ? onEditTarget({
                            kind: 'story',
                            mode: 'edit',
                            id: npc.id,
                          })
                        : onEditTarget({
                            kind: 'story',
                            mode: 'edit',
                            id: npc.id,
                          })
                  }
                  media={
                    <CustomWorldNpcPortrait
                      npc={npc}
                      profile={profile}
                      visual={npc.visual}
                      className="h-full w-full"
                      contentClassName="min-h-0 p-2"
                      scale={1.82}
                      preferImageSrc
                    />
                  }
                />
              </div>
            ))
          )}
        </div>
      ) : null}

      {activeTab === 'landmarks' ? (
        <div className="space-y-3">
          {pendingGeneratedEntity?.kind === 'landmark' ? (
            <PendingEntityCard
              title={pendingGeneratedEntity.title}
              phaseLabel={pendingGeneratedEntity.phaseLabel}
              progress={pendingGeneratedEntity.progress}
            />
          ) : null}
          {filteredSceneEntries.length === 0 ? (
            <EmptyState title="当前没有符合搜索条件的场景。" />
          ) : (
            filteredSceneEntries.map((scene) => (
              <div key={scene.id}>
                <CatalogCard
                  title={scene.name}
                  description={
                    scene.kind === 'camp'
                      ? `开局场景 · ${scene.description}`
                      : scene.description
                  }
                  badge={
                    scene.kind === 'landmark' && recentLandmarkIdSet.has(scene.id) ? (
                      <NewBadge />
                    ) : null
                  }
                  isSelectionMode={scene.kind === 'landmark' && isBulkDeleteMode}
                  isSelected={
                    scene.kind === 'landmark' &&
                    selectedBulkIds.includes(scene.id)
                  }
                  onClick={() =>
                    scene.kind === 'camp'
                      ? onEditTarget({ kind: 'camp' })
                      : isBulkDeleteMode
                        ? toggleBulkSelected(scene.id)
                        : onEditTarget({
                            kind: 'landmark',
                            mode: 'edit',
                            id: scene.id,
                          })
                  }
                  media={
                    <ImageFrame
                      src={scene.imageSrc}
                      alt={scene.name}
                      fallbackLabel={scene.name.slice(0, 4) || '场景'}
                      tone="landscape"
                    />
                  }
                  disabled={scene.kind === 'camp' && isBulkDeleteMode}
                />
              </div>
            ))
          )}
        </div>
      ) : null}
    </div>
  );
}
