import {
  ArrowLeft,
  CheckCircle2,
  Eye,
  ImageIcon,
  ImagePlus,
  Loader2,
  Play,
  Plus,
  Send,
  Settings,
  Trash2,
  Wand2,
} from 'lucide-react';
import {
  type ChangeEvent,
  type ReactNode,
  useEffect,
  useMemo,
  useState,
} from 'react';
import { createPortal } from 'react-dom';

import type { CreationAudioAsset } from '../../../packages/shared/src/contracts/creationAudio';
import type { Match3DResultDraft } from '../../../packages/shared/src/contracts/match3dAgent';
import type {
  Match3DGeneratedBackgroundAsset,
  Match3DGeneratedItemAsset,
  Match3DWorkProfile,
  PutMatch3DWorkRequest,
} from '../../../packages/shared/src/contracts/match3dWorks';
import { isGeneratedLegacyPath } from '../../services/assetReadUrlService';
import {
  generateMatch3DCoverImage,
  generateMatch3DItemAssets,
  generateMatch3DWorkTags,
  publishMatch3DWork,
  updateMatch3DGeneratedItemAssets,
  updateMatch3DWork,
} from '../../services/match3d-works';
import {
  getMatch3DGeneratedImageViewSources,
  mergeMatch3DGeneratedItemAssetsForRuntime,
  normalizeMatch3DGeneratedItemAssetsForRuntime,
  resolveMatch3DGeneratedImageAssetSource,
  resolveMatch3DGeneratedModelAssetSource,
} from '../../services/match3dGeneratedModelCache';
import {
  buildMatch3DItemSpritesheetViewRegions,
  loadMatch3DSpritesheetAssetRegions,
  type Match3DDecodedSpritesheetRegion,
} from '../../services/match3dSpritesheetParser';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAssetPickerGrid } from '../common/PlatformAssetPickerCard';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformMediaTileGrid } from '../common/PlatformMediaTileGrid';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformPillSwitch } from '../common/PlatformPillSwitch';
import { PlatformProgressBar } from '../common/PlatformProgressBar';
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
import { PlatformStatGrid } from '../common/PlatformStatGrid';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTagEditor } from '../common/PlatformTagEditor';
import { PlatformTextField } from '../common/PlatformTextField';
import { PlatformUploadPreviewCard } from '../common/PlatformUploadPreviewCard';
import { UnifiedConfirmDialog } from '../common/UnifiedConfirmDialog';
import {
  MATCH3D_RUNTIME_BOARD_BASE_CLASS,
  MATCH3D_RUNTIME_BOARD_FALLBACK_CLASS,
  MATCH3D_RUNTIME_BOARD_WIDTH,
  MATCH3D_RUNTIME_BOARD_WITH_CONTAINER_CLASS,
  MATCH3D_RUNTIME_CONTAINER_IMAGE_CLASS,
  MATCH3D_RUNTIME_CONTAINER_PLACEHOLDER_CLASS,
  MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS,
  MATCH3D_RUNTIME_GLASS_TRAY_CLASS,
  MATCH3D_RUNTIME_GLASS_TRAY_SLOT_CLASS,
  MATCH3D_RUNTIME_HEADER_CARD_CLASS,
  MATCH3D_RUNTIME_LEVEL_BADGE_CLASS,
  MATCH3D_RUNTIME_STAGE_CLASS,
  MATCH3D_RUNTIME_TIMER_CLASS,
} from '../match3d-runtime/match3dRuntimeUiStyles';
import { ResolvedAssetImage } from '../ResolvedAssetImage';

type Match3DResultViewProps = {
  profile: Match3DWorkProfile;
  draft?: Match3DResultDraft | null;
  isBusy?: boolean;
  error?: string | null;
  onBack: () => void;
  onSaved?: (profile: Match3DWorkProfile) => void;
  onPublished?: (profile: Match3DWorkProfile) => void;
  onStartTestRun: (
    profile: Match3DWorkProfile,
    options?: { itemTypeCountOverride?: number },
  ) => void;
};

type Match3DAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
type Match3DResultTab = 'work' | 'config' | 'assets';
type Match3DAssetConfigTab = 'items' | 'ui';
type Match3DAssetTaskStatus =
  | 'idle'
  | 'submitting'
  | 'waiting'
  | 'generating'
  | 'image_ready'
  | 'done'
  | 'failed'
  | 'unknown';

type Match3DBatchItemGenerationState = {
  phase: 'idle' | 'generating' | 'done' | 'failed';
  progress: number | null;
  itemNames: string[];
  message: string | null;
  error: string | null;
};

type Match3DItemSpritesheetPreviewGroup = {
  itemIndex: number;
  itemName: string;
  regions: Match3DDecodedSpritesheetRegion[];
};

type Match3DItemAssetDraft = {
  id: string;
  name: string;
  usage: string;
  prompt: string;
  referenceImageSrc: string;
  imageViews: Match3DGeneratedItemAsset['imageViews'];
  taskUuid: string | null;
  subscriptionKey: string | null;
  status: Match3DAssetTaskStatus;
  progress: number | null;
  downloads: Array<{ name: string; url: string }>;
  soundPrompt: string;
  backgroundMusicTitle: string | null;
  backgroundMusicStyle: string | null;
  backgroundMusicPrompt: string | null;
  backgroundMusic: CreationAudioAsset | null;
  clickSound: CreationAudioAsset | null;
  backgroundAsset: Match3DGeneratedItemAsset['backgroundAsset'] | null;
  error: string | null;
  updatedAt: string | null;
};

type Match3DResultEditState = {
  gameName: string;
  summary: string;
  tagsText: string;
  coverImageSrc: string;
  themeText: string;
  clearCountText: string;
  difficultyText: string;
};

type Match3DCoverSourceAsset = {
  id: string;
  label: string;
  imageSrc: string;
  kind: 'item' | 'ui';
};

type Match3DCoverReferenceDraft = {
  id: string;
  label: string;
  imageSrc: string;
  source: 'asset' | 'upload';
};

const MATCH3D_MIN_TAG_COUNT = 3;
const MATCH3D_MAX_TAG_COUNT = 6;
const MATCH3D_AUTOSAVE_DEBOUNCE_MS = 600;
const MATCH3D_DEFAULT_ASSET_COUNT = 20;
const MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH = 2;
const MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE = 20;
const MATCH3D_COVER_REFERENCE_IMAGE_LIMIT = 6;

const MATCH3D_RESULT_TABS: Array<{ id: Match3DResultTab; label: string }> = [
  { id: 'work', label: '作品信息' },
  { id: 'config', label: '难度配置' },
  { id: 'assets', label: '素材配置' },
];

const MATCH3D_ASSET_CONFIG_TABS: Array<{
  id: Match3DAssetConfigTab;
  label: string;
}> = [
  { id: 'items', label: '物品' },
  { id: 'ui', label: 'UI素材' },
];

// 中文注释：结果页难度配置必须与创作入口页保持同一组派生参数。
const MATCH3D_DIFFICULTY_OPTIONS = [
  { id: 'easy', label: '轻松', clearCount: 8, difficulty: 2, itemTypeCount: 3 },
  {
    id: 'standard',
    label: '标准',
    clearCount: 12,
    difficulty: 4,
    itemTypeCount: 9,
  },
  {
    id: 'advanced',
    label: '进阶',
    clearCount: 16,
    difficulty: 6,
    itemTypeCount: 15,
  },
  {
    id: 'hardcore',
    label: '硬核',
    clearCount: 21,
    difficulty: 8,
    itemTypeCount: 20,
  },
] as const;

type Match3DDifficultyOptionId =
  (typeof MATCH3D_DIFFICULTY_OPTIONS)[number]['id'];
type Match3DDifficultyOption = (typeof MATCH3D_DIFFICULTY_OPTIONS)[number];

const MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC =
  '/match3d-background-references/pot-fused-reference.png';

function resolveMatch3DDifficultyOptionId(
  difficulty: number | null | undefined,
  clearCount: number | null | undefined,
): Match3DDifficultyOptionId {
  const byClearCount = MATCH3D_DIFFICULTY_OPTIONS.find(
    (option) => option.clearCount === clearCount,
  );
  if (byClearCount) {
    return byClearCount.id;
  }

  if (typeof difficulty !== 'number' || !Number.isFinite(difficulty)) {
    return 'standard';
  }

  const normalizedDifficulty = Math.max(
    1,
    Math.min(10, Math.round(difficulty)),
  );
  return MATCH3D_DIFFICULTY_OPTIONS.reduce(
    (nearestOption, option) =>
      Math.abs(option.difficulty - normalizedDifficulty) <
      Math.abs(nearestOption.difficulty - normalizedDifficulty)
        ? option
        : nearestOption,
    MATCH3D_DIFFICULTY_OPTIONS[1],
  ).id;
}

function getMatch3DDifficultyOption(optionId: Match3DDifficultyOptionId) {
  return (
    MATCH3D_DIFFICULTY_OPTIONS.find((option) => option.id === optionId) ??
    MATCH3D_DIFFICULTY_OPTIONS[1]
  );
}

function getMatch3DReadyItemTypeCount(
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return generatedItemAssets.filter(hasMatch3DGeneratedFiveViewImageSource)
    .length;
}

function getMatch3DPlayableItemTypeCount(
  targetItemTypeCount: number,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return Math.max(
    1,
    Math.min(
      targetItemTypeCount,
      getMatch3DReadyItemTypeCount(generatedItemAssets),
    ),
  );
}

function getMatch3DDifficultyOptionFromEditState(
  editState: Match3DResultEditState,
) {
  return getMatch3DDifficultyOption(
    resolveMatch3DDifficultyOptionId(
      normalizeDifficulty(editState.difficultyText),
      normalizePositiveInteger(editState.clearCountText),
    ),
  );
}

function resolveMatch3DBackgroundPreviewSource(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return (
    draft?.backgroundImageSrc?.trim() ||
    draft?.generatedBackgroundAsset?.imageSrc?.trim() ||
    draft?.backgroundImageObjectKey?.trim() ||
    draft?.generatedBackgroundAsset?.imageObjectKey?.trim() ||
    profile.backgroundImageSrc?.trim() ||
    profile.generatedBackgroundAsset?.imageSrc?.trim() ||
    profile.backgroundImageObjectKey?.trim() ||
    profile.generatedBackgroundAsset?.imageObjectKey?.trim() ||
    generatedItemAssets
      .map(
        (asset) =>
          asset.backgroundAsset?.imageSrc?.trim() ||
          asset.backgroundAsset?.imageObjectKey?.trim() ||
          '',
      )
      .find(Boolean) ||
    ''
  );
}

function findMatch3DGeneratedBackgroundAsset(
  generatedItemAssets: readonly Match3DGeneratedItemAsset[] = [],
): Match3DGeneratedBackgroundAsset | null {
  return (
    generatedItemAssets.find((asset) => asset.backgroundAsset)
      ?.backgroundAsset ?? null
  );
}

function promoteMatch3DGeneratedBackgroundAsset(
  profile: Match3DWorkProfile,
): Match3DWorkProfile {
  const fallbackBackground =
    profile.generatedBackgroundAsset ??
    findMatch3DGeneratedBackgroundAsset(profile.generatedItemAssets ?? []);
  if (!fallbackBackground) {
    return profile;
  }

  return {
    ...profile,
    backgroundPrompt:
      profile.backgroundPrompt ?? fallbackBackground.prompt ?? null,
    backgroundImageSrc:
      profile.backgroundImageSrc ??
      fallbackBackground.imageSrc ??
      fallbackBackground.imageObjectKey ??
      null,
    backgroundImageObjectKey:
      profile.backgroundImageObjectKey ??
      fallbackBackground.imageObjectKey ??
      fallbackBackground.imageSrc ??
      null,
    generatedBackgroundAsset:
      profile.generatedBackgroundAsset ?? fallbackBackground,
  };
}

function resolveMatch3DContainerPreviewSource(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return (
    draft?.generatedBackgroundAsset?.containerImageSrc?.trim() ||
    draft?.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
    profile.generatedBackgroundAsset?.containerImageSrc?.trim() ||
    profile.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
    generatedItemAssets
      .map(
        (asset) =>
          asset.backgroundAsset?.containerImageSrc?.trim() ||
          asset.backgroundAsset?.containerImageObjectKey?.trim() ||
          '',
      )
      .find(Boolean) ||
    ''
  );
}

function resolveMatch3DUiSpritesheetPreviewSource(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return (
    draft?.generatedBackgroundAsset?.uiSpritesheetImageSrc?.trim() ||
    draft?.generatedBackgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
    draft?.generatedBackgroundAsset?.containerImageSrc?.trim() ||
    draft?.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
    profile.generatedBackgroundAsset?.uiSpritesheetImageSrc?.trim() ||
    profile.generatedBackgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
    profile.generatedBackgroundAsset?.containerImageSrc?.trim() ||
    profile.generatedBackgroundAsset?.containerImageObjectKey?.trim() ||
    generatedItemAssets
      .map(
        (asset) =>
          asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ||
          asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
          asset.backgroundAsset?.containerImageSrc?.trim() ||
          asset.backgroundAsset?.containerImageObjectKey?.trim() ||
          '',
      )
      .find(Boolean) ||
    ''
  );
}

function resolveMatch3DItemSpritesheetPreviewSource(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  return (
    draft?.generatedBackgroundAsset?.itemSpritesheetImageSrc?.trim() ||
    draft?.generatedBackgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
    profile.generatedBackgroundAsset?.itemSpritesheetImageSrc?.trim() ||
    profile.generatedBackgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
    generatedItemAssets
      .map(
        (asset) =>
          asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ||
          asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
          '',
      )
      .find(Boolean) ||
    ''
  );
}

function normalizeTags(value: string) {
  return [
    ...new Set(
      value
        .split(/[\n,，、]/u)
        .map((entry) => normalizeMatch3DTag(entry))
        .filter(Boolean),
    ),
  ];
}

function buildMatch3DAssetPrompt(
  profile: Match3DWorkProfile,
  assetName: string,
  usage: string,
) {
  return buildMatch3DAssetPromptFromDraft(profile.themeText, assetName, usage);
}

function buildMatch3DAssetPromptFromDraft(
  themeText: string,
  assetName: string,
  usage: string,
) {
  const normalizedTheme = themeText.trim() || '主题';
  return [
    `${normalizedTheme}题材抓大鹅游戏内2D素材：${assetName}`,
    usage,
    '适合移动端游戏直接显示，主体清晰，五视角一致，干净背景。',
  ]
    .filter(Boolean)
    .join('，');
}

function buildFallbackMatch3DClickSoundPrompt(
  profile: Pick<Match3DWorkProfile, 'themeText'>,
  assetName: string,
) {
  const normalizedTheme = profile.themeText.trim() || '抓大鹅';
  const normalizedName = assetName.trim() || '物品';
  return `${normalizedTheme}题材抓大鹅中“${normalizedName}”被点击并消除时的短促反馈音效，清脆、可爱、有轻微弹跳感，适合移动端休闲游戏。`;
}

function normalizeMatch3DTag(value: string) {
  return value
    .trim()
    .replace(/^[\s#"'`，。、“”《》；：！？,.、]+/u, '')
    .replace(/[\s#"'`，。、“”《》；：！？,.、]+$/u, '')
    .slice(0, 12);
}

function normalizeMatch3DTagListText(value: string) {
  return [
    ...new Set(
      value
        .split(/[\n,，、]/u)
        .map((entry) => normalizeMatch3DTag(entry))
        .filter(Boolean),
    ),
  ];
}

function normalizeMatch3DItemName(value: string) {
  return value
    .trim()
    .replace(/^[-*•\d.、)\s]+/u, '')
    .slice(0, 12);
}

function normalizeMatch3DItemNameList(values: readonly string[]) {
  return [...new Set(values.map(normalizeMatch3DItemName).filter(Boolean))];
}

function collectMatch3DRegenerateItemNames(
  values: readonly string[],
  assets: readonly Match3DItemAssetDraft[],
) {
  const existingNames = new Set(
    assets.map((asset) => asset.name.trim()).filter(Boolean),
  );
  return normalizeMatch3DItemNameList(values).filter((name) =>
    existingNames.has(name.trim()),
  );
}

function calculateMatch3DItemAssetsPointsCost(itemCount: number) {
  if (itemCount <= 0) {
    return 0;
  }
  return (
    Math.ceil(itemCount / MATCH3D_ITEM_ASSETS_BILLING_BATCH_SIZE) *
    MATCH3D_ITEM_ASSETS_POINTS_PER_BATCH
  );
}

function hasMatch3DGeneratedModelSource(asset: Match3DGeneratedItemAsset) {
  return Boolean(asset.modelSrc?.trim() || asset.modelObjectKey?.trim());
}

function hasMatch3DGeneratedImageSource(asset: Match3DGeneratedItemAsset) {
  return getMatch3DGeneratedImageViewSources(asset).length > 0;
}

function hasMatch3DGeneratedFiveViewImageSource(
  asset: Match3DGeneratedItemAsset,
) {
  return (
    (asset.imageViews ?? []).filter(
      (view) =>
        Boolean(view.imageSrc?.trim()) || Boolean(view.imageObjectKey?.trim()),
    ).length >= 5
  );
}

function resolveMatch3DGeneratedImageViewSourceFromDraft(
  view: NonNullable<Match3DGeneratedItemAsset['imageViews']>[number],
) {
  return view.imageObjectKey?.trim() || view.imageSrc?.trim() || '';
}

function resolveMatch3DAssetDraftImageViewSources(
  asset: Match3DItemAssetDraft,
) {
  return [
    ...new Set(
      (asset.imageViews ?? [])
        .map(resolveMatch3DGeneratedImageViewSourceFromDraft)
        .filter(Boolean),
    ),
  ];
}

function resolveMatch3DAssetDraftPreviewSources(asset: Match3DItemAssetDraft) {
  const imageViewSources = resolveMatch3DAssetDraftImageViewSources(asset);
  if (imageViewSources.length > 0) {
    return imageViewSources.slice(0, 5);
  }
  const fallbackSource = asset.referenceImageSrc.trim();
  return fallbackSource ? [fallbackSource] : [];
}

function hasPersistableMatch3DGeneratedItemAsset(
  asset: Match3DGeneratedItemAsset,
) {
  return Boolean(
    asset.imageSrc?.trim() ||
      asset.imageObjectKey?.trim() ||
      getMatch3DGeneratedImageViewSources(asset).length > 0 ||
      asset.modelSrc?.trim() ||
      asset.modelObjectKey?.trim() ||
      asset.taskUuid?.trim() ||
      asset.subscriptionKey?.trim() ||
      asset.backgroundAsset?.imageSrc?.trim() ||
      asset.backgroundAsset?.imageObjectKey?.trim() ||
      asset.backgroundAsset?.levelSceneImageSrc?.trim() ||
      asset.backgroundAsset?.levelSceneImageObjectKey?.trim() ||
      asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ||
      asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ||
      asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ||
      asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ||
      asset.backgroundAsset?.containerImageSrc?.trim() ||
      asset.backgroundAsset?.containerImageObjectKey?.trim() ||
      asset.backgroundAsset?.prompt?.trim() ||
      asset.backgroundMusic ||
      asset.clickSound,
  );
}

function getMatch3DGeneratedItemAssetPersistenceSignature(
  asset: Match3DGeneratedItemAsset,
) {
  return [
    asset.itemId.trim(),
    asset.itemName.trim(),
    asset.itemSize?.trim() ?? '',
    asset.imageSrc?.trim() ?? '',
    asset.imageObjectKey?.trim() ?? '',
    ...(asset.imageViews ?? []).flatMap((view) => [
      view.viewId.trim(),
      String(view.viewIndex),
      view.imageSrc?.trim() ?? '',
      view.imageObjectKey?.trim() ?? '',
    ]),
    asset.modelSrc?.trim() ?? '',
    asset.modelObjectKey?.trim() ?? '',
    asset.modelFileName?.trim() ?? '',
    asset.taskUuid?.trim() ?? '',
    asset.subscriptionKey?.trim() ?? '',
    asset.status.trim(),
    asset.soundPrompt?.trim() ?? '',
    asset.backgroundMusicTitle?.trim() ?? '',
    asset.backgroundMusicStyle?.trim() ?? '',
    asset.backgroundMusicPrompt?.trim() ?? '',
    asset.backgroundMusic?.audioSrc?.trim() ??
      asset.backgroundMusic?.assetObjectId?.trim() ??
      asset.backgroundMusic?.taskId?.trim() ??
      '',
    asset.backgroundAsset?.prompt?.trim() ?? '',
    asset.backgroundAsset?.levelScenePrompt?.trim() ?? '',
    asset.backgroundAsset?.levelSceneImageSrc?.trim() ?? '',
    asset.backgroundAsset?.levelSceneImageObjectKey?.trim() ?? '',
    asset.backgroundAsset?.imageSrc?.trim() ?? '',
    asset.backgroundAsset?.imageObjectKey?.trim() ?? '',
    asset.backgroundAsset?.uiSpritesheetPrompt?.trim() ?? '',
    asset.backgroundAsset?.uiSpritesheetImageSrc?.trim() ?? '',
    asset.backgroundAsset?.uiSpritesheetImageObjectKey?.trim() ?? '',
    asset.backgroundAsset?.itemSpritesheetPrompt?.trim() ?? '',
    asset.backgroundAsset?.itemSpritesheetImageSrc?.trim() ?? '',
    asset.backgroundAsset?.itemSpritesheetImageObjectKey?.trim() ?? '',
    asset.backgroundAsset?.containerPrompt?.trim() ?? '',
    asset.backgroundAsset?.containerImageSrc?.trim() ?? '',
    asset.backgroundAsset?.containerImageObjectKey?.trim() ?? '',
    asset.backgroundAsset?.status?.trim() ?? '',
    asset.backgroundAsset?.error?.trim() ?? '',
    asset.clickSound?.audioSrc?.trim() ??
      asset.clickSound?.assetObjectId?.trim() ??
      asset.clickSound?.taskId?.trim() ??
      '',
    asset.error?.trim() ?? '',
  ].join('\u001f');
}

function shouldPersistGeneratedItemAssets(
  currentAssets: readonly Match3DGeneratedItemAsset[],
  savedAssets: readonly Match3DGeneratedItemAsset[] = [],
) {
  if (currentAssets.length <= 0) {
    return false;
  }
  if (currentAssets.length !== savedAssets.length) {
    return true;
  }

  return currentAssets.some((asset, index) => {
    const savedAsset = savedAssets[index];
    return (
      !savedAsset ||
      getMatch3DGeneratedItemAssetPersistenceSignature(asset) !==
        getMatch3DGeneratedItemAssetPersistenceSignature(savedAsset)
    );
  });
}

function mergeMatch3DGeneratedItemAsset(
  base: Match3DGeneratedItemAsset,
  override: Match3DGeneratedItemAsset,
): Match3DGeneratedItemAsset {
  const overrideHasModel = hasMatch3DGeneratedModelSource(override);
  const overrideHasImages = hasMatch3DGeneratedImageSource(override);
  return {
    ...base,
    itemName: override.itemName.trim() || base.itemName,
    itemSize: override.itemSize ?? base.itemSize ?? null,
    imageSrc: override.imageSrc?.trim()
      ? override.imageSrc
      : (base.imageSrc ?? null),
    imageObjectKey: override.imageObjectKey?.trim()
      ? override.imageObjectKey
      : (base.imageObjectKey ?? null),
    imageViews:
      override.imageViews && override.imageViews.length > 0
        ? override.imageViews
        : (base.imageViews ?? []),
    modelSrc: override.modelSrc?.trim()
      ? override.modelSrc
      : (base.modelSrc ?? null),
    modelObjectKey: override.modelObjectKey?.trim()
      ? override.modelObjectKey
      : (base.modelObjectKey ?? null),
    modelFileName: override.modelFileName?.trim()
      ? override.modelFileName
      : (base.modelFileName ?? null),
    taskUuid: override.taskUuid?.trim()
      ? override.taskUuid
      : (base.taskUuid ?? null),
    subscriptionKey: override.subscriptionKey?.trim()
      ? override.subscriptionKey
      : (base.subscriptionKey ?? null),
    backgroundMusic: override.backgroundMusic ?? base.backgroundMusic ?? null,
    clickSound: override.clickSound ?? base.clickSound ?? null,
    backgroundAsset: override.backgroundAsset ?? base.backgroundAsset ?? null,
    soundPrompt: override.soundPrompt?.trim()
      ? override.soundPrompt
      : (base.soundPrompt ?? null),
    backgroundMusicTitle: override.backgroundMusicTitle?.trim()
      ? override.backgroundMusicTitle
      : (base.backgroundMusicTitle ?? null),
    backgroundMusicStyle: override.backgroundMusicStyle?.trim()
      ? override.backgroundMusicStyle
      : (base.backgroundMusicStyle ?? null),
    backgroundMusicPrompt: override.backgroundMusicPrompt?.trim()
      ? override.backgroundMusicPrompt
      : (base.backgroundMusicPrompt ?? null),
    // 中文注释：新草稿以 2D 多视角图片为正式素材；历史模型字段只做兼容保留。
    status:
      overrideHasModel && base.status !== 'model_ready'
        ? 'model_ready'
        : overrideHasImages
          ? 'image_ready'
          : base.status,
    error: override.error ?? base.error ?? null,
  };
}

function createMatch3DAssetDrafts(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null = null,
): Match3DItemAssetDraft[] {
  const generatedAssets = resolveMatch3DResultGeneratedItemAssets(
    profile,
    draft,
  );
  if (generatedAssets?.length) {
    return generatedAssets.map((asset) =>
      createMatch3DAssetDraftFromGeneratedAsset(profile, asset),
    );
  }

  const theme = profile.themeText.trim() || '主题';
  const seeds = [
    {
      id: 'primary-item',
      name: `${theme}核心物件`,
      usage: '局内主要点击消除物件',
    },
    {
      id: 'rare-item',
      name: `${theme}稀有物件`,
      usage: '用于增强辨识度的高价值物件',
    },
    {
      id: 'bonus-item',
      name: `${theme}奖励物件`,
      usage: '通关或连消反馈奖励',
    },
    {
      id: 'obstacle-item',
      name: `${theme}干扰物件`,
      usage: '堆叠层中的视觉遮挡物',
    },
    {
      id: 'tray-prop',
      name: `${theme}托盘道具`,
      usage: '备选栏和结算展示道具',
    },
    {
      id: 'scene-prop',
      name: `${theme}场景小物`,
      usage: '圆形空间周边装饰物',
    },
  ];
  const fallbackSeeds = Array.from(
    { length: MATCH3D_DEFAULT_ASSET_COUNT },
    (_, index) => {
      const seed = seeds[index];
      return (
        seed ?? {
          id: `generated-item-${index + 1}`,
          name: `${theme}物品${index + 1}`,
          usage: '局内点击消除物件',
        }
      );
    },
  );

  return fallbackSeeds.map((seed) => ({
    ...seed,
    prompt: buildMatch3DAssetPrompt(profile, seed.name, seed.usage),
    referenceImageSrc: profile.referenceImageSrc ?? profile.coverImageSrc ?? '',
    imageViews: [],
    taskUuid: null,
    subscriptionKey: null,
    status: 'idle',
    progress: null,
    downloads: [],
    soundPrompt: buildFallbackMatch3DClickSoundPrompt(profile, seed.name),
    backgroundMusicTitle: null,
    backgroundMusicStyle: null,
    backgroundMusicPrompt: null,
    backgroundMusic: null,
    clickSound: null,
    backgroundAsset: null,
    error: null,
    updatedAt: null,
  }));
}

function createMatch3DAssetDraftFromGeneratedAsset(
  profile: Match3DWorkProfile,
  asset: Match3DGeneratedItemAsset,
): Match3DItemAssetDraft {
  const modelSource = resolveMatch3DGeneratedModelAssetSource(asset);
  const imageSource = resolveMatch3DGeneratedImageAssetSource(asset);
  const downloads = modelSource
    ? [
        {
          name: asset.modelFileName ?? `${asset.itemName}.glb`,
          url: modelSource,
        },
      ]
    : [];

  return {
    id: asset.itemId,
    name: asset.itemName,
    usage: '局内点击消除物件',
    prompt: buildMatch3DAssetPrompt(
      profile,
      asset.itemName,
      '局内点击消除物件',
    ),
    referenceImageSrc:
      imageSource ||
      asset.imageSrc ||
      profile.referenceImageSrc ||
      profile.coverImageSrc ||
      '',
    imageViews: asset.imageViews ?? [],
    taskUuid: asset.taskUuid ?? null,
    subscriptionKey: asset.subscriptionKey ?? null,
    status:
      asset.status === 'model_ready'
        ? 'done'
        : normalizeMatch3DAssetStatus(asset.status),
    progress: asset.status === 'model_ready' ? 1 : null,
    downloads,
    soundPrompt:
      asset.soundPrompt?.trim() ||
      asset.clickSound?.prompt?.trim() ||
      buildFallbackMatch3DClickSoundPrompt(profile, asset.itemName),
    backgroundMusicTitle:
      asset.backgroundMusicTitle ?? asset.backgroundMusic?.title ?? null,
    backgroundMusicStyle: asset.backgroundMusicStyle ?? null,
    backgroundMusicPrompt:
      asset.backgroundMusicPrompt ?? asset.backgroundMusic?.prompt ?? null,
    backgroundMusic: asset.backgroundMusic ?? null,
    backgroundAsset: asset.backgroundAsset ?? null,
    clickSound: asset.clickSound ?? null,
    error: asset.error ?? null,
    updatedAt: profile.updatedAt,
  };
}

function createGeneratedAssetsFromDrafts(
  assetDrafts: Match3DItemAssetDraft[],
  existingAssets: readonly Match3DGeneratedItemAsset[] = [],
): Match3DGeneratedItemAsset[] {
  const existingById = new Map(
    existingAssets.map((asset) => [asset.itemId, asset]),
  );
  return assetDrafts.map((asset) => {
    const existing = existingById.get(asset.id);
    const modelFile = asset.downloads.find((file) => file.url.trim()) ?? null;
    const modelSource =
      modelFile?.url.trim() ||
      existing?.modelSrc?.trim() ||
      existing?.modelObjectKey?.trim() ||
      null;
    const modelObjectKey =
      modelFile?.url && isGeneratedLegacyPath(modelFile.url)
        ? modelFile.url.trim().replace(/^\/+/u, '')
        : (existing?.modelObjectKey ?? null);
    return {
      itemId: asset.id,
      itemName: asset.name,
      imageSrc: existing?.imageSrc ?? (asset.referenceImageSrc || null),
      imageObjectKey: existing?.imageObjectKey ?? null,
      imageViews: asset.imageViews ?? existing?.imageViews ?? [],
      modelSrc: modelSource,
      modelObjectKey,
      modelFileName: modelFile?.name?.trim() || existing?.modelFileName || null,
      taskUuid: asset.taskUuid,
      subscriptionKey: asset.subscriptionKey,
      soundPrompt: asset.soundPrompt.trim() || existing?.soundPrompt || null,
      backgroundMusicTitle:
        asset.backgroundMusicTitle ?? existing?.backgroundMusicTitle ?? null,
      backgroundMusicStyle:
        asset.backgroundMusicStyle ?? existing?.backgroundMusicStyle ?? null,
      backgroundMusicPrompt:
        asset.backgroundMusicPrompt ?? existing?.backgroundMusicPrompt ?? null,
      backgroundMusic:
        asset.backgroundMusic ?? existing?.backgroundMusic ?? null,
      clickSound: asset.clickSound,
      backgroundAsset:
        asset.backgroundAsset ??
        existing?.backgroundAsset ??
        (asset.id === assetDrafts[0]?.id
          ? (existingAssets
              .map((candidate) => candidate.backgroundAsset ?? null)
              .find(Boolean) ?? null)
          : null),
      // 中文注释：当前主链路只要求 2D 图片素材；但历史草稿若已有平台模型字段，保存时不能把模型状态降回图片态。
      status: modelSource?.trim()
        ? 'model_ready'
        : hasMatch3DGeneratedImageSource(
              existing ?? ({} as Match3DGeneratedItemAsset),
            )
          ? 'image_ready'
          : asset.status === 'done'
            ? 'model_ready'
            : asset.status,
      error: asset.error,
    };
  });
}

function normalizeMatch3DAssetStatus(status: string): Match3DAssetTaskStatus {
  const normalized = status.trim().toLowerCase();
  if (
    normalized === 'waiting' ||
    normalized === 'pending' ||
    normalized === 'queued'
  ) {
    return 'waiting';
  }
  if (
    normalized === 'generating' ||
    normalized === 'running' ||
    normalized === 'processing'
  ) {
    return 'generating';
  }
  if (normalized === 'image_ready') {
    return 'image_ready';
  }
  if (
    normalized === 'done' ||
    normalized === 'succeeded' ||
    normalized === 'success' ||
    normalized === 'completed'
  ) {
    return 'done';
  }
  if (
    normalized === 'failed' ||
    normalized === 'error' ||
    normalized === 'canceled' ||
    normalized === 'cancelled'
  ) {
    return 'failed';
  }

  return 'unknown';
}

function getMatch3DBatchGenerationStatusLabel(
  phase: Match3DBatchItemGenerationState['phase'],
) {
  if (phase === 'done') return '生成完成';
  if (phase === 'failed') return '生成失败';
  if (phase === 'generating') return '生成中';
  return '';
}

function Match3DBatchGenerationProgress({
  generationState,
}: {
  generationState: Match3DBatchItemGenerationState;
}) {
  if (generationState.phase === 'idle') {
    return null;
  }

  const normalizedProgress =
    generationState.progress === null
      ? null
      : Math.max(0, Math.min(1, generationState.progress));

  return (
    <PlatformSubpanel
      as="div"
      surface="flat"
      radius="sm"
      padding="sm"
      className="bg-white/62"
      role="status"
      aria-label="物品素材生成进度"
    >
      <div className="mb-2 flex items-center justify-between text-xs font-bold text-[var(--platform-text-base)]">
        <span>
          {getMatch3DBatchGenerationStatusLabel(generationState.phase)}
        </span>
        {normalizedProgress !== null ? (
          <span>{Math.round(normalizedProgress * 100)}%</span>
        ) : null}
      </div>
      {normalizedProgress !== null ? (
        <PlatformProgressBar
          value={normalizedProgress * 100}
          ariaLabel="物品素材生成进度百分比"
          fillClassName="bg-[var(--platform-accent)]"
          trackStyle={{ backgroundColor: 'rgba(255,255,255,0.7)' }}
        />
      ) : null}
      {generationState.message ? (
        <div className="mt-2 text-sm font-semibold text-[var(--platform-text-base)]">
          {generationState.message}
        </div>
      ) : null}
      {generationState.error ? (
        <div className="mt-2 text-sm font-semibold text-[var(--platform-button-danger-text)]">
          {generationState.error}
        </div>
      ) : null}
    </PlatformSubpanel>
  );
}

function normalizePositiveInteger(value: string) {
  const parsed = Number.parseInt(value.trim(), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

function normalizeDifficulty(value: string) {
  const parsed = Number.parseInt(value.trim(), 10);
  return Number.isFinite(parsed) && parsed >= 1 && parsed <= 10 ? parsed : null;
}

function createEditState(profile: Match3DWorkProfile): Match3DResultEditState {
  const difficultyOption = getMatch3DDifficultyOption(
    resolveMatch3DDifficultyOptionId(profile.difficulty, profile.clearCount),
  );
  return {
    gameName: profile.gameName,
    summary: profile.summary,
    tagsText: profile.tags.join('，'),
    coverImageSrc:
      profile.coverImageSrc?.trim() || profile.referenceImageSrc?.trim() || '',
    themeText: profile.themeText,
    clearCountText: String(difficultyOption.clearCount),
    difficultyText: String(difficultyOption.difficulty),
  };
}

function buildSavePayload(
  editState: Match3DResultEditState,
): PutMatch3DWorkRequest | null {
  const clearCount = normalizePositiveInteger(editState.clearCountText);
  const difficulty = normalizeDifficulty(editState.difficultyText);
  const gameName = editState.gameName.trim();
  const themeText = editState.themeText.trim();
  const summary = editState.summary.trim();
  const tags = normalizeTags(editState.tagsText);

  if (!gameName || !themeText || !clearCount || !difficulty) {
    return null;
  }

  return {
    gameName,
    themeText,
    summary,
    tags,
    coverImageSrc: editState.coverImageSrc.trim() || null,
    clearCount,
    difficulty,
  };
}

function buildPublishBlockers(
  editState: Match3DResultEditState,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  const tags = normalizeTags(editState.tagsText);
  const selectedDifficulty = getMatch3DDifficultyOptionFromEditState(editState);
  const readyItemTypeCount = getMatch3DReadyItemTypeCount(generatedItemAssets);
  const assetReadyBlocker =
    generatedItemAssets.length > 0 &&
    readyItemTypeCount < selectedDifficulty.itemTypeCount
      ? [
          `当前难度需要 ${selectedDifficulty.itemTypeCount} 种物品，已生成 ${readyItemTypeCount} 种，请先在素材配置中补齐。`,
        ]
      : [];
  const blockers = [
    ...(editState.gameName.trim() ? [] : ['游戏名称不能为空。']),
    ...(editState.themeText.trim() ? [] : ['题材主题不能为空。']),
    ...(editState.summary.trim() ? [] : ['简介不能为空。']),
    ...(editState.coverImageSrc.trim() ? [] : ['封面图不能为空。']),
    ...(tags.length >= MATCH3D_MIN_TAG_COUNT &&
    tags.length <= MATCH3D_MAX_TAG_COUNT
      ? []
      : [
          `标签数量需要在 ${MATCH3D_MIN_TAG_COUNT} 到 ${MATCH3D_MAX_TAG_COUNT} 个之间。`,
        ]),
    ...(normalizePositiveInteger(editState.clearCountText)
      ? []
      : ['需要消除次数必须为正整数。']),
    ...(normalizeDifficulty(editState.difficultyText)
      ? []
      : ['难度必须为 1 到 10。']),
    ...assetReadyBlocker,
  ];

  return [...new Set(blockers)];
}

function buildTestRunBlockers(editState: Match3DResultEditState) {
  const blockers = [
    ...(editState.gameName.trim() ? [] : ['游戏名称不能为空。']),
    ...(editState.themeText.trim() ? [] : ['题材主题不能为空。']),
    ...(normalizePositiveInteger(editState.clearCountText)
      ? []
      : ['需要消除次数必须为正整数。']),
    ...(normalizeDifficulty(editState.difficultyText)
      ? []
      : ['难度必须为 1 到 10。']),
  ];

  return [...new Set(blockers)];
}

async function readImageAsDataUrl(file: File) {
  return readPuzzleReferenceImageAsDataUrl(file);
}

async function readCoverReferenceImageAsDataUrl(file: File) {
  return readPuzzleReferenceImageAsDataUrl(file);
}

function resolveMatch3DCoverSourceAssets(
  assetDrafts: Match3DItemAssetDraft[],
  backgroundPreviewSrc: string,
  containerPreviewSrc: string,
): Match3DCoverSourceAsset[] {
  const itemAssets = assetDrafts
    .filter((asset) => asset.referenceImageSrc.trim())
    .map((asset) => ({
      id: `item:${asset.id}`,
      label: asset.name,
      imageSrc: asset.referenceImageSrc,
      kind: 'item' as const,
    }));
  const uiAssets = [
    ...(backgroundPreviewSrc.trim()
      ? [
          {
            id: 'ui:background',
            label: '游戏背景图',
            imageSrc: backgroundPreviewSrc,
            kind: 'ui' as const,
          },
        ]
      : []),
    ...(containerPreviewSrc.trim() &&
    containerPreviewSrc.trim() !== backgroundPreviewSrc.trim()
      ? [
          {
            id: 'ui:container',
            label: '游戏容器图',
            imageSrc: containerPreviewSrc,
            kind: 'ui' as const,
          },
        ]
      : []),
  ];
  return [...itemAssets, ...uiAssets];
}

function createMatch3DCoverReferenceDraftFromSource(
  asset: Match3DCoverSourceAsset,
): Match3DCoverReferenceDraft {
  return {
    id: asset.id,
    label: asset.label,
    imageSrc: asset.imageSrc,
    source: 'asset',
  };
}

function addMatch3DCoverReferenceDraft(
  currentReferences: Match3DCoverReferenceDraft[],
  nextReference: Match3DCoverReferenceDraft,
) {
  const deduped = currentReferences.filter(
    (reference) => reference.imageSrc !== nextReference.imageSrc,
  );
  return [nextReference, ...deduped].slice(
    0,
    MATCH3D_COVER_REFERENCE_IMAGE_LIMIT,
  );
}

function buildPlayableProfile(
  profile: Match3DWorkProfile,
  editState: Match3DResultEditState,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[] = [],
) {
  const payload = buildSavePayload(editState);
  if (!payload) {
    return promoteMatch3DGeneratedBackgroundAsset(
      attachMatch3DGeneratedItemAssets(profile, generatedItemAssets),
    );
  }

  return promoteMatch3DGeneratedBackgroundAsset(
    attachMatch3DGeneratedItemAssets(
      {
        ...profile,
        gameName: payload.gameName,
        themeText: payload.themeText ?? profile.themeText,
        summary: payload.summary,
        tags: payload.tags,
        coverImageSrc: payload.coverImageSrc,
        clearCount: payload.clearCount,
        difficulty: payload.difficulty,
      },
      generatedItemAssets,
    ),
  );
}

function buildCoverImageUpdatedProfile(
  profile: Match3DWorkProfile,
  editState: Match3DResultEditState,
  responseItem: Match3DWorkProfile,
  coverImageSrc: string,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[] = [],
) {
  // 中文注释：封面生成只允许更新封面字段；接口回包如果来自旧快照，不能覆盖当前物品素材或难度配置。
  const visibleProfile = buildPlayableProfile(
    profile,
    {
      ...editState,
      coverImageSrc,
    },
    generatedItemAssets.length > 0
      ? generatedItemAssets
      : (profile.generatedItemAssets ?? []),
  );

  return {
    ...visibleProfile,
    coverImageSrc,
    updatedAt: responseItem.updatedAt || visibleProfile.updatedAt,
  };
}

function resolveMatch3DResultGeneratedItemAssets(
  profile: Match3DWorkProfile,
  draft: Match3DResultDraft | null,
) {
  const profileAssets = profile.generatedItemAssets ?? [];
  const draftAssets = draft?.generatedItemAssets ?? [];
  if (draftAssets.length <= 0) {
    return normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
  }
  if (profileAssets.length <= 0) {
    return normalizeMatch3DGeneratedItemAssetsForRuntime(draftAssets);
  }

  return mergeMatch3DGeneratedItemAssetsForRuntime(
    draftAssets.map((draftAsset) => {
      const profileAsset = profileAssets.find(
        (asset) => asset.itemId === draftAsset.itemId,
      );
      return profileAsset
        ? mergeMatch3DGeneratedItemAsset(draftAsset, profileAsset)
        : draftAsset;
    }),
    profileAssets,
  );
}

function attachMatch3DGeneratedItemAssets(
  profile: Match3DWorkProfile,
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  if (generatedItemAssets.length <= 0) {
    return promoteMatch3DGeneratedBackgroundAsset(profile);
  }

  // 中文注释：试玩入口依赖当前页面可见的生成素材；保存接口若返回旧快照，不能把素材从运行态入参里丢掉。
  return promoteMatch3DGeneratedBackgroundAsset({
    ...profile,
    generatedItemAssets:
      normalizeMatch3DGeneratedItemAssetsForRuntime(generatedItemAssets),
  });
}

function buildPersistableGeneratedItemAssets(
  assetDrafts: Match3DItemAssetDraft[],
  generatedItemAssets: readonly Match3DGeneratedItemAsset[],
) {
  if (generatedItemAssets.length <= 0) {
    return [];
  }

  return normalizeMatch3DGeneratedItemAssetsForRuntime(
    createGeneratedAssetsFromDrafts(assetDrafts, generatedItemAssets).filter(
      hasPersistableMatch3DGeneratedItemAsset,
    ),
  );
}

function Match3DResultHeader({
  autoSaveState,
  isBusy,
  onBack,
}: {
  autoSaveState: Match3DAutoSaveState;
  isBusy: boolean;
  onBack: () => void;
}) {
  const badge =
    autoSaveState === 'saving' ? (
      <PlatformPillBadge tone="warning" size="xs" className="px-3 py-1">
        保存中
      </PlatformPillBadge>
    ) : autoSaveState === 'saved' ? (
      <PlatformPillBadge tone="success" size="xs" className="px-3 py-1">
        已自动保存
      </PlatformPillBadge>
    ) : autoSaveState === 'error' ? (
      <PlatformPillBadge tone="danger" size="xs" className="px-3 py-1">
        保存失败
      </PlatformPillBadge>
    ) : null;

  return (
    <div className="mb-4 flex items-center justify-between gap-3">
      <PlatformActionButton
        onClick={onBack}
        disabled={isBusy}
        tone="ghost"
        size="xs"
        className="min-h-0 self-start py-1.5 text-[11px]"
      >
        <span className="inline-flex items-center gap-1.5">
          <ArrowLeft className="h-3.5 w-3.5" />
          返回
        </span>
      </PlatformActionButton>
      {badge}
    </div>
  );
}

function Match3DResultTabs({
  activeTab,
  onChange,
}: {
  activeTab: Match3DResultTab;
  onChange: (tab: Match3DResultTab) => void;
}) {
  return (
    <PlatformSegmentedTabs
      items={MATCH3D_RESULT_TABS}
      activeId={activeTab}
      onChange={onChange}
      columns="three"
      className="mb-3"
      itemClassName="px-2 sm:px-3"
    />
  );
}

function Match3DWorkInfoTab({
  editState,
  isBusy,
  onChange,
  onGenerateTags,
}: {
  editState: Match3DResultEditState;
  isBusy: boolean;
  onChange: (nextState: Match3DResultEditState) => void;
  onGenerateTags: () => void;
}) {
  const tags = normalizeTags(editState.tagsText);
  const updateTags = (nextTags: string[]) => {
    onChange({ ...editState, tagsText: nextTags.join('，') });
  };

  return (
    <PlatformSubpanel radius="lg" padding="lg" className="space-y-3">
      <label className="block">
        <PlatformFieldLabel variant="section">作品名称</PlatformFieldLabel>
        <PlatformTextField
          value={editState.gameName}
          disabled={isBusy}
          onChange={(event) =>
            onChange({ ...editState, gameName: event.target.value })
          }
          size="lg"
          className="mt-2"
          aria-label="作品名称"
        />
      </label>

      <label className="block">
        <PlatformFieldLabel variant="section">作品描述</PlatformFieldLabel>
        <PlatformTextField
          variant="textarea"
          value={editState.summary}
          disabled={isBusy}
          onChange={(event) =>
            onChange({ ...editState, summary: event.target.value })
          }
          rows={6}
          size="md"
          className="mt-2"
          aria-label="作品描述"
        />
      </label>

      <PlatformTagEditor
        title="作品标签"
        tags={tags}
        disabled={isBusy}
        maxTags={MATCH3D_MAX_TAG_COUNT}
        addLabel="新增作品标签"
        generateLabel="AI生成作品标签"
        parseInput={normalizeMatch3DTagListText}
        onChange={updateTags}
        onGenerate={onGenerateTags}
        generateIcon={<Wand2 className="h-4 w-4" />}
        tone="warm"
        padding="none"
      />
    </PlatformSubpanel>
  );
}

function Match3DModalShell({
  title,
  children,
  onClose,
}: {
  title: string;
  children: ReactNode;
  onClose: () => void;
}) {
  const platformTheme = useAuthUi()?.platformTheme ?? 'light';
  if (typeof document === 'undefined') {
    return null;
  }

  return createPortal(
    <div
      className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[146] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
      onClick={(event) => {
        if (event.target === event.currentTarget) {
          onClose();
        }
      }}
    >
      <div
        role="dialog"
        aria-modal="true"
        aria-label={title}
        className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,48rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
          <div className="text-base font-semibold text-[var(--platform-text-strong)]">
            {title}
          </div>
          <PlatformModalCloseButton
            onClick={onClose}
            label="关闭"
            variant="platformIcon"
          />
        </div>
        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          {children}
        </div>
      </div>
    </div>,
    document.body,
  );
}

type Match3DCoverImageEditorProps = {
  editState: Match3DResultEditState;
  sourceAssets: Match3DCoverSourceAsset[];
  isGenerating: boolean;
  uploadedImageSrc: string;
  referenceImages: Match3DCoverReferenceDraft[];
  aiRedraw: boolean;
  prompt: string;
  error: string | null;
  onAiRedrawChange: (enabled: boolean) => void;
  onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
  onPromptChange: (value: string) => void;
  onReferenceSelect: (source: string) => void;
  onReferenceFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
  onReferenceRemove: (referenceId: string) => void;
  onUploadedImageRemove: () => void;
  onSubmit: () => void;
};

function Match3DCoverImageEditor({
  editState,
  sourceAssets,
  isGenerating,
  uploadedImageSrc,
  referenceImages,
  aiRedraw,
  prompt,
  error,
  onAiRedrawChange,
  onFileChange,
  onPromptChange,
  onReferenceSelect,
  onReferenceFileChange,
  onReferenceRemove,
  onUploadedImageRemove,
  onSubmit,
}: Match3DCoverImageEditorProps) {
  const previewSrc = uploadedImageSrc || editState.coverImageSrc;
  const promptLabel = uploadedImageSrc ? 'AI重绘要求' : '封面描述';
  const canSubmit = Boolean(uploadedImageSrc.trim() || prompt.trim());

  return (
    <div className="grid min-h-0 gap-4 lg:grid-cols-[minmax(16rem,0.85fr)_minmax(0,1.15fr)]">
      <div className="space-y-3">
        <div className="relative aspect-square overflow-hidden rounded-[1.35rem] border border-[var(--platform-subpanel-border)] bg-white/70">
          <input
            id="match3d-cover-upload-input"
            type="file"
            accept="image/*"
            className="sr-only"
            disabled={isGenerating}
            onChange={onFileChange}
          />
          <label
            htmlFor="match3d-cover-upload-input"
            className={`absolute inset-0 z-0 ${isGenerating ? 'cursor-not-allowed' : 'cursor-pointer'}`}
            title={previewSrc ? '更换封面图' : '上传封面图'}
          >
            <span className="sr-only">
              {previewSrc ? '更换封面图' : '上传封面图'}
            </span>
          </label>
          {previewSrc ? (
            <ResolvedAssetImage
              src={previewSrc}
              alt="封面图预览"
              className="pointer-events-none h-full w-full object-cover"
            />
          ) : (
            <div className="pointer-events-none grid h-full w-full place-items-center text-[var(--platform-text-soft)]">
              <ImageIcon className="h-10 w-10" />
            </div>
          )}
          <div className="pointer-events-none absolute inset-0 z-[1] bg-[linear-gradient(180deg,rgba(255,255,255,0.10)_0%,rgba(255,255,255,0.02)_44%,rgba(255,255,255,0.16)_100%)]" />
          {uploadedImageSrc ? (
            <>
              <PlatformPillSwitch
                label="AI重绘"
                aria-label="AI重绘"
                checked={aiRedraw}
                disabled={isGenerating}
                onChange={(event) => onAiRedrawChange(event.target.checked)}
                className="absolute bottom-3 left-3 z-10"
              />
              <PlatformIconButton
                variant="surfaceFloating"
                label="移除封面图"
                title="移除封面图"
                icon={<Trash2 className="h-4 w-4" />}
                disabled={isGenerating}
                onClick={onUploadedImageRemove}
                className="absolute left-3 top-3 z-10 h-10 w-10"
              />
            </>
          ) : (
            <label
              htmlFor="match3d-cover-upload-input"
              className={`absolute bottom-9 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap text-center text-sm font-black text-[var(--platform-text-strong)] drop-shadow-[0_1px_0_rgba(255,255,255,0.82)] transition hover:text-[var(--platform-accent)] ${isGenerating ? 'cursor-not-allowed opacity-55' : 'cursor-pointer'}`}
            >
              上传图片/填写封面描述
            </label>
          )}
        </div>
      </div>

      <div className="space-y-3">
        <label className="block">
          <PlatformFieldLabel variant="section">
            {promptLabel}
          </PlatformFieldLabel>
          <PlatformTextField
            variant="textarea"
            value={prompt}
            disabled={isGenerating}
            rows={5}
            onChange={(event) => onPromptChange(event.target.value)}
            size="md"
            className="mt-2"
            aria-label={promptLabel}
          />
        </label>

        {!uploadedImageSrc ? (
          <div>
            <div className="mb-2 flex items-center justify-between gap-3">
              <PlatformFieldLabel variant="section">参考图</PlatformFieldLabel>
              <PlatformIconButton
                asChild="label"
                className="h-9 w-9 cursor-pointer"
                label="上传参考图"
                title="上传参考图"
                icon={
                  <>
                    <ImagePlus className="h-4 w-4" />
                    <input
                      type="file"
                      accept="image/*"
                      className="sr-only"
                      disabled={isGenerating}
                      onChange={onReferenceFileChange}
                    />
                  </>
                }
              />
            </div>
            {referenceImages.length > 0 ? (
              <div className="mb-3 grid grid-cols-3 gap-2 sm:grid-cols-4">
                {referenceImages.map((reference) => (
                  <PlatformUploadPreviewCard
                    key={reference.id}
                    imageSrc={reference.imageSrc}
                    imageAlt=""
                    caption={reference.label}
                    removeLabel={`移除参考图 ${reference.label}`}
                    onRemove={() => onReferenceRemove(reference.id)}
                    disabled={isGenerating}
                    resolveAsset
                    className="h-auto w-full rounded-[1rem] border-[var(--platform-warm-border)] bg-white/74"
                    removeIcon={<Trash2 className="h-3.5 w-3.5" />}
                    removeButtonProps={{
                      title: '移除参考图',
                      className:
                        'bottom-1.5 right-1.5 top-auto h-6 w-6 bg-white/92 text-[var(--platform-text-strong)] shadow-sm hover:bg-white hover:text-[var(--platform-accent)]',
                    }}
                  />
                ))}
              </div>
            ) : null}
            {sourceAssets.length > 0 ? (
              <PlatformAssetPickerGrid
                items={sourceAssets}
                loadingLabel="读取中..."
                emptyLabel="暂无可引用素材"
                disabled={isGenerating}
                getKey={(asset) => asset.id}
                getImageSrc={(asset) => asset.imageSrc}
                getImageAlt={() => ''}
                getTitle={(asset) => asset.label}
                getAriaLabel={(asset) => `引用${asset.label}`}
                isSelected={(asset) =>
                  referenceImages.some(
                    (reference) => reference.imageSrc === asset.imageSrc,
                  )
                }
                onSelect={(asset) => onReferenceSelect(asset.imageSrc)}
                gridClassName="grid grid-cols-3 gap-2 sm:grid-cols-4"
                cardClassName="bg-white/74"
                cardRadiusClassName="rounded-[1rem]"
                imageShellClassName="aspect-square"
                bodyClassName="truncate px-2 py-2 text-[11px] font-semibold text-[var(--platform-text-base)]"
              />
            ) : null}
          </div>
        ) : null}

        {error ? (
          <PlatformStatusMessage tone="error" surface="platform" size="md">
            {error}
          </PlatformStatusMessage>
        ) : null}

        <PlatformActionButton
          disabled={!canSubmit || isGenerating}
          onClick={onSubmit}
          size="md"
          fullWidth
          className="min-h-11 gap-2"
        >
          {isGenerating ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : uploadedImageSrc && !aiRedraw ? (
            <CheckCircle2 className="h-4 w-4" />
          ) : (
            <Wand2 className="h-4 w-4" />
          )}
          {uploadedImageSrc && !aiRedraw ? '使用当前图片' : '生成封面图'}
        </PlatformActionButton>
      </div>
    </div>
  );
}

function Match3DPublishDialog({
  blockers,
  editState,
  isBusy,
  isGeneratingCover,
  isPublishing,
  sourceAssets,
  uploadedImageSrc,
  referenceImages,
  aiRedraw,
  prompt,
  coverError,
  publishError,
  onAiRedrawChange,
  onClose,
  onFileChange,
  onPromptChange,
  onPublish,
  onReferenceSelect,
  onReferenceFileChange,
  onReferenceRemove,
  onUploadedImageRemove,
  onSubmitCover,
}: {
  blockers: string[];
  editState: Match3DResultEditState;
  isBusy: boolean;
  isGeneratingCover: boolean;
  isPublishing: boolean;
  sourceAssets: Match3DCoverSourceAsset[];
  uploadedImageSrc: string;
  referenceImages: Match3DCoverReferenceDraft[];
  aiRedraw: boolean;
  prompt: string;
  coverError: string | null;
  publishError: string | null;
  onAiRedrawChange: (enabled: boolean) => void;
  onClose: () => void;
  onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
  onPromptChange: (value: string) => void;
  onPublish: () => void;
  onReferenceSelect: (source: string) => void;
  onReferenceFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
  onReferenceRemove: (referenceId: string) => void;
  onUploadedImageRemove: () => void;
  onSubmitCover: () => void;
}) {
  const platformTheme = useAuthUi()?.platformTheme ?? 'light';
  const publishReady = blockers.length === 0;

  if (typeof document === 'undefined') {
    return null;
  }

  return createPortal(
    <div
      className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[146] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
      onClick={(event) => {
        if (event.target === event.currentTarget && !isGeneratingCover) {
          onClose();
        }
      }}
    >
      <div
        role="dialog"
        aria-modal="true"
        aria-label="发布抓大鹅作品"
        className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,50rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
          <div className="text-base font-semibold text-[var(--platform-text-strong)]">
            发布抓大鹅作品
          </div>
          <PlatformModalCloseButton
            onClick={onClose}
            disabled={isGeneratingCover}
            label="关闭"
            variant="platformIcon"
            className={isGeneratingCover ? 'cursor-not-allowed opacity-55' : ''}
          />
        </div>

        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          <div className="mb-4 space-y-2">
            <PlatformFieldLabel variant="section">发布检查</PlatformFieldLabel>
            {publishError ? (
              <PlatformStatusMessage tone="error" surface="platform">
                {publishError}
              </PlatformStatusMessage>
            ) : publishReady ? (
              <PlatformStatusMessage tone="success" surface="platform">
                当前作品已满足发布条件。
              </PlatformStatusMessage>
            ) : (
              <div className="grid gap-2 sm:grid-cols-2">
                {blockers.map((blocker, index) => (
                  <PlatformStatusMessage
                    key={`match3d-publish-blocker-${index}-${blocker}`}
                    tone="warning"
                    surface="platform"
                  >
                    {blocker}
                  </PlatformStatusMessage>
                ))}
              </div>
            )}
          </div>

          <PlatformFieldLabel variant="section" className="mb-3 block">
            封面图
          </PlatformFieldLabel>
          <Match3DCoverImageEditor
            editState={editState}
            sourceAssets={sourceAssets}
            isGenerating={isGeneratingCover}
            uploadedImageSrc={uploadedImageSrc}
            referenceImages={referenceImages}
            aiRedraw={aiRedraw}
            prompt={prompt}
            error={coverError}
            onAiRedrawChange={onAiRedrawChange}
            onFileChange={onFileChange}
            onPromptChange={onPromptChange}
            onReferenceSelect={onReferenceSelect}
            onReferenceFileChange={onReferenceFileChange}
            onReferenceRemove={onReferenceRemove}
            onUploadedImageRemove={onUploadedImageRemove}
            onSubmit={onSubmitCover}
          />
        </div>

        <div className="flex flex-col-reverse gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4 sm:flex-row sm:justify-end">
          <PlatformActionButton
            onClick={onClose}
            disabled={isGeneratingCover || isPublishing}
            tone="ghost"
          >
            取消
          </PlatformActionButton>
          <PlatformActionButton
            onClick={onPublish}
            disabled={!publishReady || isBusy}
          >
            {isPublishing ? (
              <Loader2 className="h-4 w-4 animate-spin" />
            ) : (
              <Send className="h-4 w-4" />
            )}
            发布到广场
          </PlatformActionButton>
        </div>
      </div>
    </div>,
    document.body,
  );
}

function Match3DConfigTab({
  editState,
  isBusy,
  generatedItemAssets,
  totalItemCount,
  onChange,
}: {
  editState: Match3DResultEditState;
  isBusy: boolean;
  generatedItemAssets: readonly Match3DGeneratedItemAsset[];
  totalItemCount: number;
  onChange: (nextState: Match3DResultEditState) => void;
}) {
  const selectedOption = getMatch3DDifficultyOptionFromEditState(editState);
  const selectedOptionIndex = MATCH3D_DIFFICULTY_OPTIONS.findIndex(
    (option) => option.id === selectedOption.id,
  );
  const selectedSliderIndex = Math.max(0, selectedOptionIndex);
  const runtimeTypeCount = selectedOption.itemTypeCount;
  const readyItemTypeCount = getMatch3DReadyItemTypeCount(generatedItemAssets);
  const trackProgress =
    selectedSliderIndex / Math.max(1, MATCH3D_DIFFICULTY_OPTIONS.length - 1);
  const applyDifficultyOption = (option: Match3DDifficultyOption) => {
    onChange({
      ...editState,
      clearCountText: String(option.clearCount),
      difficultyText: String(option.difficulty),
    });
  };
  const handleSliderChange = (event: ChangeEvent<HTMLInputElement>) => {
    const nextIndex = Number.parseInt(event.target.value, 10);
    const nextOption = MATCH3D_DIFFICULTY_OPTIONS[nextIndex];
    if (nextOption) {
      applyDifficultyOption(nextOption);
    }
  };

  return (
    <div className="space-y-3">
      <PlatformSubpanel radius="lg" padding="lg">
        <div className="relative px-1 pb-1 pt-2">
          <div className="relative mx-[1.35rem] h-10">
            <div className="absolute left-0 right-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-white/75 shadow-[inset_0_0_0_1px_rgba(204,117,76,0.16)]" />
            <div
              className="absolute left-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-[linear-gradient(90deg,#df7f40_0%,#c7653d_54%,#e2ab86_100%)] transition-[width] duration-200"
              style={{ width: `${trackProgress * 100}%` }}
            />
            {MATCH3D_DIFFICULTY_OPTIONS.map((option, index) => {
              const selected = selectedOption.id === option.id;
              return (
                <div
                  key={option.id}
                  aria-hidden="true"
                  className={`absolute top-1/2 flex h-8 w-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-warm-border)] ${
                    selected
                      ? 'border-[var(--platform-surface-hover-border)] bg-white shadow-[0_8px_18px_rgba(112,57,30,0.16)]'
                      : 'border-[var(--platform-subpanel-border)] bg-white/90 hover:border-[var(--platform-surface-hover-border)]'
                  } ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
                  style={{
                    left: `${(index / (MATCH3D_DIFFICULTY_OPTIONS.length - 1)) * 100}%`,
                  }}
                >
                  <span
                    className={`h-3.5 w-3.5 rounded-full ${
                      selected
                        ? 'bg-[var(--platform-accent)]'
                        : 'bg-[var(--platform-warm-bg)]'
                    }`}
                  />
                </div>
              );
            })}
            <input
              type="range"
              min={0}
              max={MATCH3D_DIFFICULTY_OPTIONS.length - 1}
              step={1}
              value={selectedSliderIndex}
              disabled={isBusy}
              onChange={handleSliderChange}
              className="absolute inset-x-0 top-1/2 z-10 h-10 -translate-y-1/2 cursor-pointer opacity-0 disabled:cursor-not-allowed"
              aria-label="难度"
              aria-valuetext={selectedOption.label}
            />
          </div>
          <PlatformSegmentedTabs
            items={MATCH3D_DIFFICULTY_OPTIONS.map((option) => ({
              id: option.id,
              label: (
                <>
                  <div className="text-sm font-black">{option.label}</div>
                  <div className="mt-1 text-[10px] font-bold leading-4 text-[var(--platform-text-soft)]">
                    {option.clearCount}次 · {option.itemTypeCount}种
                  </div>
                </>
              ),
              ariaLabel: `${option.label} ${option.clearCount}次 · ${option.itemTypeCount}种`,
            }))}
            activeId={selectedOption.id}
            onChange={(optionId) =>
              applyDifficultyOption(getMatch3DDifficultyOption(optionId))
            }
            columns="four"
            gap="sm"
            size="choice"
            surface="transparent"
            tone="warm"
            frame="bare"
            disabled={isBusy}
            className="mt-3"
          />
        </div>
        <PlatformSubpanel
          as="div"
          surface="flat"
          radius="sm"
          padding="sm"
          className="mt-3"
          data-testid="match3d-difficulty-summary-panel"
        >
          <div className="flex items-center justify-between gap-3">
            <div>
              <div className="text-lg font-black text-[var(--platform-text-strong)]">
                {selectedOption.label}
              </div>
              <div className="mt-1 text-xs font-bold text-[var(--platform-text-base)]">
                {selectedOption.clearCount} 次 · {selectedOption.itemTypeCount}{' '}
                种
              </div>
            </div>
            <PlatformPillBadge
              tone="neutral"
              size="sm"
              className="border-transparent bg-[var(--platform-accent)] text-white shadow-[0_8px_18px_rgba(112,57,30,0.16)]"
            >
              难度 {selectedOption.difficulty}
            </PlatformPillBadge>
          </div>
        </PlatformSubpanel>
        <div className="sr-only" aria-live="polite">
          当前难度 {selectedOption.label}
        </div>
      </PlatformSubpanel>

      <PlatformSubpanel radius="lg" padding="lg">
        <PlatformStatGrid
          items={[
            { label: '需要消除', value: `${selectedOption.clearCount} 次` },
            { label: '总物品数', value: `${totalItemCount} 件` },
            { label: '物品种类', value: `${runtimeTypeCount} 种` },
            { label: '已生成物品种类', value: `${readyItemTypeCount} 种` },
          ]}
          columns="twoToFour"
          order="labelFirst"
          surface="plain"
        />
      </PlatformSubpanel>
    </div>
  );
}

function Match3DItemAssetListCard({
  asset,
  active,
  onClick,
  onDelete,
}: {
  asset: Match3DItemAssetDraft;
  active: boolean;
  onClick: () => void;
  onDelete: () => void;
}) {
  const previewSources = resolveMatch3DAssetDraftPreviewSources(asset);
  const previewSource = previewSources[0] ?? asset.referenceImageSrc.trim();

  return (
    <div
      className={`group min-w-0 rounded-[1.15rem] border p-2 text-left transition-colors ${
        active
          ? 'border-[var(--platform-surface-hover-border)] bg-[var(--platform-warm-bg)]'
          : 'border-[var(--platform-subpanel-border)] bg-white/76 hover:border-[var(--platform-surface-hover-border)] hover:bg-white'
      }`}
    >
      <div className="grid min-h-full grid-rows-[minmax(0,1fr)_auto] gap-2">
        <button
          type="button"
          onClick={onClick}
          className="grid min-h-0 gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-warm-border)]"
          aria-label={`打开${asset.name}物品素材`}
        >
          <PlatformMediaFrame
            src={previewSource}
            alt=""
            fallbackLabel={`${asset.name}素材预览`}
            fallbackContent={
              <ImageIcon className="h-7 w-7 text-[var(--platform-text-soft)]" />
            }
            aspect="square"
            surface="bright"
            imageClassName="h-full w-full object-contain p-1"
            className="min-h-0 rounded-[0.95rem]"
            fallbackClassName="tracking-normal"
          />
          <span className="truncate text-[13px] font-bold leading-5 text-[var(--platform-text-strong)]">
            {asset.name}
          </span>
        </button>
        <div className="flex min-w-0 justify-end">
          <PlatformIconButton
            onClick={onDelete}
            className="h-8 w-8 shrink-0 text-[var(--platform-button-danger-text)]"
            label="删除物品素材"
            title="删除"
            icon={<Trash2 className="h-4 w-4" />}
          />
        </div>
      </div>
    </div>
  );
}

function Match3DItemAssetDetail({
  asset,
  busy,
  onChange,
}: {
  asset: Match3DItemAssetDraft;
  busy: boolean;
  onChange: (asset: Match3DItemAssetDraft) => void;
}) {
  const previewSources = resolveMatch3DAssetDraftPreviewSources(asset);
  const [activePreviewIndex, setActivePreviewIndex] = useState(() =>
    Math.floor(previewSources.length / 2),
  );
  const safeActivePreviewIndex = previewSources[activePreviewIndex]
    ? activePreviewIndex
    : 0;
  const thumbnailPreviewSources =
    previewSources.length > 0
      ? previewSources
      : Array.from({ length: 4 }, () => undefined as string | undefined);
  const activePreviewSource = previewSources[safeActivePreviewIndex];

  useEffect(() => {
    setActivePreviewIndex(Math.floor(previewSources.length / 2));
  }, [asset.id, previewSources.length]);

  return (
    <PlatformSubpanel radius="xl" padding="sm" className="min-h-0 sm:p-5">
      <div className="mx-auto flex min-h-0 w-full max-w-[34rem] flex-col gap-4">
        <div className="space-y-3" aria-label={`${asset.name}五视角预览`}>
          <PlatformMediaFrame
            src={activePreviewSource}
            alt=""
            fallbackLabel={`${asset.name}当前视角`}
            fallbackContent={
              <ImageIcon className="h-12 w-12 text-[var(--platform-text-soft)]" />
            }
            aspect="square"
            surface="bright"
            imageClassName="h-full w-full object-contain p-3 sm:p-4"
            className="mx-auto w-full max-w-[22rem] rounded-[1.15rem]"
            fallbackClassName="tracking-normal"
            data-testid="match3d-item-preview-stage"
          />

          <div
            className="grid grid-flow-col gap-2 overflow-x-auto pb-1"
            data-testid="match3d-item-preview-thumbnails"
            style={{ gridAutoColumns: 'calc((100% - 1.5rem) / 4)' }}
          >
            {thumbnailPreviewSources.map((source, index) => {
              const hasSource = Boolean(source);
              const isActive =
                previewSources.length > 0 && index === safeActivePreviewIndex;
              return (
                <button
                  key={`${source ?? 'empty-thumbnail'}-${index}`}
                  type="button"
                  disabled={!hasSource || busy}
                  onClick={() => setActivePreviewIndex(index)}
                  aria-label={
                    hasSource
                      ? `切换${asset.name}视角${index + 1}`
                      : `空视角槽 ${index + 1}`
                  }
                  aria-pressed={isActive}
                  className={`grid aspect-square place-items-center overflow-hidden rounded-[0.65rem] border bg-white/82 transition ${
                    isActive
                      ? 'border-[var(--platform-surface-hover-border)] ring-2 ring-[var(--platform-warm-border)]'
                      : 'border-[var(--platform-subpanel-border)]'
                  } ${!hasSource || busy ? 'cursor-not-allowed opacity-58' : 'hover:border-[var(--platform-surface-hover-border)]'}`}
                >
                  <PlatformMediaFrame
                    src={source}
                    alt=""
                    fallbackLabel={`视角${index + 1}`}
                    fallbackContent={
                      <ImageIcon className="h-6 w-6 text-[var(--platform-text-soft)]" />
                    }
                    aspect="square"
                    surface="none"
                    imageClassName="h-full w-full object-contain p-1"
                    className="h-full w-full rounded-[0.65rem]"
                    fallbackClassName="tracking-normal"
                  />
                </button>
              );
            })}
          </div>
        </div>

        <label className="block">
          <PlatformFieldLabel variant="section">素材名称</PlatformFieldLabel>
          <PlatformTextField
            value={asset.name}
            disabled={busy}
            onChange={(event) =>
              onChange({ ...asset, name: event.target.value })
            }
            className="mt-2"
          />
        </label>
      </div>
    </PlatformSubpanel>
  );
}

function Match3DAssetsTab({
  activeAssetId,
  assets,
  batchGenerationState,
  onActiveAssetChange,
  onAddBatch,
  onRegenerateBatch,
  onAssetChange,
  onDeleteAsset,
}: {
  activeAssetId: string | null;
  assets: Match3DItemAssetDraft[];
  batchGenerationState: Match3DBatchItemGenerationState;
  onActiveAssetChange: (assetId: string | null) => void;
  onAddBatch: () => void;
  onRegenerateBatch: () => void;
  onAssetChange: (asset: Match3DItemAssetDraft) => void;
  onDeleteAsset: (assetId: string) => void;
}) {
  const activeAsset =
    assets.find((asset) => asset.id === activeAssetId) ?? null;

  return (
    <div className="min-h-0 space-y-3">
      <div className="flex flex-wrap justify-end gap-2">
        <PlatformActionButton
          onClick={onRegenerateBatch}
          tone="ghost"
          size="xs"
          className="min-h-10 gap-2"
        >
          <Wand2 className="h-4 w-4" />
          批量重新生成
        </PlatformActionButton>
        <PlatformActionButton
          onClick={onAddBatch}
          tone="ghost"
          size="xs"
          className="min-h-10 gap-2"
        >
          <Plus className="h-4 w-4" />
          批量新增
        </PlatformActionButton>
      </div>
      <Match3DBatchGenerationProgress generationState={batchGenerationState} />
      <section
        className="grid grid-cols-2 gap-2.5 sm:grid-cols-3 lg:grid-cols-4"
        aria-label="抓大鹅 2D 素材列表"
      >
        {assets.map((asset) => (
          <Match3DItemAssetListCard
            key={asset.id}
            asset={asset}
            active={asset.id === activeAssetId}
            onClick={() => onActiveAssetChange(asset.id)}
            onDelete={() => onDeleteAsset(asset.id)}
          />
        ))}
      </section>

      {activeAsset ? (
        <Match3DModalShell
          title={activeAsset.name || '物品素材'}
          onClose={() => onActiveAssetChange(null)}
        >
          <Match3DItemAssetDetail
            asset={activeAsset}
            busy={false}
            onChange={onAssetChange}
          />
        </Match3DModalShell>
      ) : null}
    </div>
  );
}

function Match3DBatchAddItemsPanel({
  values,
  generationState,
  error,
  onAddInput,
  onChangeValue,
  onClose,
  onSubmit,
}: {
  values: string[];
  generationState: Match3DBatchItemGenerationState;
  error: string | null;
  onAddInput: () => void;
  onChangeValue: (index: number, value: string) => void;
  onClose: () => void;
  onSubmit: () => void;
}) {
  const parsedNames = normalizeMatch3DItemNameList(values);
  const isGenerating = generationState.phase === 'generating';
  const pointsCost = calculateMatch3DItemAssetsPointsCost(parsedNames.length);
  const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);

  return (
    <Match3DModalShell title="批量新增物品" onClose={onClose}>
      <div className="space-y-4">
        <div className="space-y-2">
          {values.map((value, index) => (
            <label key={index} className="block">
              <span className="sr-only">物品名称 {index + 1}</span>
              <PlatformTextField
                value={value}
                disabled={isGenerating}
                onChange={(event) => onChangeValue(index, event.target.value)}
                aria-label={`物品名称 ${index + 1}`}
                placeholder="物品名称"
              />
            </label>
          ))}
          <PlatformActionButton
            disabled={isGenerating}
            onClick={onAddInput}
            tone="ghost"
            size="xs"
            fullWidth
            className="min-h-10 gap-2"
          >
            <Plus className="h-4 w-4" />
            新增物品名称
          </PlatformActionButton>
        </div>

        <div className="flex flex-wrap gap-2">
          {parsedNames.map((name) => (
            <PlatformPillBadge key={name} tone="neutral" size="xs">
              {name}
            </PlatformPillBadge>
          ))}
        </div>

        {error ? (
          <PlatformStatusMessage tone="error" surface="platform" size="md">
            {error}
          </PlatformStatusMessage>
        ) : null}

        <Match3DBatchGenerationProgress generationState={generationState} />

        <PlatformActionButton
          disabled={parsedNames.length <= 0 || isGenerating}
          onClick={() => setIsCostConfirmOpen(true)}
          size="md"
          fullWidth
          className="min-h-11 gap-2"
        >
          {isGenerating ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : (
            <Plus className="h-4 w-4" />
          )}
          生成物品素材 · {pointsCost}泥点
        </PlatformActionButton>

        <UnifiedConfirmDialog
          open={isCostConfirmOpen}
          title="确认消耗泥点"
          onClose={() => setIsCostConfirmOpen(false)}
          onConfirm={() => {
            setIsCostConfirmOpen(false);
            onSubmit();
          }}
          confirmLabel="确定"
          confirmDisabled={parsedNames.length <= 0 || isGenerating}
          showCancel
          showCloseButton={false}
          portal={false}
          overlayClassName="platform-modal-backdrop z-[90]"
          panelClassName="platform-remap-surface max-w-xs rounded-[1.35rem] shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
        >
          <div className="font-semibold">消耗 {pointsCost} 泥点</div>
        </UnifiedConfirmDialog>
      </div>
    </Match3DModalShell>
  );
}

function Match3DBatchRegenerateItemsPanel({
  values,
  targetItemNames,
  generationState,
  error,
  onChangeValue,
  onClose,
  onSubmit,
}: {
  values: string[];
  targetItemNames: string[];
  generationState: Match3DBatchItemGenerationState;
  error: string | null;
  onChangeValue: (index: number, value: string) => void;
  onClose: () => void;
  onSubmit: () => void;
}) {
  const isGenerating = generationState.phase === 'generating';
  const pointsCost = calculateMatch3DItemAssetsPointsCost(
    targetItemNames.length,
  );
  const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);

  return (
    <Match3DModalShell title="批量重新生成物品" onClose={onClose}>
      <div className="space-y-4">
        <div className="grid gap-2 sm:grid-cols-2">
          {values.map((value, index) => (
            <label key={index} className="block">
              <span className="sr-only">重新生成物品名称 {index + 1}</span>
              <PlatformTextField
                value={value}
                disabled={isGenerating}
                onChange={(event) => onChangeValue(index, event.target.value)}
                aria-label={`重新生成物品名称 ${index + 1}`}
                placeholder="物品名称"
              />
            </label>
          ))}
        </div>

        <div className="flex flex-wrap gap-2">
          {targetItemNames.map((name) => (
            <PlatformPillBadge key={name} tone="neutral" size="xs">
              {name}
            </PlatformPillBadge>
          ))}
        </div>

        {error ? (
          <PlatformStatusMessage tone="error" surface="platform" size="md">
            {error}
          </PlatformStatusMessage>
        ) : null}

        <Match3DBatchGenerationProgress generationState={generationState} />

        <PlatformActionButton
          disabled={targetItemNames.length <= 0 || isGenerating}
          onClick={() => setIsCostConfirmOpen(true)}
          size="md"
          fullWidth
          className="min-h-11 gap-2"
        >
          {isGenerating ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : (
            <Wand2 className="h-4 w-4" />
          )}
          重新生成物品素材 · {pointsCost}泥点
        </PlatformActionButton>

        <UnifiedConfirmDialog
          open={isCostConfirmOpen}
          title="确认消耗泥点"
          onClose={() => setIsCostConfirmOpen(false)}
          onConfirm={() => {
            setIsCostConfirmOpen(false);
            onSubmit();
          }}
          confirmLabel="确定"
          confirmDisabled={targetItemNames.length <= 0 || isGenerating}
          showCancel
          showCloseButton={false}
          portal={false}
          overlayClassName="platform-modal-backdrop z-[90]"
          panelClassName="platform-remap-surface max-w-xs rounded-[1.35rem] shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
        >
          <div className="font-semibold">消耗 {pointsCost} 泥点</div>
        </UnifiedConfirmDialog>
      </div>
    </Match3DModalShell>
  );
}

function Match3DAssetConfigTabs({
  activeTab,
  onChange,
}: {
  activeTab: Match3DAssetConfigTab;
  onChange: (tab: Match3DAssetConfigTab) => void;
}) {
  return (
    <PlatformSegmentedTabs
      items={MATCH3D_ASSET_CONFIG_TABS}
      activeId={activeTab}
      onChange={onChange}
      radius="lg"
      size="sm"
      surface="soft"
      className="mb-3"
    />
  );
}

function Match3DUIAssetsTab({
  backgroundPreviewSrc,
  uiSpritesheetPreviewSrc,
  itemSpritesheetPreviewSrc,
  itemNames,
  error,
}: {
  backgroundPreviewSrc: string;
  uiSpritesheetPreviewSrc: string;
  itemSpritesheetPreviewSrc: string;
  itemNames: readonly string[];
  error: string | null;
}) {
  const [isPreviewOpen, setIsPreviewOpen] = useState(false);
  const [itemSpritesheetGroups, setItemSpritesheetGroups] = useState<
    Match3DItemSpritesheetPreviewGroup[]
  >([]);

  useEffect(() => {
    if (!itemSpritesheetPreviewSrc) {
      setItemSpritesheetGroups((current) =>
        current.length > 0 ? [] : current,
      );
      return undefined;
    }

    let cancelled = false;
    const controller = new AbortController();
    void loadMatch3DSpritesheetAssetRegions({
      source: itemSpritesheetPreviewSrc,
      maxRegions: 100,
      minArea: 16,
      alphaThreshold: 8,
      signal: controller.signal,
    })
      .then((regions) => {
        if (!cancelled) {
          setItemSpritesheetGroups(
            buildMatch3DItemSpritesheetViewRegions(regions, itemNames),
          );
        }
      })
      .catch(() => {
        if (!cancelled) {
          setItemSpritesheetGroups([]);
        }
      });

    return () => {
      cancelled = true;
      controller.abort();
    };
  }, [itemNames, itemSpritesheetPreviewSrc]);

  return (
    <div className="space-y-3">
      <PlatformSubpanel radius="lg" padding="lg">
        <div className="grid gap-4 sm:grid-cols-2">
          <button
            type="button"
            onClick={() => setIsPreviewOpen(true)}
            className="mx-auto aspect-[9/16] max-h-[min(62dvh,34rem)] w-full max-w-[18rem] overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 text-left shadow-sm"
            aria-label="打开UI页面预览"
          >
            <PlatformMediaFrame
              src={backgroundPreviewSrc}
              alt="游戏背景图"
              fallbackLabel="游戏背景图"
              fallbackContent={
                <ImageIcon className="h-8 w-8 text-[var(--platform-text-soft)]" />
              }
              aspect="portrait"
              surface="none"
              className="h-full w-full rounded-none"
            />
            <span className="sr-only">打开UI页面预览</span>
          </button>

          <div className="flex min-h-0 flex-col gap-3">
            <div className="aspect-square overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-3 shadow-sm">
              <PlatformMediaFrame
                src={uiSpritesheetPreviewSrc}
                alt="UI素材图"
                fallbackLabel="UI素材图"
                fallbackContent={
                  <ImageIcon className="h-8 w-8 text-[var(--platform-text-soft)]" />
                }
                aspect="square"
                surface="none"
                imageClassName="h-full w-full object-contain"
                className="h-full w-full rounded-none"
              />
            </div>
            <PlatformActionButton
              onClick={() => setIsPreviewOpen(true)}
              tone="ghost"
              size="md"
              className="min-h-11 gap-2"
            >
              <Eye className="h-4 w-4" />
              预览UI页面
            </PlatformActionButton>
          </div>
        </div>
      </PlatformSubpanel>

      {itemSpritesheetPreviewSrc ? (
        <PlatformSubpanel radius="lg" padding="lg">
          <div className="grid gap-4 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]">
            <div className="aspect-square overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-3 shadow-sm">
              <PlatformMediaFrame
                src={itemSpritesheetPreviewSrc}
                alt="物品素材图"
                fallbackLabel="物品素材图"
                fallbackContent={
                  <ImageIcon className="h-8 w-8 text-[var(--platform-text-soft)]" />
                }
                aspect="square"
                surface="none"
                imageClassName="h-full w-full object-contain"
                className="h-full w-full rounded-none"
              />
            </div>
            {itemSpritesheetGroups.length > 0 ? (
              <div className="grid max-h-[24rem] content-start gap-3 overflow-y-auto pr-1 sm:grid-cols-2 xl:grid-cols-3">
                {itemSpritesheetGroups.map((group) => (
                  <PlatformSubpanel
                    key={`${group.itemIndex}-${group.itemName}`}
                    as="div"
                    surface="flat"
                    radius="sm"
                    padding="sm"
                    className="bg-white/58"
                  >
                    <div className="mb-2 truncate text-xs font-black text-[var(--platform-text-strong)]">
                      {group.itemName}
                    </div>
                    <PlatformMediaTileGrid
                      columns="five"
                      gap="sm"
                      tileSurface="bare"
                      imageClassName="h-full w-full object-contain p-1"
                      tileClassName="rounded-[0.55rem] border-white/70 bg-white/82"
                      items={group.regions.map((region, regionIndex) => ({
                        id: `${group.itemIndex}-${regionIndex}-${region.imageSrc}`,
                        src: region.imageSrc,
                        testId: `match3d-item-spritesheet-preview-${group.itemIndex}-${regionIndex}`,
                        fallbackLabel: group.itemName,
                      }))}
                    />
                  </PlatformSubpanel>
                ))}
              </div>
            ) : null}
          </div>
        </PlatformSubpanel>
      ) : null}

      {error ? (
        <PlatformStatusMessage tone="error" surface="platform" size="md">
          {error}
        </PlatformStatusMessage>
      ) : null}

      {isPreviewOpen ? (
        <Match3DUIRuntimePreviewPanel
          backgroundPreviewSrc={backgroundPreviewSrc}
          containerPreviewSrc={MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC}
          onClose={() => setIsPreviewOpen(false)}
        />
      ) : null}
    </div>
  );
}

function Match3DUIRuntimePreviewPanel({
  backgroundPreviewSrc,
  containerPreviewSrc,
  onClose,
}: {
  backgroundPreviewSrc: string;
  containerPreviewSrc: string;
  onClose: () => void;
}) {
  return (
    <Match3DModalShell title="UI预览" onClose={onClose}>
      <div className="mx-auto aspect-[9/16] max-h-[min(78dvh,44rem)] w-full max-w-[24rem] overflow-hidden rounded-[1.4rem] border border-white/18 bg-[#16221f] shadow-[0_18px_55px_rgba(15,23,42,0.24)]">
        <div className="relative flex h-full w-full flex-col overflow-hidden px-3 pb-4 pt-3 text-white">
          <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_12%,rgba(255,255,255,0.22),transparent_26%),linear-gradient(180deg,#b8e28d_0%,#377569_52%,#14201f_100%)]" />
          <ResolvedAssetImage
            src={backgroundPreviewSrc}
            alt=""
            aria-hidden="true"
            className="pointer-events-none absolute inset-0 h-full w-full object-cover"
          />
          <header className="relative z-10 grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-start gap-2 sm:grid-cols-[2.75rem_minmax(0,1fr)_2.75rem]">
            <span className={MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS}>
              <ArrowLeft size={20} />
            </span>
            <span className={`${MATCH3D_RUNTIME_HEADER_CARD_CLASS} mx-auto`}>
              <span className="flex max-w-full items-center justify-center gap-1.5">
                <span className={MATCH3D_RUNTIME_LEVEL_BADGE_CLASS}>
                  第 1 关
                </span>
                <span className="min-w-0 truncate text-sm font-black sm:text-base">
                  抓大鹅
                </span>
              </span>
              <span className={MATCH3D_RUNTIME_TIMER_CLASS}>1:30</span>
            </span>
            <span className={MATCH3D_RUNTIME_GLASS_ICON_BUTTON_CLASS}>
              <Settings size={18} />
            </span>
          </header>

          <section className={`z-10 ${MATCH3D_RUNTIME_STAGE_CLASS}`}>
            <div
              className={`${MATCH3D_RUNTIME_BOARD_BASE_CLASS} ${
                containerPreviewSrc
                  ? MATCH3D_RUNTIME_BOARD_WITH_CONTAINER_CLASS
                  : MATCH3D_RUNTIME_BOARD_FALLBACK_CLASS
              }`}
              style={{ width: MATCH3D_RUNTIME_BOARD_WIDTH }}
              aria-hidden="true"
              data-testid="match3d-ui-preview-board"
            >
              {containerPreviewSrc ? (
                <ResolvedAssetImage
                  src={containerPreviewSrc}
                  alt=""
                  aria-hidden="true"
                  className={MATCH3D_RUNTIME_CONTAINER_IMAGE_CLASS}
                />
              ) : (
                <div className={MATCH3D_RUNTIME_CONTAINER_PLACEHOLDER_CLASS} />
              )}
            </div>
          </section>

          <section
            className={`relative z-10 ${MATCH3D_RUNTIME_GLASS_TRAY_CLASS}`}
          >
            <div className="grid grid-cols-7 gap-1.5">
              {Array.from({ length: 7 }).map((_, index) => (
                <span
                  key={index}
                  className={MATCH3D_RUNTIME_GLASS_TRAY_SLOT_CLASS}
                />
              ))}
            </div>
          </section>
        </div>
      </div>
    </Match3DModalShell>
  );
}

function Match3DAssetConfigTab({
  activeAssetConfigTab,
  activeAssetId,
  assetDrafts,
  backgroundPreviewSrc,
  uiSpritesheetPreviewSrc,
  itemSpritesheetPreviewSrc,
  itemNames,
  backgroundGenerationError,
  batchGenerationState,
  onActiveAssetChange,
  onAddBatch,
  onRegenerateBatch,
  onAssetChange,
  onAssetConfigTabChange,
  onDeleteAsset,
}: {
  activeAssetConfigTab: Match3DAssetConfigTab;
  activeAssetId: string | null;
  assetDrafts: Match3DItemAssetDraft[];
  backgroundPreviewSrc: string;
  uiSpritesheetPreviewSrc: string;
  itemSpritesheetPreviewSrc: string;
  itemNames: readonly string[];
  backgroundGenerationError: string | null;
  batchGenerationState: Match3DBatchItemGenerationState;
  onActiveAssetChange: (assetId: string | null) => void;
  onAddBatch: () => void;
  onRegenerateBatch: () => void;
  onAssetChange: (asset: Match3DItemAssetDraft) => void;
  onAssetConfigTabChange: (tab: Match3DAssetConfigTab) => void;
  onDeleteAsset: (assetId: string) => void;
}) {
  return (
    <div className="min-h-0">
      <Match3DAssetConfigTabs
        activeTab={activeAssetConfigTab}
        onChange={onAssetConfigTabChange}
      />
      {activeAssetConfigTab === 'items' ? (
        <Match3DAssetsTab
          activeAssetId={activeAssetId}
          assets={assetDrafts}
          batchGenerationState={batchGenerationState}
          onActiveAssetChange={onActiveAssetChange}
          onAddBatch={onAddBatch}
          onRegenerateBatch={onRegenerateBatch}
          onAssetChange={onAssetChange}
          onDeleteAsset={onDeleteAsset}
        />
      ) : null}
      {activeAssetConfigTab === 'ui' ? (
        <Match3DUIAssetsTab
          backgroundPreviewSrc={backgroundPreviewSrc}
          uiSpritesheetPreviewSrc={uiSpritesheetPreviewSrc}
          itemSpritesheetPreviewSrc={itemSpritesheetPreviewSrc}
          itemNames={itemNames}
          error={backgroundGenerationError}
        />
      ) : null}
    </div>
  );
}

export function Match3DResultView({
  profile,
  draft = null,
  isBusy = false,
  error = null,
  onBack,
  onSaved,
  onPublished,
  onStartTestRun,
}: Match3DResultViewProps) {
  const promotedProfile = useMemo(
    () => promoteMatch3DGeneratedBackgroundAsset(profile),
    [profile],
  );
  const [editState, setEditState] = useState(() => createEditState(profile));
  const [activeTab, setActiveTab] = useState<Match3DResultTab>('work');
  const [activeAssetConfigTab, setActiveAssetConfigTab] =
    useState<Match3DAssetConfigTab>('items');
  const [assetDrafts, setAssetDrafts] = useState<Match3DItemAssetDraft[]>(() =>
    createMatch3DAssetDrafts(profile, draft),
  );
  const [activeAssetId, setActiveAssetId] = useState<string | null>(null);
  const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false);
  const [hasAttemptedPublish, setHasAttemptedPublish] = useState(false);
  const [coverUploadedImageSrc, setCoverUploadedImageSrc] = useState('');
  const [coverReferenceImages, setCoverReferenceImages] = useState<
    Match3DCoverReferenceDraft[]
  >([]);
  const [coverPrompt, setCoverPrompt] = useState('');
  const [coverAiRedraw, setCoverAiRedraw] = useState(false);
  const [isGeneratingCover, setIsGeneratingCover] = useState(false);
  const [coverPanelError, setCoverPanelError] = useState<string | null>(null);
  const [isBatchAddPanelOpen, setIsBatchAddPanelOpen] = useState(false);
  const [isBatchRegeneratePanelOpen, setIsBatchRegeneratePanelOpen] =
    useState(false);
  const [batchItemNameValues, setBatchItemNameValues] = useState(['']);
  const [batchRegenerateItemNameValues, setBatchRegenerateItemNameValues] =
    useState<string[]>([]);
  const [batchGenerationState, setBatchGenerationState] =
    useState<Match3DBatchItemGenerationState>({
      phase: 'idle',
      progress: null,
      itemNames: [],
      message: null,
      error: null,
    });
  const [batchAddError, setBatchAddError] = useState<string | null>(null);
  const [batchRegenerateError, setBatchRegenerateError] = useState<
    string | null
  >(null);
  const [backgroundGenerationError, setBackgroundGenerationError] = useState<
    string | null
  >(null);
  const [autoSaveState, setAutoSaveState] =
    useState<Match3DAutoSaveState>('idle');
  const [localError, setLocalError] = useState<string | null>(null);
  const [isPublishing, setIsPublishing] = useState(false);
  const [isStartingTestRun, setIsStartingTestRun] = useState(false);
  const [isGeneratingTags, setIsGeneratingTags] = useState(false);
  const generatedItemAssets = useMemo(
    () => resolveMatch3DResultGeneratedItemAssets(promotedProfile, draft),
    [draft, promotedProfile],
  );
  const blockers = useMemo(
    () => buildPublishBlockers(editState, generatedItemAssets),
    [editState, generatedItemAssets],
  );
  const testRunBlockers = useMemo(
    () => buildTestRunBlockers(editState),
    [editState],
  );
  const canStartTestRun = testRunBlockers.length === 0;
  const canSubmit = blockers.length === 0;
  const totalItemCount =
    (normalizePositiveInteger(editState.clearCountText) ??
      promotedProfile.clearCount) * 3;
  const backgroundPreviewSrc = useMemo(
    () =>
      resolveMatch3DBackgroundPreviewSource(
        promotedProfile,
        draft,
        generatedItemAssets,
      ),
    [draft, generatedItemAssets, promotedProfile],
  );
  const containerPreviewSrc = useMemo(
    () =>
      resolveMatch3DContainerPreviewSource(
        promotedProfile,
        draft,
        generatedItemAssets,
      ) || MATCH3D_CONTAINER_REFERENCE_PREVIEW_SRC,
    [draft, generatedItemAssets, promotedProfile],
  );
  const uiSpritesheetPreviewSrc = useMemo(
    () =>
      resolveMatch3DUiSpritesheetPreviewSource(
        promotedProfile,
        draft,
        generatedItemAssets,
      ),
    [draft, generatedItemAssets, promotedProfile],
  );
  const itemSpritesheetPreviewSrc = useMemo(
    () =>
      resolveMatch3DItemSpritesheetPreviewSource(
        promotedProfile,
        draft,
        generatedItemAssets,
      ),
    [draft, generatedItemAssets, promotedProfile],
  );
  const generatedItemNames = useMemo(
    () => generatedItemAssets.map((asset) => asset.itemName),
    [generatedItemAssets],
  );
  const coverSourceAssets = useMemo(
    () =>
      resolveMatch3DCoverSourceAssets(
        assetDrafts,
        backgroundPreviewSrc,
        containerPreviewSrc,
      ),
    [assetDrafts, backgroundPreviewSrc, containerPreviewSrc],
  );

  useEffect(() => {
    setEditState(createEditState(profile));
    setAutoSaveState('idle');
    setLocalError(null);
    setIsPublishDialogOpen(false);
    setHasAttemptedPublish(false);
    setCoverUploadedImageSrc('');
    setCoverReferenceImages([]);
    setCoverPrompt('');
    setCoverAiRedraw(false);
    setCoverPanelError(null);
    setBackgroundGenerationError(null);
    // 中文注释：表单草稿只在切换作品或服务端更新时间变化时重置，避免父级重渲染打断本地编辑。
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [profile.profileId, profile.updatedAt]);

  useEffect(() => {
    setAssetDrafts(createMatch3DAssetDrafts(profile, draft));
    setActiveAssetId(null);
    // 中文注释：素材草稿只跟随持久化素材字段和作品切换重建，避免无关 profile 字段刷新关闭当前面板。
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    draft?.generatedItemAssets,
    profile.generatedItemAssets,
    profile.profileId,
  ]);

  useEffect(() => {
    const payload = buildSavePayload(editState);
    if (!payload) {
      return undefined;
    }

    const currentTags = normalizeTags(profile.tags.join('，'));
    const nextTags = payload.tags;
    const changed =
      payload.gameName !== profile.gameName ||
      payload.themeText !== profile.themeText ||
      payload.summary !== profile.summary ||
      (payload.coverImageSrc ?? '') !== (profile.coverImageSrc ?? '') ||
      payload.clearCount !== profile.clearCount ||
      payload.difficulty !== profile.difficulty ||
      nextTags.length !== currentTags.length ||
      nextTags.some((tag, index) => tag !== currentTags[index]);

    if (!changed) {
      return undefined;
    }

    setAutoSaveState('saving');
    setLocalError(null);
    let cancelled = false;
    const timer = window.setTimeout(() => {
      void updateMatch3DWork(profile.profileId, payload)
        .then(({ item }) => {
          if (cancelled) {
            return;
          }
          const playableItem = attachMatch3DGeneratedItemAssets(
            item,
            generatedItemAssets,
          );
          setAutoSaveState('saved');
          onSaved?.(playableItem);
        })
        .catch((saveError) => {
          if (cancelled) {
            return;
          }
          setAutoSaveState('error');
          setLocalError(
            saveError instanceof Error ? saveError.message : '自动保存失败。',
          );
        });
    }, MATCH3D_AUTOSAVE_DEBOUNCE_MS);

    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [editState, generatedItemAssets, onSaved, profile]);

  const saveNow = async () => {
    const payload = buildSavePayload(editState);
    if (!payload) {
      setLocalError(testRunBlockers[0] ?? '请补全作品信息。');
      return null;
    }

    setAutoSaveState('saving');
    setLocalError(null);
    const { item } = await updateMatch3DWork(profile.profileId, payload);
    const currentGeneratedItemAssets = buildPersistableGeneratedItemAssets(
      assetDrafts,
      generatedItemAssets,
    );
    let playableItem = attachMatch3DGeneratedItemAssets(
      item,
      currentGeneratedItemAssets.length > 0
        ? currentGeneratedItemAssets
        : generatedItemAssets,
    );
    if (
      shouldPersistGeneratedItemAssets(
        currentGeneratedItemAssets,
        item.generatedItemAssets ?? [],
      )
    ) {
      // 中文注释：试玩和发布前必须先把当前可见 2D 多视角素材写回 profile。
      const { item: persistedItem } = await updateMatch3DGeneratedItemAssets(
        profile.profileId,
        {
          generatedItemAssets: currentGeneratedItemAssets,
        },
      );
      playableItem = attachMatch3DGeneratedItemAssets(
        persistedItem,
        currentGeneratedItemAssets,
      );
    }
    setAutoSaveState('saved');
    onSaved?.(playableItem);
    return playableItem;
  };

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

    try {
      const dataUrl = await readImageAsDataUrl(file);
      setCoverUploadedImageSrc(dataUrl);
      setCoverAiRedraw(true);
      setCoverPanelError(null);
    } catch (caughtError) {
      setCoverPanelError(
        caughtError instanceof Error ? caughtError.message : '封面图读取失败。',
      );
    }
  };

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

    try {
      const dataUrl = await readCoverReferenceImageAsDataUrl(file);
      setCoverReferenceImages((current) =>
        addMatch3DCoverReferenceDraft(current, {
          id: `upload:${Date.now()}:${file.name}`,
          label: file.name.trim() || '自定义参考图',
          imageSrc: dataUrl,
          source: 'upload',
        }),
      );
      setCoverPanelError(null);
    } catch (caughtError) {
      setCoverPanelError(
        caughtError instanceof Error ? caughtError.message : '参考图读取失败。',
      );
    }
  };

  const resetCoverEditor = () => {
    setCoverUploadedImageSrc('');
    setCoverReferenceImages([]);
    setCoverPrompt(
      [
        editState.gameName.trim(),
        editState.themeText.trim(),
        '抓大鹅作品封面图，主体清晰，适合作品卡片',
      ]
        .filter(Boolean)
        .join('，'),
    );
    setCoverAiRedraw(true);
    setCoverPanelError(null);
  };

  const openPublishDialog = () => {
    setHasAttemptedPublish(false);
    resetCoverEditor();
    setIsPublishDialogOpen(true);
  };

  const closePublishDialog = () => {
    if (isGeneratingCover || isPublishing) {
      return;
    }
    setHasAttemptedPublish(false);
    setIsPublishDialogOpen(false);
  };

  const handleSubmitCoverPanel = async () => {
    const uploadedImageSrc = coverUploadedImageSrc.trim();
    const prompt = coverPrompt.trim();
    const referenceImageSrcs = coverReferenceImages
      .map((reference) => reference.imageSrc.trim())
      .filter(Boolean);
    if (!uploadedImageSrc && !prompt) {
      setCoverPanelError('请上传图片或填写封面描述。');
      return;
    }
    if (uploadedImageSrc && !coverAiRedraw) {
      setEditState((current) => ({
        ...current,
        coverImageSrc: uploadedImageSrc,
      }));
      setCoverPanelError(null);
      setCoverUploadedImageSrc('');
      setCoverAiRedraw(true);
      return;
    }

    setIsGeneratingCover(true);
    setCoverPanelError(null);
    try {
      const response = await generateMatch3DCoverImage(profile.profileId, {
        prompt:
          prompt ||
          `${editState.gameName.trim() || '抓大鹅'}作品封面图，${editState.themeText.trim() || '休闲消除'}题材`,
        uploadedImageSrc: uploadedImageSrc || null,
        referenceImageSrcs: uploadedImageSrc ? [] : referenceImageSrcs,
      });
      setEditState((current) => ({
        ...current,
        coverImageSrc: response.coverImageSrc,
      }));
      onSaved?.(
        buildCoverImageUpdatedProfile(
          profile,
          editState,
          response.item,
          response.coverImageSrc,
          generatedItemAssets,
        ),
      );
      setCoverUploadedImageSrc('');
      setCoverReferenceImages([]);
      setCoverAiRedraw(true);
    } catch (caughtError) {
      setCoverPanelError(
        caughtError instanceof Error ? caughtError.message : '封面图生成失败。',
      );
    } finally {
      setIsGeneratingCover(false);
    }
  };

  const handleGenerateTags = async () => {
    const gameName = editState.gameName.trim();
    const themeText = editState.themeText.trim();
    if (!gameName || !themeText) {
      setLocalError('请先补齐作品名称和题材主题。');
      return;
    }

    setIsGeneratingTags(true);
    try {
      const response = await generateMatch3DWorkTags({
        gameName,
        themeText,
        summary: editState.summary.trim(),
      });
      const nextTags = normalizeTags(response.tags.join('，'));
      if (nextTags.length <= 0) {
        throw new Error('未生成有效标签。');
      }
      setEditState((current) => ({
        ...current,
        tagsText: nextTags.join('，'),
      }));
      setLocalError(null);
    } catch (caughtError) {
      setLocalError(
        caughtError instanceof Error
          ? caughtError.message
          : 'AI 生成标签失败。',
      );
    } finally {
      setIsGeneratingTags(false);
    }
  };

  const updateItemAsset = (nextAsset: Match3DItemAssetDraft) => {
    setAssetDrafts((currentAssets) =>
      currentAssets.map((asset) =>
        asset.id === nextAsset.id ? nextAsset : asset,
      ),
    );
  };

  const persistGeneratedAssetDrafts = async (
    nextDrafts: Match3DItemAssetDraft[],
  ) => {
    const nextAssets = createGeneratedAssetsFromDrafts(
      nextDrafts,
      profile.generatedItemAssets ?? [],
    );
    const { item } = await updateMatch3DGeneratedItemAssets(profile.profileId, {
      generatedItemAssets: nextAssets,
    });
    onSaved?.(item);
    return item;
  };

  const handleDeleteAssetDraft = async (assetId: string) => {
    if (busy) {
      return;
    }
    const nextDrafts = assetDrafts.filter((asset) => asset.id !== assetId);
    setAssetDrafts(nextDrafts);
    if (activeAssetId === assetId) {
      setActiveAssetId(null);
    }
    try {
      await persistGeneratedAssetDrafts(nextDrafts);
      setLocalError(null);
    } catch (caughtError) {
      setAssetDrafts(assetDrafts);
      setLocalError(
        caughtError instanceof Error
          ? caughtError.message
          : '删除物品素材失败。',
      );
    }
  };

  const handleSubmitBatchAddItems = () => {
    const itemNames = normalizeMatch3DItemNameList(batchItemNameValues);
    if (itemNames.length <= 0 || batchGenerationState.phase === 'generating') {
      setBatchAddError('请填写至少一个物品名称。');
      return;
    }

    setBatchAddError(null);
    setBatchGenerationState({
      phase: 'generating',
      progress: 0.08,
      itemNames,
      message: `正在生成 ${itemNames.length} 种物品素材`,
      error: null,
    });

    let progressTick = 0;
    const progressTimer = window.setInterval(() => {
      progressTick += 1;
      setBatchGenerationState((current) => {
        if (current.phase !== 'generating') {
          return current;
        }
        const currentProgress = current.progress ?? 0.08;
        const ceiling = progressTick < 4 ? 0.42 : 0.88;
        return {
          ...current,
          progress: Math.min(ceiling, currentProgress + 0.08),
        };
      });
    }, 1200);

    void generateMatch3DItemAssets(profile.profileId, { itemNames })
      .then((response) => {
        window.clearInterval(progressTimer);
        const refreshedProfile = attachMatch3DGeneratedItemAssets(
          response.item,
          response.generatedItemAssets,
        );
        setAssetDrafts(createMatch3DAssetDrafts(refreshedProfile, null));
        setBatchItemNameValues(['']);
        setBatchGenerationState({
          phase: 'done',
          progress: 1,
          itemNames,
          message: `已生成 ${itemNames.length} 种物品素材`,
          error: null,
        });
        setLocalError(null);
        onSaved?.(refreshedProfile);
      })
      .catch((caughtError) => {
        window.clearInterval(progressTimer);
        const message =
          caughtError instanceof Error
            ? caughtError.message
            : '批量新增物品素材失败。';
        setBatchAddError(message);
        setBatchGenerationState({
          phase: 'failed',
          progress: null,
          itemNames,
          message: null,
          error: message,
        });
      });
  };

  const handleOpenBatchRegeneratePanel = () => {
    setBatchRegenerateError(null);
    setBatchRegenerateItemNameValues(
      assetDrafts.length > 0 ? assetDrafts.map((asset) => asset.name) : [''],
    );
    setBatchGenerationState((current) =>
      current.phase === 'generating'
        ? current
        : {
            phase: 'idle',
            progress: null,
            itemNames: [],
            message: null,
            error: null,
          },
    );
    setIsBatchRegeneratePanelOpen(true);
  };

  const handleSubmitBatchRegenerateItems = () => {
    const itemNames = collectMatch3DRegenerateItemNames(
      batchRegenerateItemNameValues,
      assetDrafts,
    );
    if (itemNames.length <= 0 || batchGenerationState.phase === 'generating') {
      setBatchRegenerateError('请填写至少一个物品名称。');
      return;
    }

    setBatchRegenerateError(null);
    setBatchGenerationState({
      phase: 'generating',
      progress: 0.08,
      itemNames,
      message: `正在重新生成 ${itemNames.length} 种物品素材`,
      error: null,
    });

    let progressTick = 0;
    const progressTimer = window.setInterval(() => {
      progressTick += 1;
      setBatchGenerationState((current) => {
        if (current.phase !== 'generating') {
          return current;
        }
        const currentProgress = current.progress ?? 0.08;
        const ceiling = progressTick < 4 ? 0.42 : 0.88;
        return {
          ...current,
          progress: Math.min(ceiling, currentProgress + 0.08),
        };
      });
    }, 1200);

    void generateMatch3DItemAssets(profile.profileId, {
      itemNames,
      mode: 'replace',
    })
      .then((response) => {
        window.clearInterval(progressTimer);
        const refreshedProfile = attachMatch3DGeneratedItemAssets(
          response.item,
          response.generatedItemAssets,
        );
        setAssetDrafts(createMatch3DAssetDrafts(refreshedProfile, null));
        setBatchRegenerateItemNameValues(
          response.generatedItemAssets.map((asset) => asset.itemName),
        );
        setBatchGenerationState({
          phase: 'done',
          progress: 1,
          itemNames,
          message: `已重新生成 ${itemNames.length} 种物品素材`,
          error: null,
        });
        setLocalError(null);
        onSaved?.(refreshedProfile);
      })
      .catch((caughtError) => {
        window.clearInterval(progressTimer);
        const message =
          caughtError instanceof Error
            ? caughtError.message
            : '批量重新生成物品素材失败。';
        setBatchRegenerateError(message);
        setBatchGenerationState({
          phase: 'failed',
          progress: null,
          itemNames,
          message: null,
          error: message,
        });
      });
  };

  const handleStartTestRun = async () => {
    if (!canStartTestRun || isStartingTestRun) {
      setLocalError(testRunBlockers[0] ?? null);
      return;
    }

    setIsStartingTestRun(true);
    try {
      const savedProfile = await saveNow();
      const playableProfile =
        savedProfile ??
        buildPlayableProfile(profile, editState, generatedItemAssets);
      const playableAssets =
        playableProfile.generatedItemAssets ?? generatedItemAssets;
      const targetItemTypeCount =
        getMatch3DDifficultyOptionFromEditState(editState).itemTypeCount;
      const playableItemTypeCount = getMatch3DPlayableItemTypeCount(
        targetItemTypeCount,
        playableAssets,
      );
      onStartTestRun(playableProfile, {
        itemTypeCountOverride: playableItemTypeCount,
      });
    } catch (caughtError) {
      setLocalError(
        caughtError instanceof Error
          ? caughtError.message
          : '启动试玩前保存失败。',
      );
    } finally {
      setIsStartingTestRun(false);
    }
  };

  const handlePublish = async () => {
    setHasAttemptedPublish(true);
    if (!canSubmit || isPublishing) {
      setLocalError(blockers[0] ?? null);
      return;
    }

    setIsPublishing(true);
    try {
      const savedProfile = await saveNow();
      const { item } = await publishMatch3DWork(
        savedProfile?.profileId ?? profile.profileId,
      );
      onPublished?.(
        attachMatch3DGeneratedItemAssets(
          item,
          savedProfile?.generatedItemAssets ?? generatedItemAssets,
        ),
      );
      setLocalError(null);
      setIsPublishDialogOpen(false);
      setHasAttemptedPublish(false);
    } catch (caughtError) {
      setLocalError(
        caughtError instanceof Error
          ? caughtError.message
          : '发布抓大鹅作品失败。',
      );
    } finally {
      setIsPublishing(false);
    }
  };

  const busy = isBusy || isPublishing || isStartingTestRun || isGeneratingCover;
  const workBusy = busy || isGeneratingTags;
  const displayError = error ?? localError;
  const dialogPublishError = hasAttemptedPublish ? (error ?? localError) : null;

  return (
    <div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full flex-col xl:max-w-[min(100%,88rem)]">
      <Match3DResultHeader
        autoSaveState={autoSaveState}
        isBusy={busy}
        onBack={onBack}
      />

      <Match3DResultTabs activeTab={activeTab} onChange={setActiveTab} />

      <div className="min-h-0 flex-1 overflow-y-auto pr-1">
        {activeTab === 'work' ? (
          <Match3DWorkInfoTab
            editState={editState}
            isBusy={workBusy}
            onChange={setEditState}
            onGenerateTags={handleGenerateTags}
          />
        ) : null}
        {activeTab === 'config' ? (
          <Match3DConfigTab
            editState={editState}
            generatedItemAssets={generatedItemAssets}
            isBusy={busy}
            totalItemCount={totalItemCount}
            onChange={setEditState}
          />
        ) : null}
        {activeTab === 'assets' ? (
          <Match3DAssetConfigTab
            activeAssetConfigTab={activeAssetConfigTab}
            activeAssetId={activeAssetId}
            assetDrafts={assetDrafts}
            backgroundPreviewSrc={backgroundPreviewSrc}
            uiSpritesheetPreviewSrc={uiSpritesheetPreviewSrc}
            itemSpritesheetPreviewSrc={itemSpritesheetPreviewSrc}
            itemNames={generatedItemNames}
            backgroundGenerationError={backgroundGenerationError}
            batchGenerationState={batchGenerationState}
            onActiveAssetChange={setActiveAssetId}
            onAddBatch={() => {
              setBatchAddError(null);
              setBatchGenerationState((current) =>
                current.phase === 'generating'
                  ? current
                  : {
                      phase: 'idle',
                      progress: null,
                      itemNames: [],
                      message: null,
                      error: null,
                    },
              );
              setIsBatchAddPanelOpen(true);
            }}
            onRegenerateBatch={handleOpenBatchRegeneratePanel}
            onAssetChange={updateItemAsset}
            onAssetConfigTabChange={setActiveAssetConfigTab}
            onDeleteAsset={(assetId) => {
              void handleDeleteAssetDraft(assetId);
            }}
          />
        ) : null}
      </div>

      {displayError ? (
        <PlatformStatusMessage
          tone="error"
          surface="platform"
          size="md"
          className="mt-3"
        >
          {displayError}
        </PlatformStatusMessage>
      ) : null}

      <div className="mt-3 flex flex-col gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:flex-row sm:justify-end">
        <PlatformActionButton
          onClick={handleStartTestRun}
          disabled={!canStartTestRun || busy}
          tone="ghost"
          size="md"
          className="min-h-11 gap-2"
        >
          {isStartingTestRun ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : (
            <Play className="h-4 w-4" />
          )}
          试玩
        </PlatformActionButton>
        <PlatformActionButton
          onClick={openPublishDialog}
          disabled={busy}
          size="md"
          className="min-h-11 gap-2"
        >
          {isPublishing ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : profile.publicationStatus === 'published' ? (
            <CheckCircle2 className="h-4 w-4" />
          ) : (
            <Send className="h-4 w-4" />
          )}
          {profile.publicationStatus === 'published' ? '更新发布' : '发布'}
        </PlatformActionButton>
      </div>

      {isPublishDialogOpen ? (
        <Match3DPublishDialog
          blockers={blockers}
          editState={editState}
          sourceAssets={coverSourceAssets}
          isBusy={busy}
          isGeneratingCover={isGeneratingCover}
          isPublishing={isPublishing}
          uploadedImageSrc={coverUploadedImageSrc}
          referenceImages={coverReferenceImages}
          aiRedraw={coverAiRedraw}
          prompt={coverPrompt}
          coverError={coverPanelError}
          publishError={dialogPublishError}
          onAiRedrawChange={setCoverAiRedraw}
          onClose={closePublishDialog}
          onFileChange={handleCoverImageChange}
          onPromptChange={setCoverPrompt}
          onPublish={() => {
            void handlePublish();
          }}
          onReferenceSelect={(source) => {
            const sourceAsset = coverSourceAssets.find(
              (asset) => asset.imageSrc === source,
            );
            if (!sourceAsset) {
              return;
            }
            setCoverReferenceImages((current) =>
              addMatch3DCoverReferenceDraft(
                current,
                createMatch3DCoverReferenceDraftFromSource(sourceAsset),
              ),
            );
            setCoverPanelError(null);
          }}
          onReferenceFileChange={handleCoverReferenceImageChange}
          onReferenceRemove={(referenceId) => {
            setCoverReferenceImages((current) =>
              current.filter((reference) => reference.id !== referenceId),
            );
          }}
          onUploadedImageRemove={() => {
            setCoverUploadedImageSrc('');
            setCoverAiRedraw(true);
          }}
          onSubmitCover={() => {
            void handleSubmitCoverPanel();
          }}
        />
      ) : null}

      {isBatchAddPanelOpen ? (
        <Match3DBatchAddItemsPanel
          values={batchItemNameValues}
          generationState={batchGenerationState}
          error={batchAddError}
          onAddInput={() => {
            setBatchItemNameValues((current) => [...current, '']);
          }}
          onChangeValue={(index, value) => {
            setBatchItemNameValues((current) =>
              current.map((item, itemIndex) =>
                itemIndex === index ? value : item,
              ),
            );
          }}
          onClose={() => {
            setIsBatchAddPanelOpen(false);
          }}
          onSubmit={() => {
            handleSubmitBatchAddItems();
          }}
        />
      ) : null}

      {isBatchRegeneratePanelOpen ? (
        <Match3DBatchRegenerateItemsPanel
          values={batchRegenerateItemNameValues}
          targetItemNames={collectMatch3DRegenerateItemNames(
            batchRegenerateItemNameValues,
            assetDrafts,
          )}
          generationState={batchGenerationState}
          error={batchRegenerateError}
          onChangeValue={(index, value) => {
            setBatchRegenerateItemNameValues((current) =>
              current.map((item, itemIndex) =>
                itemIndex === index ? value : item,
              ),
            );
          }}
          onClose={() => {
            setIsBatchRegeneratePanelOpen(false);
          }}
          onSubmit={() => {
            handleSubmitBatchRegenerateItems();
          }}
        />
      ) : null}
    </div>
  );
}

export default Match3DResultView;
