﻿import type { ChangeEvent } from 'react';
import { Children, type ReactNode, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';

import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
import {
  buildCustomWorldPlayableCharacters,
  ROLE_TEMPLATE_CHARACTERS,
} from '../data/characterPresets';
import {
  CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS,
  getCustomWorldSceneRelativePositionLabel,
  normalizeCustomWorldLandmarks,
} from '../data/customWorldSceneGraph';
import {
  getAllCustomWorldSceneImages,
  resolveCustomWorldCampSceneImage,
  resolveCustomWorldLandmarkImage,
} from '../data/customWorldVisuals';
import {
  type CustomWorldSceneImageResult,
  generateCustomWorldSceneImage,
} from '../services/aiService';
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import {
  AnimationState,
  CustomWorldLandmark,
  CustomWorldNpc,
  CustomWorldPlayableNpc,
  CustomWorldProfile,
  CustomWorldSceneConnection,
  type ItemRarity,
} from '../types';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { CharacterAnimator } from './CharacterAnimator';
import { buildDefaultCustomWorldNpcVisual } from './customWorldNpcVisualDefaults';
import {
  CustomWorldNpcPortrait,
  CustomWorldNpcVisualEditor,
} from './CustomWorldNpcVisualEditor';
import { CustomWorldRoleAssetStudioModal } from './CustomWorldRoleAssetStudioModal';
import { PixelIcon } from './PixelIcon';

export type CustomWorldEditorTarget =
  | { kind: 'world' }
  | { kind: 'playable'; mode: 'create' }
  | { kind: 'playable'; mode: 'edit'; id: string }
  | { kind: 'story'; mode: 'create' }
  | { kind: 'story'; mode: 'edit'; id: string }
  | { kind: 'landmark'; mode: 'create' }
  | { kind: 'landmark'; mode: 'edit'; id: string };

interface CustomWorldEntityEditorModalProps {
  profile: CustomWorldProfile;
  target: CustomWorldEditorTarget | null;
  onClose: () => void;
  onProfileChange: (profile: CustomWorldProfile) => void;
}

const [
  BACKSTORY_UNLOCK_AFFINITY_EASED,
  BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
  BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
  BACKSTORY_UNLOCK_AFFINITY_CLOSE,
] = AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS;

const ITEM_RARITY_OPTIONS: Array<{ value: ItemRarity; label: string }> = [
  { value: 'common', label: 'common' },
  { value: 'uncommon', label: 'uncommon' },
  { value: 'rare', label: 'rare' },
  { value: 'epic', label: 'epic' },
  { value: 'legendary', label: 'legendary' },
];

function slugify(value: string) {
  const normalized = value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
    .replace(/^-+|-+$/g, '');

  return normalized || 'entry';
}

function createEntryId(prefix: string, label: string, seed: number) {
  return `${prefix}-${slugify(label || `${prefix}-${seed}`)}-${seed.toString(36)}`;
}

function parseCommaText(value: string) {
  return [
    ...new Set(
      value
        .split(/[\n,，]/u)
        .map((item) => item.trim())
        .filter(Boolean),
    ),
  ];
}

function commaText(value: string[]) {
  return value.join(', ');
}

function clampInitialAffinity(value: string, fallback: number) {
  const parsed = Number.parseInt(value, 10);
  if (!Number.isFinite(parsed)) {
    return fallback;
  }
  return Math.max(-40, Math.min(90, Math.round(parsed)));
}

function parseOptionalNumber(value: string) {
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  const parsed = Number.parseInt(trimmed, 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function createRoleSkillDraft(seedLabel: string, index: number) {
  return {
    id: createEntryId('skill', seedLabel, Date.now() + index),
    name: `新技能${index + 1}`,
    summary: '',
    style: '起手压制',
  };
}

function createRoleInitialItemDraft(seedLabel: string, index: number) {
  return {
    id: createEntryId('item', seedLabel, Date.now() + index),
    name: `新物品${index + 1}`,
    category: '材料',
    quantity: 1,
    rarity: 'rare' as ItemRarity,
    description: '',
    tags: [],
  };
}

function createBackstoryChapterDraft(seedLabel: string, index: number) {
  return {
    id: createEntryId('backstory-chapter', seedLabel, Date.now() + index),
    title: `背景片段${index + 1}`,
    affinityRequired:
      AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS[
        Math.min(index, AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS.length - 1)
      ] ?? BACKSTORY_UNLOCK_AFFINITY_CLOSE,
    teaser: '',
    content: '',
    contextSnippet: '',
  };
}

function syncLandmarksWithStoryNpcs(
  landmarks: CustomWorldLandmark[],
  storyNpcs: CustomWorldProfile['storyNpcs'],
) {
  return normalizeCustomWorldLandmarks({
    landmarks,
    storyNpcs,
  });
}

function useDraft<T>(value: T) {
  const [draft, setDraft] = useState(value);
  useEffect(() => setDraft(value), [value]);
  return [draft, setDraft] as const;
}

function readImageFileAsDataUrl(file: File) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result ?? ''));
    reader.onerror = () => reject(reader.error ?? new Error('读取图片失败。'));
    reader.readAsDataURL(file);
  });
}

function ModalShell({
  title,
  subtitle,
  onClose,
  children,
  panelClassName = 'sm:max-w-2xl',
  overlayClassName = 'z-[98]',
  bodyClassName = '',
  disableClose = false,
  usePixelFont = false,
}: {
  title: string;
  subtitle?: string;
  onClose: () => void;
  children: ReactNode;
  panelClassName?: string;
  overlayClassName?: string;
  bodyClassName?: string;
  disableClose?: boolean;
  usePixelFont?: boolean;
}) {
  return (
    <div
      className={`fixed inset-0 ${overlayClassName} flex items-end justify-center bg-black/78 p-0 backdrop-blur-sm sm:items-center sm:p-4`}
      onClick={disableClose ? undefined : onClose}
    >
      <div
        className={`pixel-nine-slice pixel-modal-shell flex h-[92vh] w-full flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.6)] sm:h-auto sm:max-h-[min(92vh,56rem)] ${usePixelFont ? 'fusion-pixel-app' : ''} ${panelClassName} sm:rounded-[1.75rem]`}
        style={getNineSliceStyle(UI_CHROME.modalPanel)}
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex items-center justify-between gap-3 border-b border-white/10 px-4 py-4 sm:px-5">
          <div className="min-w-0">
            <div className="truncate text-sm font-semibold text-white">
              {title}
            </div>
            {subtitle ? (
              <div className="mt-1 text-xs leading-6 text-zinc-400">
                {subtitle}
              </div>
            ) : null}
          </div>
          <button
            type="button"
            onClick={onClose}
            disabled={disableClose}
            className={`rounded-full border border-white/10 bg-black/20 p-2 text-zinc-400 transition-colors hover:text-white ${disableClose ? 'cursor-not-allowed opacity-45' : ''}`}
          >
            <PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
          </button>
        </div>
        <div
          className={`min-h-0 flex-1 overflow-y-auto p-4 sm:p-5 ${bodyClassName}`}
        >
          {children}
        </div>
      </div>
    </div>
  );
}

function _PortalModalShell(props: {
  title: string;
  subtitle?: string;
  onClose: () => void;
  children: ReactNode;
  panelClassName?: string;
  overlayClassName?: string;
  bodyClassName?: string;
  disableClose?: boolean;
  usePixelFont?: boolean;
}) {
  if (typeof document === 'undefined') {
    return null;
  }

  return createPortal(<ModalShell {...props} />, document.body);
}

function CompactDialogShell({
  title,
  onClose,
  children,
  overlayClassName = 'z-[140]',
  disableClose = false,
  usePixelFont = false,
}: {
  title: string;
  onClose: () => void;
  children: ReactNode;
  overlayClassName?: string;
  disableClose?: boolean;
  usePixelFont?: boolean;
}) {
  return (
    <div
      className={`fixed inset-0 ${overlayClassName} flex items-center justify-center bg-black/78 p-4 backdrop-blur-sm`}
      onClick={disableClose ? undefined : onClose}
    >
      <div
        className={`pixel-nine-slice pixel-modal-shell w-full max-w-md overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.6)] ${usePixelFont ? 'fusion-pixel-app' : ''}`}
        style={getNineSliceStyle(UI_CHROME.modalPanel)}
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex items-center justify-between gap-3 border-b border-white/10 px-4 py-4">
          <div className="min-w-0 text-sm font-semibold text-white">
            {title}
          </div>
          <button
            type="button"
            onClick={onClose}
            disabled={disableClose}
            className={`rounded-full border border-white/10 bg-black/20 p-2 text-zinc-400 transition-colors hover:text-white ${disableClose ? 'cursor-not-allowed opacity-45' : ''}`}
          >
            <PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
          </button>
        </div>
        <div className="p-4">{children}</div>
      </div>
    </div>
  );
}

function PortalCompactDialogShell(props: {
  title: string;
  onClose: () => void;
  children: ReactNode;
  overlayClassName?: string;
  disableClose?: boolean;
  usePixelFont?: boolean;
}) {
  if (typeof document === 'undefined') {
    return null;
  }

  return createPortal(<CompactDialogShell {...props} />, document.body);
}

function Field({ label, children }: { label: string; children: ReactNode }) {
  const hasVisibleChildren = Children.toArray(children).some(
    (child) => !(typeof child === 'string' && child.trim().length === 0),
  );
  if (!hasVisibleChildren) return null;

  return (
    <label className="block">
      <div className="mb-2 text-[11px] font-bold tracking-[0.14em] text-zinc-300">
        {label}
      </div>
      {children}
    </label>
  );
}

function TextInput({
  value,
  onChange,
  type = 'text',
  placeholder,
}: {
  value: string | number;
  onChange: (value: string) => void;
  type?: 'text' | 'number';
  placeholder?: string;
}) {
  return (
    <input
      type={type}
      value={value}
      placeholder={placeholder}
      onChange={(event) => onChange(event.target.value)}
      className="w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-500 focus:border-sky-300/35"
    />
  );
}

function TextArea({
  value,
  onChange,
  rows = 4,
  placeholder,
}: {
  value: string;
  onChange: (value: string) => void;
  rows?: number;
  placeholder?: string;
}) {
  return (
    <textarea
      rows={rows}
      value={value}
      placeholder={placeholder}
      onChange={(event) => onChange(event.target.value)}
      className="w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm leading-7 text-zinc-100 outline-none transition-colors placeholder:text-zinc-500 focus:border-sky-300/35"
    />
  );
}

function SelectField({
  value,
  onChange,
  options,
}: {
  value: string;
  onChange: (value: string) => void;
  options: Array<{ value: string; label: string }>;
}) {
  return (
    <select
      value={value}
      onChange={(event) => onChange(event.target.value)}
      className="w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-3 text-sm text-zinc-100 outline-none transition-colors focus:border-sky-300/35"
    >
      {options.map((option) => (
        <option key={`${option.value}-${option.label}`} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

function ImagePreview({
  src,
  alt,
  fallbackLabel,
  tone = 'square',
  children,
}: {
  src?: string;
  alt: string;
  fallbackLabel: string;
  tone?: 'square' | 'landscape';
  children?: ReactNode;
}) {
  return (
    <div
      className={`relative 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.95),rgba(8,10,17,0.92))] ${tone === 'landscape' ? 'aspect-[16/9]' : 'aspect-square'}`}
    >
      {src ? (
        <img
          src={src}
          alt={alt}
          loading="lazy"
          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>
      )}
      {children ? (
        <div className="pointer-events-none absolute inset-0">{children}</div>
      ) : null}
    </div>
  );
}

function ImageField({
  label,
  value,
  onChange,
  fallbackLabel,
  tone = 'square',
  showInput = true,
  previewOverlay,
  footer,
}: {
  label: string;
  value?: string;
  onChange: (value: string) => void;
  fallbackLabel: string;
  tone?: 'square' | 'landscape';
  showInput?: boolean;
  previewOverlay?: ReactNode;
  footer?: ReactNode;
}) {
  return (
    <div className="space-y-3">
      <div className="text-[11px] font-bold tracking-[0.14em] text-zinc-300">
        {label}
      </div>
      <ImagePreview
        src={value}
        alt={label}
        fallbackLabel={fallbackLabel}
        tone={tone}
      >
        {previewOverlay}
      </ImagePreview>
      {showInput ? (
        <TextInput
          value={value ?? ''}
          onChange={onChange}
          placeholder="支持填写项目内图片路径或外链地址"
        />
      ) : null}
      {footer}
    </div>
  );
}

function ActionButton({
  label,
  onClick,
  tone = 'default',
  disabled = false,
}: {
  label: string;
  onClick: () => void;
  tone?: 'default' | 'sky' | 'rose';
  disabled?: boolean;
}) {
  const toneClassName =
    tone === 'sky'
      ? 'border-sky-300/22 bg-sky-500/12 text-sky-50 hover:border-sky-200/40 hover:text-white'
      : tone === 'rose'
        ? 'border-rose-300/22 bg-rose-500/12 text-rose-50 hover:border-rose-200/40 hover:text-white'
      : 'border-white/12 bg-black/20 text-zinc-200 hover:border-white/22 hover:text-white';

  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className={`rounded-full border px-4 py-2 text-sm font-semibold transition-colors ${toneClassName} ${disabled ? 'cursor-not-allowed opacity-45' : ''}`}
    >
      {label}
    </button>
  );
}

function SceneSparringPreview({ profile }: { profile: CustomWorldProfile }) {
  const sparringCharacters = useMemo(() => {
    const candidates = buildCustomWorldPlayableCharacters(profile);
    if (candidates.length >= 2) {
      return candidates.slice(0, 2);
    }

    if (candidates.length === 1) {
      const firstCandidate = candidates[0];
      if (!firstCandidate) {
        return ROLE_TEMPLATE_CHARACTERS.slice(0, 2);
      }
      const fallback =
        ROLE_TEMPLATE_CHARACTERS.find(
          (character) => character.id !== firstCandidate.id,
        ) ??
        ROLE_TEMPLATE_CHARACTERS[0] ??
        firstCandidate;
      return [firstCandidate, fallback];
    }

    return ROLE_TEMPLATE_CHARACTERS.slice(0, 2);
  }, [profile]);

  const [leftCharacter, rightCharacter] = sparringCharacters;
  if (!leftCharacter || !rightCharacter) {
    return null;
  }

  return (
    <>
      <div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,17,0.02)_0%,rgba(8,10,17,0.08)_42%,rgba(8,10,17,0.68)_100%)]" />
      <div className="absolute left-3 top-3 rounded-full border border-white/12 bg-black/46 px-3 py-1 text-[10px] font-bold tracking-[0.18em] text-zinc-100">
        对战预览
      </div>
      <div className="absolute left-1/2 top-[58%] h-16 w-16 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-300/30 blur-2xl animate-pulse" />
      <div className="absolute inset-x-0 bottom-0 h-20 bg-[linear-gradient(180deg,rgba(8,10,17,0)_0%,rgba(8,10,17,0.72)_100%)]" />
      <div className="absolute inset-x-0 bottom-0 flex items-end justify-between px-2 pb-1 sm:px-4 sm:pb-2">
        <div className="flex flex-col items-center gap-1">
          <div className="rounded-full border border-sky-300/20 bg-black/48 px-2.5 py-0.5 text-[10px] text-sky-50">
            {leftCharacter.name}
          </div>
          <div className="h-24 w-24 sm:h-28 sm:w-28">
            <CharacterAnimator
              state={AnimationState.ATTACK}
              character={leftCharacter}
              className="h-full w-full"
              imageClassName="object-bottom drop-shadow-[0_10px_18px_rgba(0,0,0,0.45)]"
            />
          </div>
        </div>

        <div className="mb-10 rounded-full border border-amber-300/24 bg-amber-400/12 px-3 py-1 text-[10px] font-semibold tracking-[0.18em] text-amber-50">
          试玩中
        </div>

        <div className="flex flex-col items-center gap-1">
          <div className="rounded-full border border-rose-300/20 bg-black/48 px-2.5 py-0.5 text-[10px] text-rose-50">
            {rightCharacter.name}
          </div>
          <div className="h-24 w-24 scale-x-[-1] sm:h-28 sm:w-28">
            <CharacterAnimator
              state={AnimationState.ATTACK}
              character={rightCharacter}
              className="h-full w-full"
              imageClassName="object-bottom drop-shadow-[0_10px_18px_rgba(0,0,0,0.45)]"
            />
          </div>
        </div>
      </div>
    </>
  );
}

function ScenePresetPickerModal({
  selectedSrc,
  presetImages,
  onSelect,
  onClose,
}: {
  selectedSrc?: string;
  presetImages: string[];
  onSelect: (value: string) => void;
  onClose: () => void;
}) {
  return (
    <ModalShell
      title="预设选择"
      subtitle={`共 ${presetImages.length} 张场景图片，点击任意图片即可应用到当前场景。`}
      onClose={onClose}
    >
      <div className="mb-4 rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm leading-6 text-zinc-300">
        当前会同步更新场景图片，结果页和正式进入世界后的背景也会同步更新。
      </div>
      <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
        {presetImages.map((src, index) => {
          const isSelected = src === selectedSrc;
          return (
            <button
              key={src}
              type="button"
              onClick={() => {
                onSelect(src);
                onClose();
              }}
              className={`overflow-hidden rounded-2xl border text-left transition-colors ${
                isSelected
                  ? 'border-sky-300/55 bg-sky-500/10'
                  : 'border-white/10 bg-black/20 hover:border-white/25'
              }`}
            >
              <div className="relative aspect-[16/9] overflow-hidden">
                <img
                  src={src}
                  alt={`Scene preset ${index + 1}`}
                  loading="lazy"
                  className="h-full w-full object-cover"
                />
                <div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(180deg,rgba(8,10,17,0)_0%,rgba(8,10,17,0.82)_100%)] px-3 py-2 text-[11px] text-zinc-100">
                  预设 #{(index + 1).toString().padStart(3, '0')}
                </div>
                {isSelected ? (
                  <div className="absolute right-2 top-2 rounded-full border border-sky-200/40 bg-black/55 px-2 py-0.5 text-[10px] font-semibold text-sky-50">
                    已选中
                  </div>
                ) : null}
              </div>
            </button>
          );
        })}
      </div>
    </ModalShell>
  );
}

const FIXED_SCENE_IMAGE_SIZE = '1280*720';

function SceneImageGenerationModal({
  profile,
  landmark,
  onApply,
  onClose,
}: {
  profile: CustomWorldProfile;
  landmark: CustomWorldLandmark;
  onApply: (result: CustomWorldSceneImageResult) => void;
  onClose: () => void;
}) {
  const [userPrompt, setUserPrompt] = useDraft(
    landmark.name.trim() || landmark.description.trim(),
  );
  const [referenceImageSrc, setReferenceImageSrc] = useState('');
  const [isGenerating, setIsGenerating] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [latestResult, setLatestResult] =
    useState<CustomWorldSceneImageResult | null>(null);
  const [isExitConfirmOpen, setIsExitConfirmOpen] = useState(false);

  const originalImageSrc = useMemo(() => {
    const landmarkIndex = profile.landmarks.findIndex(
      (entry) => entry.id === landmark.id,
    );

    return resolveCustomWorldLandmarkImage(
      profile,
      landmark,
      landmarkIndex >= 0 ? landmarkIndex : profile.landmarks.length,
      profile.landmarks
        .filter((entry) => entry.id !== landmark.id)
        .map((entry) => entry.imageSrc)
        .filter((imageSrc): imageSrc is string => Boolean(imageSrc)),
    );
  }, [landmark, profile]);

  const previewImageSrc = latestResult?.imageSrc || originalImageSrc;

  const handleReferenceImageChange = async (
    event: ChangeEvent<HTMLInputElement>,
  ) => {
    const file = event.target.files?.[0];
    event.currentTarget.value = '';
    if (!file) {
      return;
    }

    try {
      const dataUrl = await readImageFileAsDataUrl(file);
      setReferenceImageSrc(dataUrl);
      setError(null);
    } catch (uploadError) {
      setError(
        uploadError instanceof Error
          ? uploadError.message
          : '参考图读取失败，请重试。',
      );
    }
  };

  const handleRequestClose = () => {
    if (isGenerating) {
      return;
    }
    if (latestResult) {
      setIsExitConfirmOpen(true);
      return;
    }
    onClose();
  };

  const handleGenerate = async () => {
    if (!userPrompt.trim()) {
      setError('请先描述想要生成的画面内容。');
      return;
    }

    setIsGenerating(true);
    setError(null);

    try {
      const result = await generateCustomWorldSceneImage({
        profile,
        landmark,
        userPrompt,
        size: FIXED_SCENE_IMAGE_SIZE,
        ...(referenceImageSrc ? { referenceImageSrc } : {}),
      });
      setLatestResult(result);
    } catch (generationError) {
      setError(
        generationError instanceof Error
          ? generationError.message
          : '场景图片生成失败，请稍后重试。',
      );
    } finally {
      setIsGenerating(false);
    }
  };

  const handleSave = () => {
    if (!latestResult || isGenerating) {
      return;
    }
    onApply(latestResult);
    onClose();
  };

  return (
    <>
      <ModalShell
        title={`智能生成：${landmark.name || '当前场景'}`}
        onClose={handleRequestClose}
        panelClassName="sm:max-w-5xl"
        overlayClassName="z-[99]"
        disableClose={isGenerating}
        usePixelFont
      >
        <div className="grid gap-5 lg:grid-cols-[minmax(0,1.1fr)_minmax(20rem,0.9fr)]">
          <div className="space-y-4">
            <Field label="画面内容描述">
              <TextArea
                value={userPrompt}
                onChange={(value) => setUserPrompt(value)}
                rows={8}
                placeholder="例如：雨夜的悬桥横跨黑色峡谷，桥下翻涌蓝绿色雾潮，远处有半坍塌塔楼与零星灯火。"
              />
            </Field>

            <Field label="自定义参考图（可选）">
              <div className="space-y-3">
                <label className="block rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4">
                  <input
                    type="file"
                    accept="image/png,image/jpeg,image/webp"
                    onChange={(event) => {
                      void handleReferenceImageChange(event);
                    }}
                    className="w-full text-xs text-zinc-300 file:mr-3 file:rounded-lg file:border-0 file:bg-sky-500 file:px-3 file:py-1.5 file:text-xs file:font-medium file:text-black"
                  />
                </label>
                {referenceImageSrc ? (
                  <div className="flex items-center gap-3 rounded-2xl border border-white/8 bg-black/20 p-3">
                    <div className="h-16 w-24 overflow-hidden rounded-xl border border-white/10 bg-black/30">
                      <img
                        src={referenceImageSrc}
                        alt="自定义参考图"
                        className="h-full w-full object-cover"
                      />
                    </div>
                    <div className="min-w-0 flex-1 text-xs leading-5 text-zinc-400">
                      已载入自定义参考图
                    </div>
                    <ActionButton
                      label="移除"
                      onClick={() => setReferenceImageSrc('')}
                      disabled={isGenerating}
                    />
                  </div>
                ) : null}
              </div>
            </Field>
          </div>

          <div className="space-y-4">
            <div className="rounded-2xl border border-white/8 bg-black/18 p-3">
              <ImagePreview
                src={previewImageSrc}
                alt={landmark.name || '场景预览'}
                fallbackLabel={
                  landmark.name ? landmark.name.slice(0, 4) : '场景'
                }
                tone="landscape"
              />
            </div>

            {latestResult ? (
              <div className="rounded-2xl border border-emerald-300/18 bg-emerald-500/10 px-4 py-3 text-sm leading-6 text-emerald-50">
                已生成完毕，请保存后再退出页面
              </div>
            ) : null}

            {error ? (
              <div className="rounded-2xl border border-rose-400/18 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
                {error}
              </div>
            ) : null}

            <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
              <ActionButton
                label="保存"
                onClick={handleSave}
                disabled={!latestResult || isGenerating}
              />
              <ActionButton
                label={
                  isGenerating
                    ? '正在生成...'
                    : latestResult
                      ? '重新生成'
                      : '开始生成'
                }
                onClick={() => {
                  void handleGenerate();
                }}
                tone="sky"
                disabled={isGenerating}
              />
            </div>
          </div>
        </div>
      </ModalShell>

      {isExitConfirmOpen ? (
        <PortalCompactDialogShell
          title="确认退出"
          onClose={() => setIsExitConfirmOpen(false)}
          overlayClassName="z-[140]"
          usePixelFont
        >
          <div className="space-y-4">
            <div className="rounded-2xl border border-amber-300/16 bg-amber-500/10 px-4 py-4 text-sm leading-6 text-amber-50">
              当前生成画面还未保存，退出后将丢失这次生成结果，仍然退出吗？
            </div>
            <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
              <ActionButton
                label="继续编辑"
                onClick={() => setIsExitConfirmOpen(false)}
              />
              <ActionButton
                label="仍然退出"
                onClick={() => {
                  setIsExitConfirmOpen(false);
                  onClose();
                }}
                tone="sky"
              />
            </div>
          </div>
        </PortalCompactDialogShell>
      ) : null}
    </>
  );
}

function SaveBar({
  onClose,
  onSave,
  extraAction,
}: {
  onClose: () => void;
  onSave: () => void;
  extraAction?: ReactNode;
}) {
  return (
    <div className="sticky bottom-0 z-10 -mx-4 border-t border-white/10 bg-[linear-gradient(180deg,rgba(8,10,17,0.2)_0%,rgba(8,10,17,0.96)_28%)] px-4 pb-[calc(env(safe-area-inset-bottom,0px)+0.35rem)] pt-3 backdrop-blur sm:static sm:mx-0 sm:border-0 sm:bg-transparent sm:px-0 sm:pb-0 sm:pt-2 sm:backdrop-blur-0">
      <div
        className={`flex flex-col gap-3 ${
          extraAction
            ? 'sm:flex-row sm:items-center sm:justify-between'
            : 'sm:flex-row sm:justify-end'
        }`}
      >
        {extraAction ? (
          <div className="flex flex-col gap-3 sm:flex-row">{extraAction}</div>
        ) : null}
        <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
          <button
            type="button"
            onClick={onClose}
            className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-sm text-zinc-300 transition-colors hover:text-white"
          >
            取消
          </button>
          <button
            type="button"
            onClick={onSave}
            className="pixel-nine-slice pixel-pressable text-left"
            style={getNineSliceStyle(UI_CHROME.choiceButton, {
              paddingX: 16,
              paddingY: 10,
            })}
          >
            <div className="flex items-center justify-between gap-4">
              <span className="text-sm font-semibold text-white">保存修改</span>
              <span className="text-white/60">→</span>
            </div>
          </button>
        </div>
      </div>
    </div>
  );
}

function SectionPanel({
  title,
  subtitle,
  actions,
  children,
}: {
  title: string;
  subtitle?: string;
  actions?: ReactNode;
  children: ReactNode;
}) {
  return (
    <div className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3 sm:px-4 sm:py-4">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-300">
            {title}
          </div>
          {subtitle ? (
            <div className="mt-2 text-sm leading-6 text-zinc-400">
              {subtitle}
            </div>
          ) : null}
        </div>
        {actions}
      </div>
      <div className="mt-4 space-y-3">{children}</div>
    </div>
  );
}

function BackstoryRevealEditor({
  value,
  onChange,
}: {
  value: CustomWorldPlayableNpc['backstoryReveal'];
  onChange: (value: CustomWorldPlayableNpc['backstoryReveal']) => void;
}) {
  const updateChapter = (
    index: number,
    updater: (
      chapter: CustomWorldPlayableNpc['backstoryReveal']['chapters'][number],
    ) => CustomWorldPlayableNpc['backstoryReveal']['chapters'][number],
  ) => {
    onChange({
      ...value,
      chapters: value.chapters.map((chapter, chapterIndex) =>
        chapterIndex === index ? updater(chapter) : chapter,
      ),
    });
  };

  const addChapter = () => {
    onChange({
      ...value,
      chapters: [
        ...value.chapters,
        createBackstoryChapterDraft('custom-role', value.chapters.length),
      ],
    });
  };

  const removeChapter = (index: number) => {
    if (value.chapters.length <= 1) {
      window.alert('至少保留一个背景章节。');
      return;
    }

    onChange({
      ...value,
      chapters: value.chapters.filter(
        (_chapter, chapterIndex) => chapterIndex !== index,
      ),
    });
  };

  return (
    <SectionPanel
      title="背景公开与章节"
      subtitle="这里直接决定结果页、关系推进和后续剧情提示词看到的背景摘要与章节线索。"
      actions={
        <ActionButton label="新增章节" onClick={addChapter} tone="sky" />
      }
    >
      <Field label="公开背景摘要">
        <TextArea
          value={value.publicSummary}
          onChange={(nextValue) =>
            onChange({
              ...value,
              publicSummary: nextValue,
            })
          }
          rows={3}
        />
      </Field>
      {value.chapters.map((chapter, index) => (
        <div
          key={`${chapter.id}-${index}`}
          className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
        >
          <div className="mb-3 flex items-center justify-between gap-3">
            <div className="text-sm font-semibold text-white">
              背景章节 #{index + 1}
            </div>
            <ActionButton
              label="删除章节"
              onClick={() => removeChapter(index)}
            />
          </div>
          <Field label="章节标题">
            <TextInput
              value={chapter.title}
              onChange={(nextValue) =>
                updateChapter(index, (current) => ({
                  ...current,
                  title: nextValue,
                }))
              }
            />
          </Field>
          <Field label="解锁好感">
            <TextInput
              type="number"
              value={chapter.affinityRequired}
              onChange={(nextValue) =>
                updateChapter(index, (current) => ({
                  ...current,
                  affinityRequired: clampInitialAffinity(
                    nextValue,
                    current.affinityRequired,
                  ),
                }))
              }
            />
          </Field>
          <Field label="章节提示">
            <TextArea
              value={chapter.teaser}
              onChange={(nextValue) =>
                updateChapter(index, (current) => ({
                  ...current,
                  teaser: nextValue,
                }))
              }
              rows={2}
            />
          </Field>
          <Field label="章节内容">
            <TextArea
              value={chapter.content}
              onChange={(nextValue) =>
                updateChapter(index, (current) => ({
                  ...current,
                  content: nextValue,
                }))
              }
              rows={3}
            />
          </Field>
          <Field label="剧情引用摘要">
            <TextArea
              value={chapter.contextSnippet}
              onChange={(nextValue) =>
                updateChapter(index, (current) => ({
                  ...current,
                  contextSnippet: nextValue,
                }))
              }
              rows={2}
            />
          </Field>
        </div>
      ))}
    </SectionPanel>
  );
}

function SkillListEditor({
  value,
  onChange,
  labelSeed,
}: {
  value: CustomWorldPlayableNpc['skills'];
  onChange: (value: CustomWorldPlayableNpc['skills']) => void;
  labelSeed: string;
}) {
  const updateSkill = (
    index: number,
    updater: (
      skill: CustomWorldPlayableNpc['skills'][number],
    ) => CustomWorldPlayableNpc['skills'][number],
  ) => {
    onChange(
      value.map((skill, skillIndex) =>
        skillIndex === index ? updater(skill) : skill,
      ),
    );
  };

  return (
    <SectionPanel
      title="技能"
      subtitle="技能名、摘要和风格都会进入结果页与运行时 NPC 档案。"
      actions={
        <ActionButton
          label="新增技能"
          onClick={() =>
            onChange([...value, createRoleSkillDraft(labelSeed, value.length)])
          }
          tone="sky"
        />
      }
    >
      {value.map((skill, index) => (
        <div
          key={`${skill.id}-${index}`}
          className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
        >
          <div className="mb-3 flex items-center justify-between gap-3">
            <div className="text-sm font-semibold text-white">
              技能 #{index + 1}
            </div>
            <ActionButton
              label="删除技能"
              onClick={() =>
                onChange(
                  value.filter((_skill, skillIndex) => skillIndex !== index),
                )
              }
            />
          </div>
          <Field label="技能名称">
            <TextInput
              value={skill.name}
              onChange={(nextValue) =>
                updateSkill(index, (current) => ({
                  ...current,
                  name: nextValue,
                }))
              }
            />
          </Field>
          <Field label="技能风格">
            <TextInput
              value={skill.style}
              onChange={(nextValue) =>
                updateSkill(index, (current) => ({
                  ...current,
                  style: nextValue,
                }))
              }
            />
          </Field>
          <Field label="技能摘要">
            <TextArea
              value={skill.summary}
              onChange={(nextValue) =>
                updateSkill(index, (current) => ({
                  ...current,
                  summary: nextValue,
                }))
              }
              rows={3}
            />
          </Field>
        </div>
      ))}
    </SectionPanel>
  );
}

function InitialItemsEditor({
  value,
  onChange,
  labelSeed,
}: {
  value: CustomWorldPlayableNpc['initialItems'];
  onChange: (value: CustomWorldPlayableNpc['initialItems']) => void;
  labelSeed: string;
}) {
  const updateItem = (
    index: number,
    updater: (
      item: CustomWorldPlayableNpc['initialItems'][number],
    ) => CustomWorldPlayableNpc['initialItems'][number],
  ) => {
    onChange(
      value.map((item, itemIndex) =>
        itemIndex === index ? updater(item) : item,
      ),
    );
  };

  return (
    <SectionPanel
      title="初始物品"
      subtitle="这里的内容会影响结果页展示，也会作为后续运行时参考档案。"
      actions={
        <ActionButton
          label="新增物品"
          onClick={() =>
            onChange([
              ...value,
              createRoleInitialItemDraft(labelSeed, value.length),
            ])
          }
          tone="sky"
        />
      }
    >
      {value.map((item, index) => (
        <div
          key={`${item.id}-${index}`}
          className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
        >
          <div className="mb-3 flex items-center justify-between gap-3">
            <div className="text-sm font-semibold text-white">
              物品 #{index + 1}
            </div>
            <ActionButton
              label="删除物品"
              onClick={() =>
                onChange(
                  value.filter((_item, itemIndex) => itemIndex !== index),
                )
              }
            />
          </div>
          <Field label="名称">
            <TextInput
              value={item.name}
              onChange={(nextValue) =>
                updateItem(index, (current) => ({
                  ...current,
                  name: nextValue,
                }))
              }
            />
          </Field>
          <div className="grid gap-3 sm:grid-cols-2">
            <Field label="分类">
              <TextInput
                value={item.category}
                onChange={(nextValue) =>
                  updateItem(index, (current) => ({
                    ...current,
                    category: nextValue,
                  }))
                }
              />
            </Field>
            <Field label="稀有度">
              <SelectField
                value={item.rarity}
                onChange={(nextValue) =>
                  updateItem(index, (current) => ({
                    ...current,
                    rarity: nextValue as ItemRarity,
                  }))
                }
                options={ITEM_RARITY_OPTIONS}
              />
            </Field>
          </div>
          <Field label="数量">
            <TextInput
              type="number"
              value={item.quantity}
              onChange={(nextValue) =>
                updateItem(index, (current) => ({
                  ...current,
                  quantity: Math.max(
                    1,
                    parseOptionalNumber(nextValue) ?? current.quantity,
                  ),
                }))
              }
            />
          </Field>
          <Field label="描述">
            <TextArea
              value={item.description}
              onChange={(nextValue) =>
                updateItem(index, (current) => ({
                  ...current,
                  description: nextValue,
                }))
              }
              rows={3}
            />
          </Field>
          <Field label="标签">
            <TextArea
              value={commaText(item.tags)}
              onChange={(nextValue) =>
                updateItem(index, (current) => ({
                  ...current,
                  tags: parseCommaText(nextValue),
                }))
              }
              rows={2}
            />
          </Field>
        </div>
      ))}
    </SectionPanel>
  );
}

function StoryNpcVisualEditorModal({
  npc,
  visual,
  onChange,
  onOpenAiStudio,
  onClose,
}: {
  npc: CustomWorldNpc;
  visual: NonNullable<CustomWorldNpc['visual']>;
  onChange: (visual: NonNullable<CustomWorldNpc['visual']>) => void;
  onOpenAiStudio?: () => void;
  onClose: () => void;
}) {
  return (
    <ModalShell
      title={`修改形象：${npc.name}`}
      subtitle="在独立面板中组合中世纪奇幻角色形象，左侧预览会保持吸顶。"
      onClose={onClose}
      panelClassName="sm:max-w-6xl"
      overlayClassName="z-[99]"
    >
      <CustomWorldNpcVisualEditor
        npc={{
          id: npc.id,
          name: npc.name,
          role: npc.role,
          description: npc.description,
        }}
        value={visual}
        onChange={onChange}
        onAiGenerate={() => {
          onClose();
          onOpenAiStudio?.();
        }}
      />
    </ModalShell>
  );
}

function WorldEditor({
  profile,
  onSave,
  onClose,
}: {
  profile: CustomWorldProfile;
  onSave: (profile: CustomWorldProfile) => void;
  onClose: () => void;
}) {
  const [draft, setDraft] = useDraft(profile);
  const [isCampPresetPickerOpen, setIsCampPresetPickerOpen] = useState(false);
  const [isCampAiGenerateOpen, setIsCampAiGenerateOpen] = useState(false);
  const presetImages = useMemo(() => getAllCustomWorldSceneImages(), []);
  const resolvedCampScene = useMemo(
    () => resolveCustomWorldCampScene(draft),
    [draft],
  );
  const resolvedCampImageSrc = useMemo(
    () => resolveCustomWorldCampSceneImage(draft),
    [draft],
  );
  const campSceneDraft = useMemo<CustomWorldLandmark>(
    () => ({
      id: 'custom-scene-camp',
      name: resolvedCampScene.name,
      description: resolvedCampScene.description,
      dangerLevel: resolvedCampScene.dangerLevel,
      imageSrc: resolvedCampScene.imageSrc,
      sceneNpcIds: [],
      connections: [],
    }),
    [resolvedCampScene],
  );

  return (
    <ModalShell
      title="编辑世界信息"
      subtitle="修改后的内容会直接反映在结果页，并会作为进入世界前的最终档案。"
      onClose={onClose}
    >
      <div className="space-y-4">
        <Field label="世界名称">
          <TextInput
            value={draft.name}
            onChange={(value) =>
              setDraft((current) => ({ ...current, name: value }))
            }
          />
        </Field>
        <Field label="副标题">
          <TextInput
            value={draft.subtitle}
            onChange={(value) =>
              setDraft((current) => ({ ...current, subtitle: value }))
            }
          />
        </Field>
        <Field label="世界概述">
          <TextArea
            value={draft.summary}
            onChange={(value) =>
              setDraft((current) => ({ ...current, summary: value }))
            }
            rows={4}
          />
        </Field>
        <Field label="世界基调">
          <TextArea
            value={draft.tone}
            onChange={(value) =>
              setDraft((current) => ({ ...current, tone: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="主线目标">
          <TextArea
            value={draft.playerGoal}
            onChange={(value) =>
              setDraft((current) => ({ ...current, playerGoal: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="开局归处名称">
          <TextInput
            value={resolvedCampScene.name}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                camp: {
                  ...resolveCustomWorldCampScene(current),
                  name: value,
                },
              }))
            }
          />
        </Field>
        <Field label="开局归处描述">
          <TextArea
            value={resolvedCampScene.description}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                camp: {
                  ...resolveCustomWorldCampScene(current),
                  description: value,
                },
              }))
            }
            rows={4}
          />
        </Field>
        <Field label="开局归处危险度">
          <TextInput
            value={resolvedCampScene.dangerLevel}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                camp: {
                  ...resolveCustomWorldCampScene(current),
                  dangerLevel: value,
                },
              }))
            }
          />
        </Field>
        <ImageField
          label="开局归处背景"
          value={resolvedCampImageSrc}
          onChange={(value) =>
            setDraft((current) => ({
              ...current,
              camp: {
                ...resolveCustomWorldCampScene(current),
                imageSrc: value || undefined,
              },
            }))
          }
          fallbackLabel={resolvedCampScene.name.slice(0, 4) || '归处'}
          tone="landscape"
          showInput={false}
          previewOverlay={<SceneSparringPreview profile={draft} />}
          footer={
            <div className="space-y-3">
              <div className="flex flex-wrap gap-3">
                <ActionButton
                  label="预设选择"
                  onClick={() => setIsCampPresetPickerOpen(true)}
                  tone="sky"
                />
                <ActionButton
                  label="智能生成"
                  onClick={() => setIsCampAiGenerateOpen(true)}
                />
              </div>
              <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm leading-6 text-zinc-300">
                开局归处会直接作为进入自定义世界时的第一张背景。这里可以单独指定或生成这张背景图。
              </div>
            </div>
          }
        />
        <Field label="玩家原始设定">
          <TextArea
            value={draft.settingText}
            onChange={(value) =>
              setDraft((current) => ({ ...current, settingText: value }))
            }
            rows={4}
          />
        </Field>
        {isCampPresetPickerOpen ? (
          <ScenePresetPickerModal
            selectedSrc={resolvedCampScene.imageSrc}
            presetImages={presetImages}
            onSelect={(value) =>
              setDraft((current) => ({
                ...current,
                camp: {
                  ...resolveCustomWorldCampScene(current),
                  imageSrc: value,
                },
              }))
            }
            onClose={() => setIsCampPresetPickerOpen(false)}
          />
        ) : null}
        {isCampAiGenerateOpen ? (
          <SceneImageGenerationModal
            profile={draft}
            landmark={campSceneDraft}
            onApply={(result) => {
              setDraft((current) => ({
                ...current,
                camp: {
                  ...resolveCustomWorldCampScene(current),
                  imageSrc: result.imageSrc,
                },
              }));
            }}
            onClose={() => setIsCampAiGenerateOpen(false)}
          />
        ) : null}
        <SaveBar
          onClose={onClose}
          onSave={() => {
            onSave(draft);
            onClose();
          }}
        />
      </div>
    </ModalShell>
  );
}

function PlayableNpcEditor({
  npc,
  mode,
  onSave,
  onClose,
}: {
  npc: CustomWorldPlayableNpc;
  mode: 'create' | 'edit';
  onSave: (npc: CustomWorldPlayableNpc) => void;
  onClose: () => void;
}) {
  const [draft, setDraft] = useDraft(npc);
  const [isAiAssetStudioOpen, setIsAiAssetStudioOpen] = useState(false);
  const selectedTemplate =
    ROLE_TEMPLATE_CHARACTERS.find(
      (character) => character.id === draft.templateCharacterId,
    ) ??
    ROLE_TEMPLATE_CHARACTERS[0] ??
    null;

  return (
    <ModalShell
      title={mode === 'create' ? '新增可扮演角色' : `编辑角色：${npc.name}`}
      subtitle="这里可以直接修改可扮演角色的完整档案字段，结果页和正式选角都会同步使用。"
      onClose={onClose}
    >
      <div className="space-y-4">
        {selectedTemplate ? (
          <div className="grid gap-4 rounded-2xl border border-white/8 bg-black/20 p-4 sm:grid-cols-[7rem_minmax(0,1fr)]">
            <div className="overflow-hidden rounded-2xl border border-white/10 bg-black/30">
              <img
                src={draft.imageSrc || selectedTemplate.portrait}
                alt={selectedTemplate.name}
                className="h-28 w-full object-cover object-top"
              />
            </div>
            <div className="min-w-0">
              <div className="text-[11px] tracking-[0.18em] text-zinc-500">
                当前外观模板
              </div>
              <div className="mt-2 text-base font-semibold text-white">
                {selectedTemplate.name}
              </div>
              <div className="mt-1 text-sm text-zinc-400">
                {selectedTemplate.title}
              </div>
              <div className="mt-3 text-sm leading-6 text-zinc-300">
                {selectedTemplate.description}
              </div>
              <div className="mt-3 flex flex-wrap gap-2">
                {draft.generatedVisualAssetId ? (
                  <span className="rounded-full border border-emerald-400/20 bg-emerald-500/10 px-2.5 py-1 text-[10px] text-emerald-100">
                    已应用主图
                  </span>
                ) : null}
                {draft.generatedAnimationSetId ? (
                  <span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-2.5 py-1 text-[10px] text-amber-100">
                    已应用动作
                  </span>
                ) : null}
              </div>
              <div className="mt-3">
                <ActionButton
                  label="AI生成形象与动作"
                  onClick={() => setIsAiAssetStudioOpen(true)}
                  tone="sky"
                />
              </div>
            </div>
          </div>
        ) : null}

        <Field label="外观模板">
          <SelectField
            value={draft.templateCharacterId ?? ROLE_TEMPLATE_CHARACTERS[0]?.id ?? ''}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                templateCharacterId: value,
              }))
            }
            options={ROLE_TEMPLATE_CHARACTERS.map((character) => ({
              value: character.id,
              label: `${character.name} / ${character.title}`,
            }))}
          />
        </Field>
        <Field label="名称">
          <TextInput
            value={draft.name}
            onChange={(value) =>
              setDraft((current) => ({ ...current, name: value }))
            }
          />
        </Field>
        <Field label="称号 / 头衔">
          <TextInput
            value={draft.title}
            onChange={(value) =>
              setDraft((current) => ({ ...current, title: value }))
            }
          />
        </Field>
        <Field label="世界身份 / 职责">
          <TextInput
            value={draft.role}
            onChange={(value) =>
              setDraft((current) => ({ ...current, role: value }))
            }
          />
        </Field>
        <Field label="简介">
          <TextArea
            value={draft.description}
            onChange={(value) =>
              setDraft((current) => ({ ...current, description: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="背景">
          <TextArea
            value={draft.backstory}
            onChange={(value) =>
              setDraft((current) => ({ ...current, backstory: value }))
            }
            rows={5}
          />
        </Field>
        <Field label="性格">
          <TextArea
            value={draft.personality}
            onChange={(value) =>
              setDraft((current) => ({ ...current, personality: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="当前动机">
          <TextArea
            value={draft.motivation}
            onChange={(value) =>
              setDraft((current) => ({ ...current, motivation: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="战斗风格">
          <TextArea
            value={draft.combatStyle}
            onChange={(value) =>
              setDraft((current) => ({ ...current, combatStyle: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="初始好感">
          <TextInput
            type="number"
            value={draft.initialAffinity}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                initialAffinity: clampInitialAffinity(
                  value,
                  current.initialAffinity,
                ),
              }))
            }
          />
        </Field>
        <Field label="关系切入口">
          <TextArea
            value={commaText(draft.relationshipHooks)}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                relationshipHooks: parseCommaText(value),
              }))
            }
            rows={2}
          />
        </Field>
        <Field label="标签">
          <TextArea
            value={commaText(draft.tags)}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                tags: parseCommaText(value),
              }))
            }
            rows={2}
          />
        </Field>
        <BackstoryRevealEditor
          value={draft.backstoryReveal}
          onChange={(backstoryReveal) =>
            setDraft((current) => ({
              ...current,
              backstoryReveal,
            }))
          }
        />
        <SkillListEditor
          value={draft.skills}
          onChange={(skills) =>
            setDraft((current) => ({
              ...current,
              skills,
            }))
          }
          labelSeed={draft.name || draft.id}
        />
        <InitialItemsEditor
          value={draft.initialItems}
          onChange={(initialItems) =>
            setDraft((current) => ({
              ...current,
              initialItems,
            }))
          }
          labelSeed={draft.name || draft.id}
        />
        <SaveBar
          onClose={onClose}
          onSave={() => {
            onSave({
              ...draft,
              templateCharacterId:
                draft.templateCharacterId ?? ROLE_TEMPLATE_CHARACTERS[0]?.id,
            });
            onClose();
          }}
        />
        {isAiAssetStudioOpen ? (
          <CustomWorldRoleAssetStudioModal
            role={draft}
            roleKind="playable"
            onApply={(nextRole) =>
              setDraft((current) => ({
                ...current,
                ...nextRole,
              }))
            }
            onClose={() => setIsAiAssetStudioOpen(false)}
          />
        ) : null}
      </div>
    </ModalShell>
  );
}

function StoryNpcEditor({
  npc,
  mode,
  onSave,
  onClose,
}: {
  npc: CustomWorldNpc;
  mode: 'create' | 'edit';
  onSave: (npc: CustomWorldNpc) => void;
  onClose: () => void;
}) {
  const [draft, setDraft] = useDraft(npc);
  const [isVisualEditorOpen, setIsVisualEditorOpen] = useState(false);
  const [isAiAssetStudioOpen, setIsAiAssetStudioOpen] = useState(false);

  return (
    <ModalShell
      title={mode === 'create' ? '新增场景角色' : `编辑场景角色：${npc.name}`}
      subtitle="这里可以直接修改场景角色的完整档案字段，形象编辑仍保留在独立面板。"
      onClose={onClose}
    >
      <div className="space-y-4">
        <div className="rounded-3xl border border-white/10 bg-black/20 p-4">
          <div className="grid gap-4 sm:grid-cols-[10rem_minmax(0,1fr)] sm:items-center">
            <div className="flex justify-center">
              <CustomWorldNpcPortrait
                npc={draft}
                visual={draft.visual}
                className="aspect-square w-full max-w-[9.5rem]"
                scale={2.05}
                preferImageSrc
              />
            </div>
            <div className="min-w-0 space-y-3">
              <div>
                <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-300">
                  场景角色形象预览
                </div>
                <div className="mt-2 text-sm leading-6 text-zinc-400">
                  形象设定已单独拆到专门面板中编辑。这里保持卡片大小预览，方便边改档案边确认当前人物形象。
                </div>
              </div>
              <div className="flex flex-wrap gap-3">
                <ActionButton
                  label="修改形象"
                  onClick={() => setIsVisualEditorOpen(true)}
                  tone="sky"
                />
                <ActionButton
                  label="AI生成形象与动作"
                  onClick={() => setIsAiAssetStudioOpen(true)}
                  tone="sky"
                />
              </div>
              <div className="flex flex-wrap gap-2">
                {draft.generatedVisualAssetId ? (
                  <span className="rounded-full border border-emerald-400/20 bg-emerald-500/10 px-2.5 py-1 text-[10px] text-emerald-100">
                    已应用主图
                  </span>
                ) : null}
                {draft.generatedAnimationSetId ? (
                  <span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-2.5 py-1 text-[10px] text-amber-100">
                    已应用动作
                  </span>
                ) : null}
              </div>
            </div>
          </div>
        </div>
        <Field label="名称">
          <TextInput
            value={draft.name}
            onChange={(value) =>
              setDraft((current) => ({ ...current, name: value }))
            }
          />
        </Field>
        <Field label="头衔 / 职能">
          <TextInput
            value={draft.title}
            onChange={(value) =>
              setDraft((current) => ({ ...current, title: value }))
            }
          />
        </Field>
        <Field label="世界身份 / 职能">
          <TextInput
            value={draft.role}
            onChange={(value) =>
              setDraft((current) => ({ ...current, role: value }))
            }
          />
        </Field>
        <Field label="描述">
          <TextArea
            value={draft.description}
            onChange={(value) =>
              setDraft((current) => ({ ...current, description: value }))
            }
            rows={4}
          />
        </Field>
        <Field label="背景">
          <TextArea
            value={draft.backstory}
            onChange={(value) =>
              setDraft((current) => ({ ...current, backstory: value }))
            }
            rows={4}
          />
        </Field>
        <Field label="性格">
          <TextArea
            value={draft.personality}
            onChange={(value) =>
              setDraft((current) => ({ ...current, personality: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="动机">
          <TextArea
            value={draft.motivation}
            onChange={(value) =>
              setDraft((current) => ({ ...current, motivation: value }))
            }
            rows={4}
          />
        </Field>
        <Field label="战斗风格">
          <TextArea
            value={draft.combatStyle}
            onChange={(value) =>
              setDraft((current) => ({ ...current, combatStyle: value }))
            }
            rows={3}
          />
        </Field>
        <Field label="初始好感">
          <TextInput
            type="number"
            value={draft.initialAffinity}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                initialAffinity: clampInitialAffinity(
                  value,
                  current.initialAffinity,
                ),
              }))
            }
          />
        </Field>
        <Field label="关系切入口">
          <TextArea
            value={commaText(draft.relationshipHooks)}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                relationshipHooks: parseCommaText(value),
              }))
            }
            rows={3}
          />
        </Field>
        <Field label="标签">
          <TextArea
            value={commaText(draft.tags)}
            onChange={(value) =>
              setDraft((current) => ({
                ...current,
                tags: parseCommaText(value),
              }))
            }
            rows={2}
          />
        </Field>
        <BackstoryRevealEditor
          value={draft.backstoryReveal}
          onChange={(backstoryReveal) =>
            setDraft((current) => ({
              ...current,
              backstoryReveal,
            }))
          }
        />
        <SkillListEditor
          value={draft.skills}
          onChange={(skills) =>
            setDraft((current) => ({
              ...current,
              skills,
            }))
          }
          labelSeed={draft.name || draft.id}
        />
        <InitialItemsEditor
          value={draft.initialItems}
          onChange={(initialItems) =>
            setDraft((current) => ({
              ...current,
              initialItems,
            }))
          }
          labelSeed={draft.name || draft.id}
        />
        <SaveBar
          onClose={onClose}
          onSave={() => {
            onSave(draft);
            onClose();
          }}
        />
        {isVisualEditorOpen ? (
          <StoryNpcVisualEditorModal
            npc={draft}
            visual={
              draft.visual ??
              buildDefaultCustomWorldNpcVisual({
                id: draft.id,
                name: draft.name,
                role: draft.role,
                description: draft.description,
              })
            }
            onChange={(visual) =>
              setDraft((current) => ({ ...current, visual }))
            }
            onOpenAiStudio={() => setIsAiAssetStudioOpen(true)}
            onClose={() => setIsVisualEditorOpen(false)}
          />
        ) : null}
        {isAiAssetStudioOpen ? (
          <CustomWorldRoleAssetStudioModal
            role={draft}
            roleKind="story"
            onApply={(nextRole) =>
              setDraft((current) => ({
                ...current,
                ...nextRole,
              }))
            }
            onClose={() => setIsAiAssetStudioOpen(false)}
          />
        ) : null}
      </div>
    </ModalShell>
  );
}

function LandmarkEditor({
  profile,
  landmark,
  mode,
  onSaveProfile,
  onClose,
}: {
  profile: CustomWorldProfile;
  landmark: CustomWorldLandmark;
  mode: 'create' | 'edit';
  onSaveProfile: (profile: CustomWorldProfile) => void;
  onClose: () => void;
}) {
  const [draft, setDraft] = useDraft(landmark);
  const [draftStoryNpcs, setDraftStoryNpcs] = useDraft(profile.storyNpcs);
  const [isPresetPickerOpen, setIsPresetPickerOpen] = useState(false);
  const [isAiGenerateOpen, setIsAiGenerateOpen] = useState(false);
  const [npcEditorState, setNpcEditorState] = useState<{
    mode: 'create' | 'edit';
    npc: CustomWorldNpc;
  } | null>(null);
  const presetImages = useMemo(() => getAllCustomWorldSceneImages(), []);
  const resolvedDraftImageSrc = useMemo(() => {
    const landmarkIndex = profile.landmarks.findIndex(
      (entry) => entry.id === draft.id,
    );

    return resolveCustomWorldLandmarkImage(
      profile,
      draft,
      landmarkIndex >= 0 ? landmarkIndex : profile.landmarks.length,
      profile.landmarks
        .filter((entry) => entry.id !== draft.id)
        .map((entry) => entry.imageSrc)
        .filter((imageSrc): imageSrc is string => Boolean(imageSrc)),
    );
  }, [draft, profile]);
  const storyNpcById = useMemo(
    () => new Map(draftStoryNpcs.map((npc) => [npc.id, npc])),
    [draftStoryNpcs],
  );
  const availableTargetLandmarks = useMemo(
    () => profile.landmarks.filter((entry) => entry.id !== draft.id),
    [draft.id, profile.landmarks],
  );

  const toggleSceneNpc = (npcId: string) => {
    setDraft((current) => ({
      ...current,
      sceneNpcIds: current.sceneNpcIds.includes(npcId)
        ? current.sceneNpcIds.filter((entry) => entry !== npcId)
        : [...current.sceneNpcIds, npcId],
    }));
  };

  const updateConnection = (
    index: number,
    updater: (
      connection: CustomWorldSceneConnection,
    ) => CustomWorldSceneConnection,
  ) => {
    setDraft((current) => ({
      ...current,
      connections: current.connections.map((connection, connectionIndex) =>
        connectionIndex === index ? updater(connection) : connection,
      ),
    }));
  };

  const addConnection = () => {
    const fallbackTarget = availableTargetLandmarks[0];
    if (!fallbackTarget) {
      window.alert('请先保留至少一个其他场景，才能配置连接关系。');
      return;
    }

    setDraft((current) => ({
      ...current,
      connections: [
        ...current.connections,
        {
          targetLandmarkId: fallbackTarget.id,
          relativePosition: 'forward',
          summary: `可通往${fallbackTarget.name}`,
        },
      ],
    }));
  };

  const saveLandmarkProfile = () => {
    if (draft.sceneNpcIds.length < 3) {
      window.alert('每个场景至少需要分配 3 个 NPC。');
      return;
    }

    const nextLandmarks =
      mode === 'create'
        ? [...profile.landmarks, draft]
        : profile.landmarks.map((entry) =>
            entry.id === draft.id ? draft : entry,
          );

    onSaveProfile({
      ...profile,
      storyNpcs: draftStoryNpcs,
      landmarks: syncLandmarksWithStoryNpcs(nextLandmarks, draftStoryNpcs),
    });
    onClose();
  };

  return (
    <ModalShell
      title={mode === 'create' ? '新增场景' : `编辑场景：${landmark.name}`}
      subtitle="这里可以同时配置场景图片、场景内 NPC，以及场景之间的相对位置连接关系。"
      onClose={onClose}
    >
      <div className="space-y-4">
        <ImageField
          label="场景图片"
          value={resolvedDraftImageSrc}
          onChange={(value) =>
            setDraft((current) => ({
              ...current,
              imageSrc: value || undefined,
            }))
          }
          fallbackLabel={draft.name ? draft.name.slice(0, 4) : '背景'}
          tone="landscape"
          showInput={false}
          previewOverlay={<SceneSparringPreview profile={profile} />}
          footer={
            <div className="space-y-3">
              <div className="flex flex-wrap gap-3">
                <ActionButton
                  label="预设选择"
                  onClick={() => setIsPresetPickerOpen(true)}
                  tone="sky"
                />
                <ActionButton
                  label="智能生成"
                  onClick={() => setIsAiGenerateOpen(true)}
                />
              </div>
              <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-sm leading-6 text-zinc-300">
                预览图中的切割预览会优先提取当前自定义世界的可选角色颜色，仅用于辅助确认场景截图，不会修改颜色面板。智能生成会把结果保存为本地可复用的场景资源。
              </div>
            </div>
          }
        />
        <Field label="名称">
          <TextInput
            value={draft.name}
            onChange={(value) =>
              setDraft((current) => ({ ...current, name: value }))
            }
          />
        </Field>
        <Field label="描述">
          <TextArea
            value={draft.description}
            onChange={(value) =>
              setDraft((current) => ({ ...current, description: value }))
            }
            rows={5}
          />
        </Field>
        <Field label="危险度">
          <TextInput
            value={draft.dangerLevel}
            onChange={(value) =>
              setDraft((current) => ({ ...current, dangerLevel: value }))
            }
          />
        </Field>
        <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
          <div className="flex flex-wrap items-center justify-between gap-3">
            <div>
              <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-300">
                场景内 NPC
              </div>
              <div className="mt-2 text-sm leading-6 text-zinc-400">
                每个场景至少保留 3 个 NPC。可以在这里直接继续新增
                NPC，并立即加入当前场景。
              </div>
            </div>
            <ActionButton
              label="新增 NPC 并加入此场景"
              onClick={() =>
                setNpcEditorState({
                  mode: 'create',
                  npc: createStoryNpc({ storyNpcs: draftStoryNpcs }),
                })
              }
              tone="sky"
            />
          </div>
          <div className="mt-3 space-y-2">
            {draft.sceneNpcIds.length > 0 ? (
              draft.sceneNpcIds.map((npcId) => {
                const npc = storyNpcById.get(npcId);
                return (
                  <div
                    key={`${draft.id}-selected-npc-${npcId}`}
                    className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
                  >
                    <div className="flex flex-wrap items-center justify-between gap-3">
                      <div className="min-w-0">
                        <div className="text-sm font-semibold text-white">
                          {npc?.name ?? '未匹配场景角色'}
                        </div>
                        <div className="mt-1 text-xs leading-6 text-zinc-400">
                          {npc?.role || npc?.title || '未填写身份'}
                        </div>
                      </div>
                      <div className="flex flex-wrap gap-2">
                        {npc ? (
                          <ActionButton
                            label="编辑"
                            onClick={() =>
                              setNpcEditorState({
                                mode: 'edit',
                                npc,
                              })
                            }
                            tone="sky"
                          />
                        ) : null}
                        <ActionButton
                          label="移出场景"
                          onClick={() => toggleSceneNpc(npcId)}
                        />
                      </div>
                    </div>
                  </div>
                );
              })
            ) : (
              <div className="rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4 text-sm text-zinc-500">
                还没有为这个场景分配 NPC。
              </div>
            )}
          </div>
          <div className="mt-3 max-h-64 space-y-2 overflow-y-auto pr-1">
            {draftStoryNpcs.map((npc) => {
              const selected = draft.sceneNpcIds.includes(npc.id);
              return (
                <button
                  key={`${draft.id}-npc-picker-${npc.id}`}
                  type="button"
                  onClick={() => toggleSceneNpc(npc.id)}
                  className={`w-full rounded-2xl border px-3 py-3 text-left transition-colors ${
                    selected
                      ? 'border-sky-300/28 bg-sky-500/10'
                      : 'border-white/8 bg-black/20 hover:border-white/18'
                  }`}
                >
                  <div className="flex items-start justify-between gap-3">
                    <div className="min-w-0">
                      <div className="text-sm font-semibold text-white">
                        {npc.name}
                      </div>
                      <div className="mt-1 text-xs leading-6 text-zinc-400">
                        {npc.role}
                      </div>
                    </div>
                    <div className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[10px] text-zinc-300">
                      {selected ? '已加入' : '点击加入'}
                    </div>
                  </div>
                </button>
              );
            })}
          </div>
        </div>
        <div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
          <div className="flex flex-wrap items-center justify-between gap-3">
            <div>
              <div className="text-[11px] font-bold tracking-[0.16em] text-zinc-300">
                场景连接关系
              </div>
              <div className="mt-2 text-sm leading-6 text-zinc-400">
                编辑当前场景与其他场景之间的相对位置关系。保存时会自动同步反向连线，避免地图断开。
              </div>
            </div>
            <ActionButton label="新增连接" onClick={addConnection} tone="sky" />
          </div>
          <div className="mt-3 space-y-3">
            {draft.connections.length > 0 ? (
              draft.connections.map((connection, index) => (
                <div
                  key={`${draft.id}-connection-${index}`}
                  className="rounded-2xl border border-white/8 bg-black/20 px-3 py-3"
                >
                  <div className="grid gap-3 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
                    <Field label="目标场景">
                      <SelectField
                        value={connection.targetLandmarkId}
                        onChange={(value) =>
                          updateConnection(index, (current) => ({
                            ...current,
                            targetLandmarkId: value,
                          }))
                        }
                        options={availableTargetLandmarks.map((entry) => ({
                          value: entry.id,
                          label: entry.name,
                        }))}
                      />
                    </Field>
                    <Field label="相对位置">
                      <SelectField
                        value={connection.relativePosition}
                        onChange={(value) =>
                          updateConnection(index, (current) => ({
                            ...current,
                            relativePosition:
                              value as CustomWorldSceneConnection['relativePosition'],
                          }))
                        }
                        options={CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.map(
                          (option) => ({
                            value: option.value,
                            label: option.label,
                          }),
                        )}
                      />
                    </Field>
                  </div>
                  <Field label="连接说明">
                    <TextArea
                      value={connection.summary}
                      onChange={(value) =>
                        updateConnection(index, (current) => ({
                          ...current,
                          summary: value,
                        }))
                      }
                      rows={2}
                      placeholder="例如：沿山脊向北翻过去，可到达断桥。"
                    />
                  </Field>
                  <div className="flex justify-end">
                    <ActionButton
                      label="删除连接"
                      onClick={() =>
                        setDraft((current) => ({
                          ...current,
                          connections: current.connections.filter(
                            (_item, connectionIndex) =>
                              connectionIndex !== index,
                          ),
                        }))
                      }
                    />
                  </div>
                </div>
              ))
            ) : (
              <div className="rounded-2xl border border-dashed border-white/12 bg-black/20 px-4 py-4 text-sm text-zinc-500">
                当前还没有手动配置连接。若直接保存，系统会自动补一条可遍历的主路连接。
              </div>
            )}
          </div>
          {draft.connections.length > 0 ? (
            <div className="mt-3 rounded-2xl border border-white/8 bg-black/20 px-4 py-3 text-xs leading-6 text-zinc-400">
              当前连接预览：
              {draft.connections
                .map((connection) => {
                  const targetLandmark = availableTargetLandmarks.find(
                    (entry) => entry.id === connection.targetLandmarkId,
                  );
                  return `${getCustomWorldSceneRelativePositionLabel(connection.relativePosition)} -> ${targetLandmark?.name ?? '未匹配场景'}`;
                })
                .join('；')}
            </div>
          ) : null}
        </div>
        <SaveBar
          onClose={onClose}
          onSave={saveLandmarkProfile}
        />
        {isPresetPickerOpen ? (
          <ScenePresetPickerModal
            selectedSrc={draft.imageSrc}
            presetImages={presetImages}
            onSelect={(value) =>
              setDraft((current) => ({ ...current, imageSrc: value }))
            }
            onClose={() => setIsPresetPickerOpen(false)}
          />
        ) : null}
        {isAiGenerateOpen ? (
          <SceneImageGenerationModal
            profile={profile}
            landmark={draft}
            onApply={(result) => {
              setDraft((current) => ({
                ...current,
                imageSrc: result.imageSrc,
              }));
            }}
            onClose={() => setIsAiGenerateOpen(false)}
          />
        ) : null}
        {npcEditorState ? (
          <StoryNpcEditor
            npc={npcEditorState.npc}
            mode={npcEditorState.mode}
            onSave={(nextNpc) => {
              setDraftStoryNpcs((current) =>
                npcEditorState.mode === 'create'
                  ? [...current, nextNpc]
                  : current.map((item) =>
                      item.id === nextNpc.id ? nextNpc : item,
                    ),
              );
              setDraft((current) => ({
                ...current,
                sceneNpcIds: current.sceneNpcIds.includes(nextNpc.id)
                  ? current.sceneNpcIds
                  : [...current.sceneNpcIds, nextNpc.id],
              }));
              setNpcEditorState(null);
            }}
            onClose={() => setNpcEditorState(null)}
          />
        ) : null}
      </div>
    </ModalShell>
  );
}

function createPlayableNpc(
  profile: CustomWorldProfile,
): CustomWorldPlayableNpc {
  const seed = Date.now() + profile.playableNpcs.length;
  const template =
    ROLE_TEMPLATE_CHARACTERS[
      profile.playableNpcs.length % Math.max(1, ROLE_TEMPLATE_CHARACTERS.length)
    ] ?? ROLE_TEMPLATE_CHARACTERS[0];

  return {
    id: createEntryId(
      'playable-npc',
      `角色-${profile.playableNpcs.length + 1}`,
      seed,
    ),
    name: `自定义角色${profile.playableNpcs.length + 1}`,
    title: '自定义身份',
    role: '世界中的行动者',
    description: '',
    backstory: '',
    personality: '',
    motivation: '',
    combatStyle: '',
    initialAffinity: 18,
    relationshipHooks: ['首次接触', '合作空间'],
    tags: ['自定义'],
    backstoryReveal: {
      publicSummary: '',
      chapters: [
        {
          id: 'surface',
          title: '表层来意',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'scar',
          title: '旧事裂痕',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'hidden',
          title: '隐藏执念',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'final',
          title: '最终底牌',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
      ],
    },
    skills: [
      { id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
      { id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
      { id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
    ],
    initialItems: [
      {
        id: 'item-1',
        name: '随身武具',
        category: '武器',
        quantity: 1,
        rarity: 'rare',
        description: '',
        tags: ['自定义'],
      },
      {
        id: 'item-2',
        name: '补给包',
        category: '消耗品',
        quantity: 2,
        rarity: 'uncommon',
        description: '',
        tags: ['自定义'],
      },
      {
        id: 'item-3',
        name: '私人物件',
        category: '专属物品',
        quantity: 1,
        rarity: 'rare',
        description: '',
        tags: ['自定义'],
      },
    ],
    templateCharacterId: template?.id,
  };
}

function createStoryNpc(
  profile: Pick<CustomWorldProfile, 'storyNpcs'>,
): CustomWorldNpc {
  const seed = Date.now() + profile.storyNpcs.length;
  const npc = {
    id: createEntryId(
      'story-npc',
      `场景角色-${profile.storyNpcs.length + 1}`,
      seed,
    ),
    name: `自定义场景角色${profile.storyNpcs.length + 1}`,
    title: '自定义头衔',
    role: '自定义身份',
    description: '',
    backstory: '',
    personality: '',
    motivation: '',
    combatStyle: '',
    initialAffinity: 6,
    relationshipHooks: ['合作', '互动'],
    tags: ['自定义'],
    backstoryReveal: {
      publicSummary: '',
      chapters: [
        {
          id: 'surface',
          title: '表层来意',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'scar',
          title: '旧事裂痕',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'hidden',
          title: '隐藏执念',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
        {
          id: 'final',
          title: '最终底牌',
          affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
          teaser: '',
          content: '',
          contextSnippet: '',
        },
      ],
    },
    skills: [
      { id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
      { id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
      { id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
    ],
    initialItems: [
      {
        id: 'item-1',
        name: '随身武具',
        category: '武器',
        quantity: 1,
        rarity: 'rare',
        description: '',
        tags: ['自定义'],
      },
      {
        id: 'item-2',
        name: '补给包',
        category: '消耗品',
        quantity: 2,
        rarity: 'uncommon',
        description: '',
        tags: ['自定义'],
      },
      {
        id: 'item-3',
        name: '私人物件',
        category: '专属物品',
        quantity: 1,
        rarity: 'rare',
        description: '',
        tags: ['自定义'],
      },
    ],
  } satisfies CustomWorldNpc;

  return npc;
}

function createLandmark(profile: CustomWorldProfile): CustomWorldLandmark {
  const seed = Date.now() + profile.landmarks.length;
  const previousLandmark = profile.landmarks[profile.landmarks.length - 1];
  return {
    id: createEntryId(
      'landmark',
      `scene-${profile.landmarks.length + 1}`,
      seed,
    ),
    name: `自定义场景${profile.landmarks.length + 1}`,
    description: '',
    dangerLevel: '中',
    imageSrc: undefined,
    sceneNpcIds: profile.storyNpcs.slice(0, 3).map((npc) => npc.id),
    connections: previousLandmark
      ? [
          {
            targetLandmarkId: previousLandmark.id,
            relativePosition: 'back',
            summary: `暂时接回${previousLandmark.name}这条旧路`,
          },
        ]
      : [],
  };
}

export function CustomWorldEntityEditorModal({
  profile,
  target,
  onClose,
  onProfileChange,
}: CustomWorldEntityEditorModalProps) {
  if (!target) return null;

  if (target.kind === 'world') {
    return (
      <WorldEditor
        profile={profile}
        onSave={onProfileChange}
        onClose={onClose}
      />
    );
  }

  if (target.kind === 'playable') {
    if (target.mode === 'create') {
      return (
        <PlayableNpcEditor
          npc={createPlayableNpc(profile)}
          mode="create"
          onSave={(nextNpc) =>
            onProfileChange({
              ...profile,
              playableNpcs: [...profile.playableNpcs, nextNpc],
            })
          }
          onClose={onClose}
        />
      );
    }

    const npc = profile.playableNpcs.find((item) => item.id === target.id);
    return npc ? (
      <PlayableNpcEditor
        npc={npc}
        mode="edit"
        onSave={(nextNpc) =>
          onProfileChange({
            ...profile,
            playableNpcs: profile.playableNpcs.map((item) =>
              item.id === nextNpc.id ? nextNpc : item,
            ),
          })
        }
        onClose={onClose}
      />
    ) : null;
  }

  if (target.kind === 'story') {
    if (target.mode === 'create') {
      return (
        <StoryNpcEditor
          npc={createStoryNpc(profile)}
          mode="create"
          onSave={(nextNpc) =>
            onProfileChange({
              ...profile,
              storyNpcs: [...profile.storyNpcs, nextNpc],
            })
          }
          onClose={onClose}
        />
      );
    }

    const npc = profile.storyNpcs.find((item) => item.id === target.id);
    return npc ? (
      <StoryNpcEditor
        npc={npc}
        mode="edit"
        onSave={(nextNpc) =>
          onProfileChange({
            ...profile,
            storyNpcs: profile.storyNpcs.map((item) =>
              item.id === nextNpc.id ? nextNpc : item,
            ),
          })
        }
        onClose={onClose}
      />
    ) : null;
  }

  if (target.mode === 'create') {
    return (
      <LandmarkEditor
        profile={profile}
        landmark={createLandmark(profile)}
        mode="create"
        onSaveProfile={onProfileChange}
        onClose={onClose}
      />
    );
  }

  const landmark = profile.landmarks.find((entry) => entry.id === target.id);
  return landmark ? (
    <LandmarkEditor
      profile={profile}
      landmark={landmark}
      mode="edit"
      onSaveProfile={onProfileChange}
      onClose={onClose}
    />
  ) : null;
}
