import type { ReactNode } from 'react';

import { resolveCustomWorldNpcMonsterPreset } from '../data/customWorldNpcMonsters';
import {
  buildBodyPath,
  buildMedievalNpcVisual,
  buildMedievalNpcVisualOverrideFromCustomWorldVisual,
  buildRaceAssetPath,
  getMedievalAtlasAsset,
  getMedievalAtlasOptions,
  getMedievalHeadOptions,
  getMedievalPoseOptions,
  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,
  type MedievalAtlasSourceType,
  type MedievalAtlasUsage,
  type MedievalRace,
  sanitizeCustomWorldNpcVisual,
} from '../data/medievalNpcVisuals';
import {
  type CustomWorldNpc,
  type CustomWorldNpcVisual,
  type CustomWorldProfile,
} from '../types';
import { buildDefaultCustomWorldNpcVisual } from './customWorldNpcVisualDefaults';
import { HostileNpcAnimator } from './HostileNpcAnimator';
import { MedievalNpcAnimator } from './MedievalNpcAnimator';
import { ResolvedAssetImage } from './ResolvedAssetImage';

type EditableNpcSource = Pick<
  CustomWorldNpc,
  'id' | 'name' | 'role' | 'description' | 'imageSrc'
>
  & Partial<
    Pick<
      CustomWorldNpc,
      | 'title'
      | 'backstory'
      | 'personality'
      | 'motivation'
      | 'combatStyle'
      | 'initialAffinity'
      | 'relationshipHooks'
      | 'tags'
    >
  >;
type GearSlot = 'headgear' | 'mainHand' | 'offHand';

function buildCustomWorldNpcEncounter(npc: EditableNpcSource) {
  return {
    id: npc.id,
    kind: 'npc' as const,
    npcName: npc.name,
    npcDescription: npc.description,
    npcAvatar: npc.name.slice(0, 1) || '角',
    context: npc.role,
  };
}

function buildPreviewSpec(npc: EditableNpcSource, visual?: CustomWorldNpcVisual) {
  const encounter = buildCustomWorldNpcEncounter(npc);
  const baseSpec = buildMedievalNpcVisual(encounter);

  if (!visual) {
    return baseSpec;
  }

  return {
    ...baseSpec,
    ...buildMedievalNpcVisualOverrideFromCustomWorldVisual(visual),
  };
}

function getGearUsage(slot: GearSlot): MedievalAtlasUsage {
  if (slot === 'headgear') return 'headgear';
  if (slot === 'mainHand') return 'mainHand';
  return 'offHand';
}

function getDefaultFileForType(type: MedievalAtlasSourceType, usage: MedievalAtlasUsage) {
  const assets = getMedievalAtlasOptions(type);
  if (usage === 'offHand' && type === 'melee') {
    return assets.find(asset => asset.file === 'shield.png')?.file ?? assets[0]?.file ?? '';
  }
  return assets[0]?.file ?? '';
}

function getDefaultFrameForSelection(type: MedievalAtlasSourceType, file: string, usage: MedievalAtlasUsage) {
  return getMedievalPoseOptions(type, file, usage)[0]?.value ?? 0;
}

function buildDefaultGear(type: MedievalAtlasSourceType, usage: MedievalAtlasUsage) {
  const file = getDefaultFileForType(type, usage);
  if (!file) return null;

  return {
    type,
    file,
    frameIndex: getDefaultFrameForSelection(type, file, usage),
  };
}

function getGearSummary(visual: CustomWorldNpcVisual) {
  return [
    visual.headgear ? getMedievalAtlasAsset(visual.headgear.type, visual.headgear.file)?.label ?? '头饰' : '无头饰',
    visual.mainHand ? getMedievalAtlasAsset(visual.mainHand.type, visual.mainHand.file)?.label ?? '主手' : '无主手',
    visual.offHand ? getMedievalAtlasAsset(visual.offHand.type, visual.offHand.file)?.label ?? '副手' : '无副手',
  ].join(' / ');
}

function PreviewFrame({
  children,
  className = '',
}: {
  children: ReactNode;
  className?: string;
}) {
  return (
    <div className={`relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.16),transparent_52%),linear-gradient(180deg,rgba(19,24,39,0.94),rgba(8,10,17,0.92))] ${className}`}>
      <div className="absolute inset-0 opacity-10 [background-image:linear-gradient(rgba(255,255,255,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.18)_1px,transparent_1px)] [background-size:10px_10px]" />
      <div className="relative z-[1] flex items-center justify-center">
        {children}
      </div>
    </div>
  );
}

function SpriteFramePreview({
  src,
  frameIndex = 0,
  tileSize = 32,
  scale = 1,
}: {
  src: string;
  frameIndex?: number;
  tileSize?: number;
  scale?: number;
}) {
  return (
    <div
      style={{
        width: `${tileSize}px`,
        height: `${tileSize}px`,
        backgroundImage: `url("${encodeURI(src)}")`,
        backgroundRepeat: 'no-repeat',
        backgroundPosition: `-${frameIndex * tileSize}px 0px`,
        imageRendering: 'pixelated',
        transform: `scale(${scale})`,
        transformOrigin: 'center',
      }}
    />
  );
}

function AtlasFramePreview({
  type,
  file,
  frameIndex,
}: {
  type: MedievalAtlasSourceType;
  file: string;
  frameIndex: number;
}) {
  const asset = getMedievalAtlasAsset(type, file);

  if (!asset) {
    return <div className="text-[10px] font-semibold text-zinc-500">无</div>;
  }

  const col = frameIndex % asset.columns;
  const row = Math.floor(frameIndex / asset.columns);

  return (
    <div
      style={{
        width: `${asset.tileWidth}px`,
        height: `${asset.tileHeight}px`,
        backgroundImage: `url("${encodeURI(asset.src)}")`,
        backgroundRepeat: 'no-repeat',
        backgroundPosition: `-${col * asset.tileWidth}px -${row * asset.tileHeight}px`,
        backgroundSize: 'auto',
        imageRendering: 'pixelated',
        transform: asset.tileWidth > 32 || asset.tileHeight > 32 ? 'scale(0.75)' : undefined,
        transformOrigin: 'center',
      }}
    />
  );
}

function EmptyPreview({ label }: { label: string }) {
  return (
    <div className="text-[10px] font-semibold tracking-[0.08em] text-zinc-500">
      {label}
    </div>
  );
}

function PortraitOptionPreview({
  npc,
  visual,
}: {
  npc: EditableNpcSource;
  visual: CustomWorldNpcVisual;
}) {
  return (
    <PreviewFrame className="h-14 w-14">
      <MedievalNpcAnimator
        visualSpec={buildPreviewSpec(npc, visual)}
        scale={1.1}
        className="origin-center"
      />
    </PreviewFrame>
  );
}

function OptionCard({
  label,
  selected,
  onClick,
  preview,
}: {
  key?: string;
  label: string;
  selected: boolean;
  onClick: () => void;
  preview: ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={`rounded-2xl border px-3 py-3 text-left transition-colors ${
        selected
          ? 'border-sky-300/45 bg-sky-500/12 text-white'
          : 'border-white/10 bg-black/20 text-zinc-300 hover:border-white/20 hover:text-white'
      }`}
    >
      <div className="flex items-center gap-3">
        {preview}
        <div className="min-w-0">
          <div className="text-sm font-semibold leading-5">{label}</div>
        </div>
      </div>
    </button>
  );
}

function OptionSection({
  title,
  subtitle,
  children,
}: {
  title: string;
  subtitle?: string;
  children: ReactNode;
}) {
  return (
    <section className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-4">
      <div>
        <div className="text-[11px] font-bold tracking-[0.18em] text-zinc-200">{title}</div>
        {subtitle ? <div className="mt-1 text-xs leading-5 text-zinc-500">{subtitle}</div> : null}
      </div>
      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
        {children}
      </div>
    </section>
  );
}

function ActionButton({
  label,
  onClick,
  tone = 'default',
}: {
  label: string;
  onClick: () => void;
  tone?: 'default' | 'sky';
}) {
  return (
    <button
      type="button"
      onPointerDown={(event) => {
        event.stopPropagation();
      }}
      onMouseDown={(event) => {
        event.stopPropagation();
      }}
      onClick={onClick}
      className={`rounded-full border px-4 py-2 text-sm font-semibold transition-colors ${
        tone === 'sky'
          ? 'border-sky-300/22 bg-sky-500/12 text-sky-50 hover:border-sky-200/40 hover:text-white'
          : 'border-white/12 bg-black/20 text-zinc-200 hover:border-white/22 hover:text-white'
      }`}
    >
      {label}
    </button>
  );
}

export function CustomWorldNpcPortrait({
  npc,
  profile,
  visual,
  className = '',
  contentClassName = 'min-h-[7rem] p-3',
  scale = 2.05,
  preferImageSrc = false,
}: {
  npc: EditableNpcSource;
  profile?: CustomWorldProfile | null;
  visual?: CustomWorldNpcVisual;
  className?: string;
  contentClassName?: string;
  scale?: number;
  preferImageSrc?: boolean;
}) {
  const previewSpec = buildPreviewSpec(npc, visual ? sanitizeCustomWorldNpcVisual(visual) : undefined);
  const monsterPreset = visual
    ? null
    : resolveCustomWorldNpcMonsterPreset(npc, undefined, profile ?? null);
  const preferredImageSrc =
    preferImageSrc && npc.imageSrc?.trim() ? npc.imageSrc.trim() : '';

  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.96),rgba(8,10,17,0.92))] ${className}`}>
      <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:16px_16px]" />
      <div
        className={`relative flex h-full items-center justify-center ${contentClassName}`}
      >
        {preferredImageSrc ? (
          <ResolvedAssetImage
            src={preferredImageSrc}
            alt={npc.name}
            className="h-full w-full object-contain drop-shadow-[0_12px_18px_rgba(0,0,0,0.38)]"
          />
        ) : monsterPreset ? (
          <div
            className="origin-center drop-shadow-[0_12px_18px_rgba(0,0,0,0.38)]"
            style={{
              transform: `scale(${Math.max(1, scale * 0.72)})`,
              transformOrigin: 'center',
            }}
          >
            <HostileNpcAnimator hostileNpc={monsterPreset} />
          </div>
        ) : (
          <MedievalNpcAnimator
            visualSpec={previewSpec}
            scale={scale}
            className="origin-center drop-shadow-[0_12px_18px_rgba(0,0,0,0.38)]"
          />
        )}
      </div>
    </div>
  );
}

export function CustomWorldNpcVisualEditor({
  npc,
  profile,
  value,
  onChange,
  onAiGenerate,
}: {
  npc: EditableNpcSource;
  profile?: CustomWorldProfile | null;
  value?: CustomWorldNpcVisual;
  onChange: (value: CustomWorldNpcVisual) => void;
  onAiGenerate: () => void;
}) {
  const effectiveVisual = sanitizeCustomWorldNpcVisual(value ?? buildDefaultCustomWorldNpcVisual(npc));
  const spriteCounts = getRaceSpriteCounts(effectiveVisual.race);
  const headOptions = getMedievalHeadOptions(effectiveVisual.race);
  const headgearAssets = effectiveVisual.headgear ? getMedievalAtlasOptions(effectiveVisual.headgear.type) : [];
  const mainHandAssets = effectiveVisual.mainHand ? getMedievalAtlasOptions(effectiveVisual.mainHand.type) : [];
  const offHandAssets = effectiveVisual.offHand ? getMedievalAtlasOptions(effectiveVisual.offHand.type) : [];
  const headgearPoseOptions = effectiveVisual.headgear ? getMedievalPoseOptions(effectiveVisual.headgear.type, effectiveVisual.headgear.file, 'headgear') : [];
  const mainHandPoseOptions = effectiveVisual.mainHand ? getMedievalPoseOptions(effectiveVisual.mainHand.type, effectiveVisual.mainHand.file, 'mainHand') : [];
  const offHandPoseOptions = effectiveVisual.offHand ? getMedievalPoseOptions(effectiveVisual.offHand.type, effectiveVisual.offHand.file, 'offHand') : [];

  const updateVisual = (nextVisual: CustomWorldNpcVisual) => {
    onChange(sanitizeCustomWorldNpcVisual(nextVisual));
  };

  const buildPatchedVisual = (patch: Partial<CustomWorldNpcVisual>) => (
    sanitizeCustomWorldNpcVisual({
      ...effectiveVisual,
      ...patch,
    })
  );

  const updateGearType = (slot: GearSlot, nextType: MedievalAtlasSourceType | 'none') => {
    if (nextType === 'none') {
      updateVisual({
        ...effectiveVisual,
        [slot]: null,
      });
      return;
    }

    updateVisual({
      ...effectiveVisual,
      [slot]: buildDefaultGear(nextType, getGearUsage(slot)),
    });
  };

  const updateGearFile = (slot: GearSlot, nextFile: string) => {
    const currentGear = effectiveVisual[slot];
    if (!currentGear) return;

    updateVisual({
      ...effectiveVisual,
      [slot]: {
        ...currentGear,
        file: nextFile,
        frameIndex: getDefaultFrameForSelection(currentGear.type, nextFile, getGearUsage(slot)),
      },
    });
  };

  const updateGearFrame = (slot: GearSlot, nextFrameIndex: number) => {
    const currentGear = effectiveVisual[slot];
    if (!currentGear) return;

    updateVisual({
      ...effectiveVisual,
      [slot]: {
        ...currentGear,
        frameIndex: nextFrameIndex,
      },
    });
  };

  return (
    <div className="grid gap-5 lg:grid-cols-[12rem_minmax(0,1fr)]">
      <div className="self-start lg:sticky lg:top-0">
        <div className="mx-auto w-full max-w-[9.5rem] space-y-3">
          <CustomWorldNpcPortrait
            npc={npc}
            profile={profile}
            visual={effectiveVisual}
            className="aspect-square"
            scale={2.05}
          />
          <div className="rounded-2xl border border-white/10 bg-black/25 px-3 py-3 text-center text-xs leading-5 text-zinc-300">
            {getGearSummary(effectiveVisual)}
          </div>
          <div className="flex flex-col gap-2">
            <ActionButton label="恢复默认组合" onClick={() => onChange(buildDefaultCustomWorldNpcVisual(npc))} />
            <ActionButton label="智能生成" onClick={onAiGenerate} tone="sky" />
          </div>
        </div>
      </div>

      <div className="space-y-5">
        <OptionSection title="种族" subtitle="切换基础种族，并预览对应的整体轮廓。">
          {(Object.entries(MEDIEVAL_RACE_LABELS) as Array<[MedievalRace, string]>).map(([race, label]) => {
            const previewVisual = buildPatchedVisual({ race });
            return (
              <OptionCard
                key={`race-${race}`}
                label={label}
                selected={effectiveVisual.race === race}
                onClick={() => updateVisual(previewVisual)}
                preview={<PortraitOptionPreview npc={npc} visual={previewVisual} />}
              />
            );
          })}
        </OptionSection>

        <OptionSection title="服装颜色" subtitle="预览身体部位素材。">
          {MEDIEVAL_BODY_COLORS.map(color => (
            <OptionCard
              key={`body-${color}`}
              label={MEDIEVAL_BODY_COLOR_LABELS[color] ?? color}
              selected={effectiveVisual.bodyColor === color}
              onClick={() => updateVisual(buildPatchedVisual({ bodyColor: color }))}
              preview={(
                <PreviewFrame>
                  <SpriteFramePreview src={buildBodyPath(color)} frameIndex={0} />
                </PreviewFrame>
              )}
            />
          ))}
        </OptionSection>

        <OptionSection title="肤色" subtitle="预览头部部位素材。">
          {headOptions.map(option => (
            <OptionCard
              key={`head-${option.value}`}
              label={option.label}
              selected={effectiveVisual.headIndex === option.value}
              onClick={() => updateVisual(buildPatchedVisual({ headIndex: option.value }))}
              preview={(
                <PreviewFrame>
                  <SpriteFramePreview src={buildRaceAssetPath(effectiveVisual.race, 'head', option.value)} frameIndex={0} />
                </PreviewFrame>
              )}
            />
          ))}
        </OptionSection>

        <OptionSection title="发型" subtitle="文字和发型部位预览同步显示。">
          {MEDIEVAL_HAIR_STYLE_LABELS.map((label, index) => (
            <OptionCard
              key={`hair-style-${index}`}
              label={label}
              selected={effectiveVisual.hairStyleFrame === index}
              onClick={() => updateVisual(buildPatchedVisual({ hairStyleFrame: index }))}
              preview={(
                <PreviewFrame>
                  <SpriteFramePreview
                    src={buildRaceAssetPath(effectiveVisual.race, 'hair', effectiveVisual.hairColorIndex)}
                    frameIndex={index}
                  />
                </PreviewFrame>
              )}
            />
          ))}
        </OptionSection>

        <OptionSection title="发色" subtitle="基于当前发型预览不同发色。">
          {Array.from({ length: spriteCounts.hair }, (_, index) => {
            const value = index + 1;
            return (
              <OptionCard
                key={`hair-color-${value}`}
                label={MEDIEVAL_HAIR_COLOR_LABELS[index] ?? `发色 ${value}`}
                selected={effectiveVisual.hairColorIndex === value}
                onClick={() => updateVisual(buildPatchedVisual({ hairColorIndex: value }))}
                preview={(
                  <PreviewFrame>
                    <SpriteFramePreview
                      src={buildRaceAssetPath(effectiveVisual.race, 'hair', value)}
                      frameIndex={effectiveVisual.hairStyleFrame}
                    />
                  </PreviewFrame>
                )}
              />
            );
          })}
        </OptionSection>

        <OptionSection title="胡须样式" subtitle="可直接切换为不显示，也可预览每种胡须部位。">
          <OptionCard
            label="不显示"
            selected={!effectiveVisual.facialHairEnabled}
            onClick={() => updateVisual(buildPatchedVisual({ facialHairEnabled: false, facialHairStyleFrame: 0 }))}
            preview={(
              <PreviewFrame>
                <EmptyPreview label="无" />
              </PreviewFrame>
            )}
          />
          {MEDIEVAL_FACIAL_HAIR_STYLE_LABELS.map((label, index) => (
            <OptionCard
              key={`facial-style-${index}`}
              label={label}
              selected={effectiveVisual.facialHairEnabled && effectiveVisual.facialHairStyleFrame === index}
              onClick={() => updateVisual(buildPatchedVisual({ facialHairEnabled: true, facialHairStyleFrame: index }))}
              preview={(
                <PreviewFrame>
                  <SpriteFramePreview
                    src={buildRaceAssetPath(effectiveVisual.race, 'facialHair', effectiveVisual.facialHairColorIndex)}
                    frameIndex={index}
                  />
                </PreviewFrame>
              )}
            />
          ))}
        </OptionSection>

        {effectiveVisual.facialHairEnabled ? (
          <OptionSection title="胡须颜色" subtitle="预览当前胡须样式下的颜色变化。">
            {Array.from({ length: spriteCounts.facialHair }, (_, index) => {
              const value = index + 1;
              return (
                <OptionCard
                  key={`facial-color-${value}`}
                  label={MEDIEVAL_FACIAL_HAIR_COLOR_LABELS[index] ?? `胡须颜色 ${value}`}
                  selected={effectiveVisual.facialHairColorIndex === value}
                  onClick={() => updateVisual(buildPatchedVisual({ facialHairColorIndex: value }))}
                  preview={(
                    <PreviewFrame>
                      <SpriteFramePreview
                        src={buildRaceAssetPath(effectiveVisual.race, 'facialHair', value)}
                        frameIndex={effectiveVisual.facialHairStyleFrame}
                      />
                    </PreviewFrame>
                  )}
                />
              );
            })}
          </OptionSection>
        ) : null}

        <OptionSection title="头饰类型" subtitle="先选装备类型，再挑具体素材和姿态。">
          <OptionCard
            label="不装备"
            selected={!effectiveVisual.headgear}
            onClick={() => updateGearType('headgear', 'none')}
            preview={(
              <PreviewFrame>
                <EmptyPreview label="无" />
              </PreviewFrame>
            )}
          />
          {([
            ['cloth', '布帽'],
            ['leather', '皮具'],
            ['metal', '金属头盔'],
          ] as Array<[MedievalAtlasSourceType, string]>).map(([type, label]) => {
            const gear = buildDefaultGear(type, 'headgear');
            return (
              <OptionCard
                key={`headgear-type-${type}`}
                label={label}
                selected={effectiveVisual.headgear?.type === type}
                onClick={() => updateGearType('headgear', type)}
                preview={(
                  <PreviewFrame>
                    {gear ? <AtlasFramePreview type={gear.type} file={gear.file} frameIndex={gear.frameIndex} /> : <EmptyPreview label="无" />}
                  </PreviewFrame>
                )}
              />
            );
          })}
        </OptionSection>

        {effectiveVisual.headgear ? (
          <>
            <OptionSection title="头饰素材" subtitle="素材卡片同时展示名称和头饰部位预览。">
              {headgearAssets.map(asset => (
                <OptionCard
                  key={`headgear-file-${asset.file}`}
                  label={asset.label}
                  selected={effectiveVisual.headgear?.file === asset.file}
                  onClick={() => updateGearFile('headgear', asset.file)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.headgear!.type}
                        file={asset.file}
                        frameIndex={getDefaultFrameForSelection(effectiveVisual.headgear!.type, asset.file, 'headgear')}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>

            <OptionSection title="头饰姿态" subtitle="预览当前头饰素材在不同姿态下的部位变化。">
              {headgearPoseOptions.map(option => (
                <OptionCard
                  key={`headgear-pose-${option.value}`}
                  label={option.label}
                  selected={effectiveVisual.headgear?.frameIndex === option.value}
                  onClick={() => updateGearFrame('headgear', option.value)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.headgear!.type}
                        file={effectiveVisual.headgear!.file}
                        frameIndex={option.value}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>
          </>
        ) : null}

        <OptionSection title="主手类型" subtitle="预览不同主手武器类型。">
          <OptionCard
            label="不装备"
            selected={!effectiveVisual.mainHand}
            onClick={() => updateGearType('mainHand', 'none')}
            preview={(
              <PreviewFrame>
                <EmptyPreview label="无" />
              </PreviewFrame>
            )}
          />
          {([
            ['melee', '近战武器'],
            ['magic', '法器'],
            ['ranged', '远程武器'],
          ] as Array<[MedievalAtlasSourceType, string]>).map(([type, label]) => {
            const gear = buildDefaultGear(type, 'mainHand');
            return (
              <OptionCard
                key={`main-hand-type-${type}`}
                label={label}
                selected={effectiveVisual.mainHand?.type === type}
                onClick={() => updateGearType('mainHand', type)}
                preview={(
                  <PreviewFrame>
                    {gear ? <AtlasFramePreview type={gear.type} file={gear.file} frameIndex={gear.frameIndex} /> : <EmptyPreview label="无" />}
                  </PreviewFrame>
                )}
              />
            );
          })}
        </OptionSection>

        {effectiveVisual.mainHand ? (
          <>
            <OptionSection title="主手素材" subtitle="用当前武器姿态预览每个素材。">
              {mainHandAssets.map(asset => (
                <OptionCard
                  key={`main-hand-file-${asset.file}`}
                  label={asset.label}
                  selected={effectiveVisual.mainHand?.file === asset.file}
                  onClick={() => updateGearFile('mainHand', asset.file)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.mainHand!.type}
                        file={asset.file}
                        frameIndex={getDefaultFrameForSelection(effectiveVisual.mainHand!.type, asset.file, 'mainHand')}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>

            <OptionSection title="主手姿态" subtitle="预览当前主手素材在不同姿态下的部位。">
              {mainHandPoseOptions.map(option => (
                <OptionCard
                  key={`main-hand-pose-${option.value}`}
                  label={option.label}
                  selected={effectiveVisual.mainHand?.frameIndex === option.value}
                  onClick={() => updateGearFrame('mainHand', option.value)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.mainHand!.type}
                        file={effectiveVisual.mainHand!.file}
                        frameIndex={option.value}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>
          </>
        ) : null}

        <OptionSection title="副手类型" subtitle="可选择不装备，或为副手配置盾牌 / 近战部件。">
          <OptionCard
            label="不装备"
            selected={!effectiveVisual.offHand}
            onClick={() => updateGearType('offHand', 'none')}
            preview={(
              <PreviewFrame>
                <EmptyPreview label="无" />
              </PreviewFrame>
            )}
          />
          {(() => {
            const gear = buildDefaultGear('melee', 'offHand');
            return (
              <OptionCard
                label="盾牌 / 近战副手"
                selected={effectiveVisual.offHand?.type === 'melee'}
                onClick={() => updateGearType('offHand', 'melee')}
                preview={(
                  <PreviewFrame>
                    {gear ? <AtlasFramePreview type={gear.type} file={gear.file} frameIndex={gear.frameIndex} /> : <EmptyPreview label="无" />}
                  </PreviewFrame>
                )}
              />
            );
          })()}
        </OptionSection>

        {effectiveVisual.offHand ? (
          <>
            <OptionSection title="副手素材" subtitle="素材卡片展示副手部件预览。">
              {offHandAssets.map(asset => (
                <OptionCard
                  key={`off-hand-file-${asset.file}`}
                  label={asset.label}
                  selected={effectiveVisual.offHand?.file === asset.file}
                  onClick={() => updateGearFile('offHand', asset.file)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.offHand!.type}
                        file={asset.file}
                        frameIndex={getDefaultFrameForSelection(effectiveVisual.offHand!.type, asset.file, 'offHand')}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>

            <OptionSection title="副手姿态" subtitle="预览当前副手素材在不同姿态下的部位。">
              {offHandPoseOptions.map(option => (
                <OptionCard
                  key={`off-hand-pose-${option.value}`}
                  label={option.label}
                  selected={effectiveVisual.offHand?.frameIndex === option.value}
                  onClick={() => updateGearFrame('offHand', option.value)}
                  preview={(
                    <PreviewFrame>
                      <AtlasFramePreview
                        type={effectiveVisual.offHand!.type}
                        file={effectiveVisual.offHand!.file}
                        frameIndex={option.value}
                      />
                    </PreviewFrame>
                  )}
                />
              ))}
            </OptionSection>
          </>
        ) : null}
      </div>
    </div>
  );
}
