import { Move, RotateCcw, Save } from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';

import {
  buildMedievalNpcVisual,
  getMedievalAtlasAsset,
  getMedievalAtlasOptions,
  getMedievalHeadOptions,
  getMedievalPoseOptions,
  getNpcVisualOverrideById,
  getRaceSpriteCounts,
  MEDIEVAL_BODY_COLOR_LABELS,
  MEDIEVAL_BODY_COLORS,
  MEDIEVAL_FACIAL_HAIR_COLOR_LABELS,
  MEDIEVAL_FACIAL_HAIR_STYLE_LABELS,
  MEDIEVAL_HAIR_COLOR_LABELS,
  MEDIEVAL_HAIR_STYLE_LABELS,
  MEDIEVAL_RACE_LABELS,
  MedievalNpcVisualOverride,
  MedievalNpcVisualSpec,
  MedievalRace,
} from '../data/medievalNpcVisuals';
import npcVisualOverridesJson from '../data/npcVisualOverrides.json';
import { getScenePresetsByWorld } from '../data/scenePresets';
import { EditorNotice } from '../editor/shared/EditorNotice';
import { fetchJson } from '../editor/shared/jsonClient';
import { Encounter, WorldType } from '../types';
import { MedievalNpcAnimator } from './MedievalNpcAnimator';
import {
  buildOverrideFromEditorState,
  type EditableNpcVisualState,
  type EditorNpcOption,
  type GearSourceType,
  getDefaultFileForType,
  getDefaultFrameForSelection,
  isNpcLayoutConfig,
  isRecord,
  sanitizeFrameSelection,
} from './npcVisualEditorModel';
import {
  NPC_LAYOUT_CONFIG_API_PATH,
  NPC_VISUAL_OVERRIDES_API_PATH,
  persistNpcLayoutConfig,
  persistNpcVisualOverrides,
} from './npcVisualEditorPersistence';
import {
  cloneNpcLayoutConfig,
  DEFAULT_NPC_LAYOUT_CONFIG,
  type NpcLayoutConfig,
  type NpcLayoutPart,
} from './npcVisualShared';

const INITIAL_OVERRIDES = npcVisualOverridesJson as Record<
  string,
  MedievalNpcVisualOverride
>;
const INITIAL_LAYOUT = cloneNpcLayoutConfig(DEFAULT_NPC_LAYOUT_CONFIG);

const PART_LABELS: Record<NpcLayoutPart, string> = {
  body: '身体',
  head: '头部',
  facialHair: '面部毛发',
  hair: '发型',
  headgear: '头饰',
  hand: '手部',
  mainHand: '主手',
  offHand: '副手',
};

function flattenNpcOptions() {
  const sceneGroups = [
    ...getScenePresetsByWorld(WorldType.WUXIA),
    ...getScenePresetsByWorld(WorldType.XIANXIA),
  ];
  const npcMap = new Map<string, EditorNpcOption>();

  for (const scene of sceneGroups) {
    for (const npc of scene.npcs ?? []) {
      const existing = npcMap.get(npc.id);
      if (existing) {
        existing.sceneNames.push(scene.name);
        continue;
      }

      npcMap.set(npc.id, {
        encounter: {
          id: npc.id,
          kind: 'npc',
          npcName: npc.name,
          npcDescription: npc.description,
          npcAvatar: npc.avatar,
          context: npc.role,
          characterId: npc.characterId,
        },
        sceneNames: [scene.name],
      });
    }
  }

  return [...npcMap.values()].sort((a, b) =>
    a.encounter.npcName.localeCompare(b.encounter.npcName, 'zh-Hans-CN'),
  );
}

function inferSourceType(src: string | undefined): GearSourceType {
  if (!src) return 'none';
  if (src.includes('/wardrobe/cloth/')) return 'cloth';
  if (src.includes('/wardrobe/leather/')) return 'leather';
  if (src.includes('/wardrobe/metal/')) return 'metal';
  if (src.includes('/weapons/melee weapons/')) return 'melee';
  if (src.includes('/weapons/magic weapons/')) return 'magic';
  if (src.includes('/weapons/ranged weapons/')) return 'ranged';
  return 'none';
}

function parseStateFromSpec(
  spec: MedievalNpcVisualSpec,
): EditableNpcVisualState {
  const race = spec.race;
  const bodyColor = spec.bodySrc.match(/body_(.+)\.png$/u)?.[1] ?? 'black';
  const headIndex = Number(spec.headSrc.match(/_(\d+)\.png$/u)?.[1] ?? '1');
  const hairColorIndex = Number(
    spec.hairSrc.match(/_(\d+)\.png$/u)?.[1] ?? '1',
  );
  const facialHairColorIndex = Number(
    spec.facialHairSrc?.match(/_(\d+)\.png$/u)?.[1] ?? '1',
  );

  const headgearType = inferSourceType(spec.headgear?.src);
  const mainHandType = inferSourceType(spec.mainHand?.src);
  const offHandType = inferSourceType(spec.offHand?.src);

  const headgearFile =
    spec.headgear?.src.split('/').pop() ?? getDefaultFileForType(headgearType);
  const mainHandFile =
    spec.mainHand?.src.split('/').pop() ?? getDefaultFileForType(mainHandType);
  const offHandFile = spec.offHand?.src.split('/').pop() ?? 'shield.png';

  return {
    race,
    bodyColor,
    headIndex,
    hairColorIndex,
    hairStyleFrame: spec.hairFrame,
    facialHairEnabled: !!spec.facialHairSrc,
    facialHairColorIndex,
    facialHairStyleFrame: spec.facialHairFrame ?? 0,
    headgearType,
    headgearFile,
    headgearFrame: sanitizeFrameSelection(
      headgearType,
      headgearFile,
      spec.headgear?.frameIndex ?? 0,
      'headgear',
    ),
    mainHandType,
    mainHandFile,
    mainHandFrame: sanitizeFrameSelection(
      mainHandType,
      mainHandFile,
      spec.mainHand?.frameIndex ?? 0,
      'mainHand',
    ),
    offHandType,
    offHandFile,
    offHandFrame: sanitizeFrameSelection(
      offHandType,
      offHandFile,
      spec.offHand?.frameIndex ?? 0,
      'offHand',
    ),
  };
}

function buildPreviewSpec(
  encounter: Encounter,
  editorState: EditableNpcVisualState,
) {
  return {
    ...buildMedievalNpcVisual(encounter),
    ...buildOverrideFromEditorState(editorState),
  } as MedievalNpcVisualSpec;
}

function SelectField({
  label,
  value,
  onChange,
  options,
  disabled = false,
}: {
  label: string;
  value: string | number;
  onChange: (next: string) => void;
  options: Array<{ label: string; value: string | number }>;
  disabled?: boolean;
}) {
  return (
    <label className="block">
      <div className="mb-1 text-xs font-medium text-zinc-300">{label}</div>
      <select
        value={String(value)}
        onChange={(event) => onChange(event.target.value)}
        disabled={disabled}
        className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
      >
        {options.map((option) => (
          <option key={`${label}-${option.value}`} value={String(option.value)}>
            {option.label}
          </option>
        ))}
      </select>
    </label>
  );
}

export function NpcVisualEditor({
  embedded = false,
  selectedNpcId: controlledNpcId,
  hideNpcSelector = false,
}: {
  embedded?: boolean;
  selectedNpcId?: string;
  hideNpcSelector?: boolean;
}) {
  const npcOptions = useMemo(() => flattenNpcOptions(), []);
  const [internalSelectedNpcId, setInternalSelectedNpcId] = useState(
    controlledNpcId ?? npcOptions[0]?.encounter.id ?? '',
  );
  const [overrideMap, setOverrideMap] =
    useState<Record<string, MedievalNpcVisualOverride>>(INITIAL_OVERRIDES);
  const [layoutDraft, setLayoutDraft] =
    useState<NpcLayoutConfig>(INITIAL_LAYOUT);
  const [layoutHistory, setLayoutHistory] = useState<NpcLayoutConfig[]>([]);
  const [editorState, setEditorState] = useState<EditableNpcVisualState | null>(
    null,
  );
  const [loadMessage, setLoadMessage] = useState<string | null>(null);
  const [saveMessage, setSaveMessage] = useState<string | null>(null);
  const [isSaving, setIsSaving] = useState(false);
  const [isSavingLayout, setIsSavingLayout] = useState(false);
  const [selectedPart, setSelectedPart] = useState<NpcLayoutPart>('head');
  const [dragState, setDragState] = useState<{
    part: NpcLayoutPart;
    startX: number;
    startY: number;
    originX: number;
    originY: number;
  } | null>(null);

  const effectiveNpcId = controlledNpcId ?? internalSelectedNpcId;
  const selectedNpc =
    npcOptions.find((option) => option.encounter.id === effectiveNpcId) ??
    npcOptions[0];

  useEffect(() => {
    let disposed = false;

    const loadRemoteConfig = async () => {
      const [overridesResult, layoutResult] = await Promise.allSettled([
        fetchJson<unknown>(
          NPC_VISUAL_OVERRIDES_API_PATH,
          '加载角色形象覆盖配置失败',
        ),
        fetchJson<unknown>(
          NPC_LAYOUT_CONFIG_API_PATH,
          '加载角色布局配置失败',
        ),
      ]);

      if (disposed) {
        return;
      }

      const messages: string[] = [];

      if (overridesResult.status === 'fulfilled') {
        if (isRecord(overridesResult.value)) {
          setOverrideMap(
            overridesResult.value as Record<string, MedievalNpcVisualOverride>,
          );
        } else {
          messages.push(
          '角色形象覆盖配置响应无效，使用内置默认值。',
        );
        }
      } else {
        messages.push(
            overridesResult.reason instanceof Error
              ? `${overridesResult.reason.message}，使用内置角色形象覆盖配置。`
              : '加载角色形象覆盖配置失败，使用内置默认值。',
          );
      }

      if (layoutResult.status === 'fulfilled') {
        if (isNpcLayoutConfig(layoutResult.value)) {
          setLayoutDraft(cloneNpcLayoutConfig(layoutResult.value));
          setLayoutHistory([]);
        } else {
          messages.push(
          '角色布局配置响应无效，使用内置默认值。',
        );
        }
      } else {
        messages.push(
            layoutResult.reason instanceof Error
              ? `${layoutResult.reason.message}，使用内置角色布局配置。`
              : '加载角色布局配置失败，使用内置默认值。',
          );
      }

      setLoadMessage(messages.length > 0 ? messages.join(' ') : null);
    };

    void loadRemoteConfig();

    return () => {
      disposed = true;
    };
  }, []);

  useEffect(() => {
    if (controlledNpcId) {
      setInternalSelectedNpcId(controlledNpcId);
    }
  }, [controlledNpcId]);

  useEffect(() => {
    if (!selectedNpc) return;
    const encounterId = selectedNpc.encounter.id ?? '';
    const override = encounterId
      ? (overrideMap[encounterId] ?? getNpcVisualOverrideById(encounterId) ?? undefined)
      : undefined;
    const spec = override
      ? ({
          ...buildMedievalNpcVisual(selectedNpc.encounter),
          ...override,
        } as MedievalNpcVisualSpec)
      : buildMedievalNpcVisual(selectedNpc.encounter);
    setEditorState(parseStateFromSpec(spec));
  }, [overrideMap, selectedNpc]);

  useEffect(() => {
    if (!editorState) return;

    const nextHeadgearFrame = sanitizeFrameSelection(
      editorState.headgearType,
      editorState.headgearFile,
      editorState.headgearFrame,
      'headgear',
    );
    const nextMainHandFrame = sanitizeFrameSelection(
      editorState.mainHandType,
      editorState.mainHandFile,
      editorState.mainHandFrame,
      'mainHand',
    );
    const nextOffHandFrame = sanitizeFrameSelection(
      editorState.offHandType,
      editorState.offHandFile,
      editorState.offHandFrame,
      'offHand',
    );

    if (
      nextHeadgearFrame === editorState.headgearFrame &&
      nextMainHandFrame === editorState.mainHandFrame &&
      nextOffHandFrame === editorState.offHandFrame
    ) {
      return;
    }

    setEditorState((prev) =>
      prev
        ? {
            ...prev,
            headgearFrame: nextHeadgearFrame,
            mainHandFrame: nextMainHandFrame,
            offHandFrame: nextOffHandFrame,
          }
        : prev,
    );
  }, [
    editorState,
    editorState?.headgearType,
    editorState?.headgearFile,
    editorState?.headgearFrame,
    editorState?.mainHandType,
    editorState?.mainHandFile,
    editorState?.mainHandFrame,
    editorState?.offHandType,
    editorState?.offHandFile,
    editorState?.offHandFrame,
  ]);

  useEffect(() => {
    if (!dragState) return;

    const handlePointerMove = (event: PointerEvent) => {
      const dx = Math.round((event.clientX - dragState.startX) / 2.4);
      const dy = Math.round((event.clientY - dragState.startY) / 2.4);
      setLayoutDraft((prev) => ({
        ...prev,
        [dragState.part]: {
          x: dragState.originX + dx,
          y: dragState.originY + dy,
        },
      }));
    };

    const handlePointerUp = () => {
      setDragState(null);
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', handlePointerUp, { once: true });

    return () => {
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', handlePointerUp);
    };
  }, [dragState]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const target = event.target as HTMLElement | null;
      const tagName = target?.tagName ?? '';
      if (
        tagName === 'INPUT' ||
        tagName === 'TEXTAREA' ||
        tagName === 'SELECT'
      ) {
        return;
      }

      let dx = 0;
      let dy = 0;
      if (event.key === 'ArrowLeft') dx = -1;
      if (event.key === 'ArrowRight') dx = 1;
      if (event.key === 'ArrowUp') dy = -1;
      if (event.key === 'ArrowDown') dy = 1;
      if (dx === 0 && dy === 0) return;

      event.preventDefault();
      const step = event.shiftKey ? 5 : 1;
      setLayoutHistory((prev) => [...prev, cloneNpcLayoutConfig(layoutDraft)]);
      setLayoutDraft((prev) => ({
        ...prev,
        [selectedPart]: {
          x: prev[selectedPart].x + dx * step,
          y: prev[selectedPart].y + dy * step,
        },
      }));
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [layoutDraft, selectedPart]);

  if (!selectedNpc || !editorState) {
    return (
      <div className="rounded-2xl border border-white/10 bg-black/20 p-6 text-sm text-zinc-300">
        请先选择一个场景角色进行编辑
      </div>
    );
  }

  const raceCounts = getRaceSpriteCounts(editorState.race);
  const previewSpec = buildPreviewSpec(selectedNpc.encounter, editorState);

  const headgearAssets =
    editorState.headgearType === 'none'
      ? []
      : getMedievalAtlasOptions(editorState.headgearType);
  const mainHandAssets =
    editorState.mainHandType === 'none'
      ? []
      : getMedievalAtlasOptions(editorState.mainHandType);
  const offHandAssets =
    editorState.offHandType === 'none'
      ? []
      : getMedievalAtlasOptions(editorState.offHandType);
  const headgearPoseOptions =
    editorState.headgearType === 'none'
      ? []
      : getMedievalPoseOptions(
          editorState.headgearType,
          editorState.headgearFile,
          'headgear',
        );
  const mainHandPoseOptions =
    editorState.mainHandType === 'none'
      ? []
      : getMedievalPoseOptions(
          editorState.mainHandType,
          editorState.mainHandFile,
          'mainHand',
        );
  const offHandPoseOptions =
    editorState.offHandType === 'none'
      ? []
      : getMedievalPoseOptions(
          editorState.offHandType,
          editorState.offHandFile,
          'offHand',
        );

  const saveOverrides = async () => {
    setIsSaving(true);
    setSaveMessage(null);

    try {
      const { nextOverrideMap, saveMessage: nextSaveMessage } =
        await persistNpcVisualOverrides({
          overrideMap,
          npcId: selectedNpc.encounter.id!,
          editorState,
        });
      setOverrideMap(nextOverrideMap);
      setSaveMessage(nextSaveMessage);
    } catch (error) {
      setSaveMessage(error instanceof Error ? error.message : '保存失败');
    } finally {
      setIsSaving(false);
    }
  };

  const saveLayout = async () => {
    setIsSavingLayout(true);
    setSaveMessage(null);

    try {
      const { saveMessage: nextSaveMessage } = await persistNpcLayoutConfig({
        layoutDraft,
      });
      setSaveMessage(nextSaveMessage);
      setLayoutHistory([]);
    } catch (error) {
      setSaveMessage(error instanceof Error ? error.message : '保存失败');
    } finally {
      setIsSavingLayout(false);
    }
  };

  const rollbackLayout = () => {
    setLayoutHistory((prev) => {
      if (prev.length === 0) return prev;
      const next = [...prev];
      const previousLayout = next.pop()!;
      setLayoutDraft(previousLayout);
      setSaveMessage('布局已重置');
      return next;
    });
  };

  const setField = <K extends keyof EditableNpcVisualState>(
    key: K,
    value: EditableNpcVisualState[K],
  ) => {
    setEditorState((prev) => (prev ? { ...prev, [key]: value } : prev));
  };

  const updateGearType = (
    key: 'headgear' | 'mainHand' | 'offHand',
    nextType: GearSourceType,
  ) => {
    if (key === 'headgear') {
      const nextFile = getDefaultFileForType(nextType);
      setEditorState((prev) =>
        prev
          ? {
              ...prev,
              headgearType: nextType,
              headgearFile: nextFile,
              headgearFrame: getDefaultFrameForSelection(
                nextType,
                nextFile,
                'headgear',
              ),
            }
          : prev,
      );
      return;
    }

    if (key === 'mainHand') {
      const nextFile = getDefaultFileForType(nextType);
      setEditorState((prev) =>
        prev
          ? {
              ...prev,
              mainHandType: nextType,
              mainHandFile: nextFile,
              mainHandFrame: getDefaultFrameForSelection(
                nextType,
                nextFile,
                'mainHand',
              ),
            }
          : prev,
      );
      return;
    }

    const nextFile = nextType === 'none' ? '' : 'shield.png';
    setEditorState((prev) =>
      prev
        ? {
            ...prev,
            offHandType: nextType,
            offHandFile: nextFile,
            offHandFrame: getDefaultFrameForSelection(
              nextType,
              nextFile,
              'offHand',
            ),
          }
        : prev,
    );
  };

  const updateGearFile = (
    key: 'headgear' | 'mainHand' | 'offHand',
    nextFile: string,
  ) => {
    if (key === 'headgear') {
      setEditorState((prev) =>
        prev
          ? {
              ...prev,
              headgearFile: nextFile,
              headgearFrame: getDefaultFrameForSelection(
                prev.headgearType,
                nextFile,
                'headgear',
              ),
            }
          : prev,
      );
      return;
    }

    if (key === 'mainHand') {
      setEditorState((prev) =>
        prev
          ? {
              ...prev,
              mainHandFile: nextFile,
              mainHandFrame: getDefaultFrameForSelection(
                prev.mainHandType,
                nextFile,
                'mainHand',
              ),
            }
          : prev,
      );
      return;
    }

    setEditorState((prev) =>
      prev
        ? {
            ...prev,
            offHandFile: nextFile,
            offHandFrame: getDefaultFrameForSelection(
              prev.offHandType,
              nextFile,
              'offHand',
            ),
          }
        : prev,
    );
  };

  const handlePartPointerDown = (
    part: NpcLayoutPart,
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    event.preventDefault();
    event.stopPropagation();
    setSelectedPart(part);
    setLayoutHistory((prev) => [...prev, cloneNpcLayoutConfig(layoutDraft)]);
    setDragState({
      part,
      startX: event.clientX,
      startY: event.clientY,
      originX: layoutDraft[part].x,
      originY: layoutDraft[part].y,
    });
  };

  return (
    <div
      className={
        embedded ? 'text-zinc-100' : 'min-h-screen bg-[#0b0d11] text-zinc-100'
      }
    >
      <div className={embedded ? '' : 'mx-auto max-w-7xl px-6 py-8'}>
        {!embedded && (
          <div className="mb-8">
            <div className="text-xs uppercase tracking-[0.3em] text-emerald-400/70">
              场景角色形象编辑器
            </div>
            <h1 className="mt-2 text-3xl font-semibold text-white">
              场景角色形象编辑器
            </h1>
            <p className="mt-2 max-w-4xl text-sm leading-relaxed text-zinc-400">
              选择并编辑场景角色的外观，支持调整种族、肤色、发型、装备等多种视觉元素。
            </p>
          </div>
        )}

        <div className="grid gap-6 lg:grid-cols-[360px_1fr]">
          <div className="rounded-2xl border border-white/10 bg-white/5 p-5">
            {!hideNpcSelector && (
              <div className="mb-4">
              <SelectField
                label="当前场景角色"
                value={selectedNpc.encounter.id ?? ''}
                onChange={(value) => setInternalSelectedNpcId(value)}
                options={npcOptions.map((option) => ({
                  value: option.encounter.id ?? '',
                  label: `${option.encounter.npcName} (${option.sceneNames.join(' / ')})`,
                }))}
              />
            </div>
            )}

            <div className="mb-4 rounded-xl border border-white/10 bg-black/20 px-4 py-3">
              <div className="text-sm font-semibold text-white">
                {selectedNpc.encounter.npcName}
              </div>
              <div className="mt-1 text-xs text-zinc-400">
                {selectedNpc.encounter.context}
              </div>
              <div className="mt-2 text-xs leading-relaxed text-zinc-400">
                {selectedNpc.encounter.npcDescription}
              </div>
            </div>

            <div className="grid gap-3">
              <SelectField
                label="种族"
                value={editorState.race}
                onChange={(value) => setField('race', value as MedievalRace)}
                options={( 
                  ['human', 'elf', 'orc', 'goblin'] as MedievalRace[]
                ).map((value) => ({
                  value,
                  label: MEDIEVAL_RACE_LABELS[value],
                }))}
              />
              <SelectField
                label="肤色"
                value={editorState.bodyColor}
                onChange={(value) => setField('bodyColor', value)}
                options={MEDIEVAL_BODY_COLORS.map((value) => ({
                  value,
                  label: MEDIEVAL_BODY_COLOR_LABELS[value] ?? value,
                }))}
              />
              <SelectField
                label="头部"
                value={editorState.headIndex}
                onChange={(value) => setField('headIndex', Number(value))}
                options={getMedievalHeadOptions(editorState.race)}
              />
              <SelectField
                label="发型"
                value={editorState.hairStyleFrame}
                onChange={(value) => setField('hairStyleFrame', Number(value))}
                options={MEDIEVAL_HAIR_STYLE_LABELS.map((label, index) => ({
                  value: index,
                  label,
                }))}
              />
              <SelectField
                label="发色"
                value={editorState.hairColorIndex}
                onChange={(value) => setField('hairColorIndex', Number(value))}
                options={Array.from(
                  { length: raceCounts.hair },
                  (_, index) => ({
                    value: index + 1,
                    label: MEDIEVAL_HAIR_COLOR_LABELS[index] ?? '自定义发色',
                  }),
                )}
              />
              <SelectField
                label="面部毛发"
                value={
                  editorState.facialHairEnabled
                    ? editorState.facialHairStyleFrame + 1
                    : 0
                }
                onChange={(value) => {
                  const next = Number(value);
                  setField('facialHairEnabled', next > 0);
                  setField('facialHairStyleFrame', next > 0 ? next - 1 : 0);
                }}
                options={[
                  { value: 0, label: '隐藏面部毛发' },
                  ...MEDIEVAL_FACIAL_HAIR_STYLE_LABELS.map((label, index) => ({
                    value: index + 1,
                    label,
                  })),
                ]}
              />
              {editorState.facialHairEnabled && (
                <SelectField
                  label="面部毛发颜色"
                  value={editorState.facialHairColorIndex}
                  onChange={(value) =>
                    setField('facialHairColorIndex', Number(value))
                  }
                  options={Array.from(
                    { length: raceCounts.facialHair },
                    (_, index) => ({
                      value: index + 1,
                      label:
                        MEDIEVAL_FACIAL_HAIR_COLOR_LABELS[index] ?? '面部毛发颜色',
                    }),
                  )}
                />
              )}

              <SelectField
                label="头饰类型"
                value={editorState.headgearType}
                onChange={(value) =>
                  updateGearType('headgear', value as GearSourceType)
                }
                options={[
                  { value: 'none', label: '无头饰' },
                  { value: 'cloth', label: '布制' },
                  { value: 'leather', label: '皮革' },
                  { value: 'metal', label: '金属' },
                ]}
              />
              {editorState.headgearType !== 'none' && (
                <>
                  <SelectField
                  label="头饰文件"
                  value={editorState.headgearFile}
                  onChange={(value) => updateGearFile('headgear', value)}
                  options={headgearAssets.map((asset) => ({
                    value: asset.file,
                    label: asset.label,
                  }))}
                />
                <SelectField
                  label="头饰帧"
                  value={editorState.headgearFrame}
                  onChange={(value) =>
                    setField('headgearFrame', Number(value))
                  }
                  options={headgearPoseOptions.map((option) => ({
                    value: option.value,
                    label: option.label,
                  }))}
                />
                </>
              )}

              <SelectField
                label="主手类型"
                value={editorState.mainHandType}
                onChange={(value) =>
                  updateGearType('mainHand', value as GearSourceType)
                }
                options={[
                  { value: 'none', label: '无主手' },
                  { value: 'melee', label: '近战' },
                  { value: 'magic', label: '魔法' },
                  { value: 'ranged', label: '远程' },
                ]}
              />
              {editorState.mainHandType !== 'none' && (
                <>
                  <SelectField
                  label="主手文件"
                  value={editorState.mainHandFile}
                  onChange={(value) => updateGearFile('mainHand', value)}
                  options={mainHandAssets.map((asset) => ({
                    value: asset.file,
                    label: asset.label,
                  }))}
                />
                <SelectField
                  label="主手帧"
                  value={editorState.mainHandFrame}
                  onChange={(value) =>
                    setField('mainHandFrame', Number(value))
                  }
                  options={mainHandPoseOptions.map((option) => ({
                    value: option.value,
                    label: option.label,
                  }))}
                />
                </>
              )}

              <SelectField
                label="副手类型"
                value={editorState.offHandType}
                onChange={(value) =>
                  updateGearType('offHand', value as GearSourceType)
                }
                options={[
                  { value: 'none', label: '无副手' },
                  { value: 'melee', label: '近战' },
                ]}
              />
              {editorState.offHandType !== 'none' && (
                <>
                  <SelectField
                  label="副手文件"
                  value={editorState.offHandFile}
                  onChange={(value) => updateGearFile('offHand', value)}
                  options={offHandAssets.map((asset) => ({
                    value: asset.file,
                    label: asset.label,
                  }))}
                />
                <SelectField
                  label="副手帧"
                  value={editorState.offHandFrame}
                  onChange={(value) =>
                    setField('offHandFrame', Number(value))
                  }
                  options={offHandPoseOptions.map((option) => ({
                    value: option.value,
                    label: option.label,
                  }))}
                />
                </>
              )}
            </div>

            <div className="mt-5 flex flex-wrap items-center gap-3">
              <button
                onClick={saveOverrides}
                disabled={isSaving}
                className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-black transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-60"
              >
                <Save className="h-4 w-4" />
                <span>{isSaving ? '正在保存覆盖...' : '保存覆盖'}</span>
              </button>
              <button
                onClick={saveLayout}
                disabled={isSavingLayout}
                className="inline-flex items-center gap-2 rounded-lg border border-emerald-400/30 bg-emerald-500/10 px-4 py-2 text-sm font-medium text-emerald-100 transition hover:bg-emerald-500/20 disabled:cursor-not-allowed disabled:opacity-60"
              >
                <Move className="h-4 w-4" />
                <span>{isSavingLayout ? '正在保存布局...' : '保存布局'}</span>
              </button>
              <button
                onClick={rollbackLayout}
                disabled={layoutHistory.length === 0}
                className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-black/20 px-4 py-2 text-sm font-medium text-zinc-200 transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
              >
                <RotateCcw className="h-4 w-4" />
                <span>重置布局</span>
              </button>
              {loadMessage && (
                <EditorNotice message={loadMessage} tone="warning" />
              )}
              {saveMessage && <EditorNotice message={saveMessage} />}
            </div>
          </div>
          <div className="rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.16),transparent_40%),linear-gradient(180deg,#10131a,#090b0f)] p-6">
            <div className="mb-4 flex items-center justify-between gap-4">
              <div>
                <div className="text-sm font-semibold text-white">布局预览</div>
                <div className="mt-1 text-xs text-zinc-400">
                  拖动标记微调场景角色的预览布局。<br />
                  移动时按住换挡键可按 0.5 步长微调。</div>
              </div>
              <div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-xs text-zinc-300">
                当前选中：
                <span className="ml-1 text-emerald-300">
                  {PART_LABELS[selectedPart]}
                </span>
              </div>
            </div>

            <div className="mb-4 flex flex-wrap gap-2">
              {(Object.keys(PART_LABELS) as NpcLayoutPart[]).map((part) => (
                <button
                  key={part}
                  onClick={() => setSelectedPart(part)}
                  className={`rounded-full border px-3 py-1 text-xs transition ${selectedPart === part ? 'border-emerald-400/40 bg-emerald-500/15 text-emerald-100' : 'border-white/10 bg-black/20 text-zinc-400 hover:text-white'}`}
                >
                  {PART_LABELS[part]}
                </button>
              ))}
            </div>

            <div className="flex min-h-[520px] items-center justify-center rounded-2xl border border-white/10 bg-[linear-gradient(180deg,#21242a,#111317)] p-6">
              <div className="relative flex h-[420px] w-[320px] items-end justify-center overflow-hidden rounded-xl bg-[linear-gradient(180deg,#2a2f39,#16191f)] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.04)]">
                <div className="absolute inset-0 opacity-10 [background-image:linear-gradient(rgba(255,255,255,0.16)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.16)_1px,transparent_1px)] [background-size:20px_20px]" />
                <MedievalNpcAnimator
                  visualSpec={previewSpec}
                  layoutConfig={layoutDraft}
                  selectedPart={selectedPart}
                  onPartPointerDown={handlePartPointerDown}
                  className="mb-10 drop-shadow-[0_18px_24px_rgba(0,0,0,0.45)]"
                />
              </div>
            </div>

            <div className="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
              {(Object.keys(PART_LABELS) as NpcLayoutPart[]).map((part) => (
                <button
                  key={part}
                  onClick={() => setSelectedPart(part)}
                  className={`rounded-xl border px-3 py-2 text-left text-xs transition ${selectedPart === part ? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100' : 'border-white/10 bg-black/20 text-zinc-400 hover:border-white/20 hover:text-white'}`}
                >
                  <div>{PART_LABELS[part]}</div>
                  <div className="mt-1 font-mono">
                    x {layoutDraft[part].x} / y {layoutDraft[part].y}
                  </div>
                </button>
              ))}
            </div>

            <div className="mt-4 rounded-xl border border-white/10 bg-black/20 px-4 py-3 text-xs leading-relaxed text-zinc-400">
              Current loadout:<span className="ml-2 text-zinc-200">
                {editorState.headgearType !== 'none'
                  ? (getMedievalAtlasAsset(
                      editorState.headgearType,
                      editorState.headgearFile,
                    )?.label ?? '未知头饰')
                  : '无头饰'}
              </span>
              <span className="mx-2 text-zinc-600">/</span>
              <span className="text-zinc-200">
                {editorState.mainHandType !== 'none'
                  ? (getMedievalAtlasAsset(
                      editorState.mainHandType,
                      editorState.mainHandFile,
                    )?.label ?? '未知主手武器')
                  : '无主手武器'}
              </span>
              <span className="mx-2 text-zinc-600">/</span>
              <span className="text-zinc-200">
                {editorState.offHandType !== 'none'
                  ? (getMedievalAtlasAsset(
                      editorState.offHandType,
                      editorState.offHandFile,
                    )?.label ?? '未知副手武器')
                  : '无副手武器'}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
