import {
  ArrowLeft,
  CheckCircle2,
  Eye,
  History,
  ImagePlus,
  LayoutTemplate,
  Loader2,
  MessageSquareText,
  Play,
  Plus,
  Sparkles,
  Trash2,
  Wand2,
  X,
} from 'lucide-react';
import { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import type { CreativeDraftEditResult } from '../../../packages/shared/src/contracts/creativeAgent';
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
  PuzzleDraftLevel,
  PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import { updatePuzzleWork } from '../../services/puzzle-works';
import { resolvePuzzleUiBackgroundSource } from '../../services/puzzle-runtime/puzzleUiBackgroundSource';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { useAuthUi } from '../auth/AuthUiContext';
import PuzzleHistoryAssetPickerDialog from '../puzzle-agent/PuzzleHistoryAssetPickerDialog';
import {
  PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
  type PuzzleImageModelId,
} from '../puzzle-agent/puzzleImageModelOptions';
import { PuzzleImageModelPicker } from '../puzzle-agent/PuzzleImageModelPicker';
import { ResolvedAssetImage } from '../ResolvedAssetImage';

type PuzzleResultViewProps = {
  session: PuzzleAgentSessionSnapshot;
  profileId?: string | null;
  isBusy?: boolean;
  error?: string | null;
  onBack: () => void;
  onExecuteAction: (payload: PuzzleAgentActionRequest) => void;
  onStartTestRun?: (draft: PuzzleResultDraft) => void;
  creativeDraftEdit?: {
    isBusy: boolean;
    error: string | null;
    onSubmit: (payload: {
      instruction: string;
      currentDraft: PuzzleResultDraft;
    }) => Promise<CreativeDraftEditResult | null> | void;
  } | null;
};

type PuzzleAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
type PuzzleResultTab = 'levels' | 'work' | 'assets';
type PuzzleAssetConfigTabId = 'ui';

type DraftEditState = {
  workTitle: string;
  workDescription: string;
  themeTags: string[];
  levels: PuzzleDraftLevel[];
};

const PUZZLE_MIN_THEME_TAG_COUNT = 3;
const PUZZLE_MAX_THEME_TAG_COUNT = 6;
const PUZZLE_AUTOSAVE_DEBOUNCE_MS = 600;
const PUZZLE_IMAGE_GENERATION_POINT_COST = 2;
const PUZZLE_PUBLISH_POINT_COST = 1;
const PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS = 90;
const PUZZLE_UI_BACKGROUND_REFERENCE_SRC =
  '/ui-previews/puzzle-image-compact-ui-2026-05-08.png';

const PUZZLE_RESULT_TABS: Array<{ id: PuzzleResultTab; label: string }> = [
  { id: 'levels', label: '拼图关卡' },
  { id: 'work', label: '作品信息' },
  { id: 'assets', label: '素材配置' },
];

const PUZZLE_ASSET_CONFIG_TABS: Array<{
  id: PuzzleAssetConfigTabId;
  label: string;
}> = [
  { id: 'ui', label: 'UI' },
];

type PuzzleLevelGenerationRuntime = {
  startedAtMs: number;
  estimateSeconds: number;
};

function resolvePuzzleLevelGenerationProgress(
  level: PuzzleDraftLevel,
  runtime: PuzzleLevelGenerationRuntime | null,
  nowMs: number,
) {
  if (level.generationStatus !== 'generating') {
    return {
      isGenerating: false,
      progressPercent: 0,
      secondsLeft: 0,
    };
  }

  const estimateSeconds =
    runtime?.estimateSeconds ?? PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS;
  const elapsedSeconds = runtime
    ? Math.max(0, Math.floor((nowMs - runtime.startedAtMs) / 1000))
    : 0;
  const secondsLeft = Math.max(0, estimateSeconds - elapsedSeconds);
  const progressPercent = Math.min(
    96,
    Math.max(
      6,
      Math.round(((estimateSeconds - secondsLeft) / estimateSeconds) * 100),
    ),
  );

  return {
    isGenerating: true,
    progressPercent,
    secondsLeft,
  };
}

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

function buildDefaultPuzzleUiBackgroundPrompt(
  editState: DraftEditState,
  level: PuzzleDraftLevel | null,
) {
  const tags = editState.themeTags
    .map((tag) => tag.trim())
    .filter(Boolean)
    .join('，');
  return [
    editState.workTitle.trim(),
    editState.workDescription.trim(),
    level?.levelName.trim(),
    level?.pictureDescription.trim(),
    tags,
    '移动端拼图游戏 UI 背景，中央正方形拼图区边界清晰，拼图区外氛围与作品名称一致',
  ]
    .filter(Boolean)
    .join('。');
}

function resolveLevelFormalImageSrc(level: PuzzleDraftLevel) {
  const selectedCandidate =
    level.candidates.find(
      (candidate) =>
        candidate.selected ||
        (level.selectedCandidateId
          ? candidate.candidateId === level.selectedCandidateId
          : false),
    ) ??
    level.candidates[level.candidates.length - 1] ??
    null;

  return (
    selectedCandidate?.imageSrc?.trim() || level.coverImageSrc?.trim() || ''
  );
}

function buildFallbackLevelFromDraft(
  draft: PuzzleResultDraft,
): PuzzleDraftLevel {
  return {
    levelId: 'puzzle-level-1',
    levelName: draft.levelName || '',
    pictureDescription: draft.summary,
    candidates: draft.candidates,
    selectedCandidateId: draft.selectedCandidateId,
    coverImageSrc: draft.coverImageSrc,
    coverAssetId: draft.coverAssetId,
    generationStatus: draft.generationStatus,
  };
}

function normalizeDraftLevels(draft: PuzzleResultDraft) {
  const sourceLevels =
    draft.levels && draft.levels.length > 0
      ? draft.levels
      : [buildFallbackLevelFromDraft(draft)];

  return sourceLevels.map((level, index) => ({
    ...level,
    levelId: level.levelId?.trim() || `puzzle-level-${index + 1}`,
    levelName: level.levelName?.trim() || '',
    pictureDescription: level.pictureDescription?.trim() || draft.summary,
    pictureReference: level.pictureReference ?? null,
    uiBackgroundPrompt: level.uiBackgroundPrompt ?? null,
    uiBackgroundImageSrc: level.uiBackgroundImageSrc ?? null,
    uiBackgroundImageObjectKey: level.uiBackgroundImageObjectKey ?? null,
    candidates: level.candidates ?? [],
    selectedCandidateId: level.selectedCandidateId ?? null,
    coverImageSrc: level.coverImageSrc ?? null,
    coverAssetId: level.coverAssetId ?? null,
    backgroundMusic: level.backgroundMusic ?? null,
    generationStatus: level.generationStatus || 'idle',
  }));
}

function syncDraftFromEditState(
  draft: PuzzleResultDraft,
  editState: DraftEditState,
): PuzzleResultDraft {
  const levels = editState.levels;
  const primaryLevel = levels[0] ?? buildFallbackLevelFromDraft(draft);
  return {
    ...draft,
    workTitle: editState.workTitle.trim(),
    workDescription: editState.workDescription.trim(),
    levelName: primaryLevel.levelName,
    summary: editState.workDescription.trim(),
    themeTags: editState.themeTags,
    candidates: primaryLevel.candidates,
    selectedCandidateId: primaryLevel.selectedCandidateId,
    coverImageSrc: primaryLevel.coverImageSrc,
    coverAssetId: primaryLevel.coverAssetId,
    generationStatus: primaryLevel.generationStatus,
    levels,
  };
}

function createDraftEditState(draft: PuzzleResultDraft): DraftEditState {
  return {
    workTitle: draft.workTitle ?? '',
    workDescription: draft.workDescription ?? '',
    themeTags: normalizeThemeTagInput(draft.themeTags.join('，')),
    levels: normalizeDraftLevels(draft),
  };
}

function mergeDraftEditStateWithIncomingState(
  currentState: DraftEditState | null,
  incomingState: DraftEditState,
): DraftEditState {
  if (!currentState) {
    return incomingState;
  }

  const incomingLevelsById = new Map(
    incomingState.levels.map((level) => [level.levelId, level]),
  );
  const shouldPreserveLocalEdits = currentState.levels.some((level) => {
    const incomingLevel = incomingLevelsById.get(level.levelId);
    return (
      level.generationStatus === 'generating' &&
      Boolean(incomingLevel) &&
      incomingLevel?.generationStatus !== 'generating'
    );
  });

  if (!shouldPreserveLocalEdits) {
    return incomingState;
  }

  const mergedLevels = currentState.levels.map((level) => {
    const incomingLevel = incomingLevelsById.get(level.levelId);
    if (
      !incomingLevel ||
      level.generationStatus !== 'generating' ||
      incomingLevel.generationStatus === 'generating'
    ) {
      return level;
    }

    return {
      ...level,
      candidates: incomingLevel.candidates,
      selectedCandidateId: incomingLevel.selectedCandidateId,
      coverImageSrc: incomingLevel.coverImageSrc,
      coverAssetId: incomingLevel.coverAssetId,
      pictureReference: incomingLevel.pictureReference ?? level.pictureReference,
      uiBackgroundPrompt:
        incomingLevel.uiBackgroundPrompt ?? level.uiBackgroundPrompt,
      uiBackgroundImageSrc:
        incomingLevel.uiBackgroundImageSrc ?? level.uiBackgroundImageSrc,
      uiBackgroundImageObjectKey:
        incomingLevel.uiBackgroundImageObjectKey ??
        level.uiBackgroundImageObjectKey,
      backgroundMusic: incomingLevel.backgroundMusic ?? level.backgroundMusic,
      generationStatus: incomingLevel.generationStatus || 'ready',
    };
  });
  const mergedLevelIds = new Set(mergedLevels.map((level) => level.levelId));
  const appendedIncomingLevels = incomingState.levels.filter(
    (level) => !mergedLevelIds.has(level.levelId),
  );

  return {
    ...currentState,
    levels: [...mergedLevels, ...appendedIncomingLevels],
  };
}

function createBlankPuzzleLevel(
  existingLevels: PuzzleDraftLevel[],
): PuzzleDraftLevel {
  const nextIndex = existingLevels.length + 1;
  return {
    levelId: `puzzle-level-${Date.now()}-${nextIndex}`,
    levelName: '',
    pictureDescription: '',
    pictureReference: null,
    uiBackgroundPrompt: null,
    uiBackgroundImageSrc: null,
    uiBackgroundImageObjectKey: null,
    candidates: [],
    selectedCandidateId: null,
    coverImageSrc: null,
    coverAssetId: null,
    backgroundMusic: null,
    generationStatus: 'idle',
  };
}

function buildPublishReady(
  session: PuzzleAgentSessionSnapshot,
  editState: DraftEditState,
) {
  const preservedBlockers =
    session.resultPreview?.blockers
      .filter(
        (entry) =>
          ![
            'MISSING_LEVEL_NAME',
            'INVALID_TAG_COUNT',
            'MISSING_COVER_IMAGE',
          ].includes(entry.code),
      )
      .map((entry) => entry.message) ?? [];
  const levels = editState.levels;
  const hasGeneratingLevel = levels.some(
    (level) => level.generationStatus === 'generating',
  );
  const blockers = [
    ...(session.resultPreview ? [] : ['等待结果页草稿完成后再发布。']),
    ...preservedBlockers,
    ...(editState.workTitle.trim() ? [] : ['作品名称不能为空。']),
    ...(editState.workDescription.trim() ? [] : ['作品描述不能为空。']),
    ...(editState.themeTags.length >= PUZZLE_MIN_THEME_TAG_COUNT &&
    editState.themeTags.length <= PUZZLE_MAX_THEME_TAG_COUNT
      ? []
      : [
          `正式标签数量必须在 ${PUZZLE_MIN_THEME_TAG_COUNT} 到 ${PUZZLE_MAX_THEME_TAG_COUNT} 之间。`,
        ]),
    ...(levels.length > 0 ? [] : ['至少需要一个拼图关卡。']),
    ...levels.flatMap((level, index) => [
      ...(level.levelName.trim() ? [] : [`第${index + 1}关名称不能为空。`]),
      ...(resolveLevelFormalImageSrc(level)
        ? []
        : [`第${index + 1}关缺少正式图。`]),
      ...(level.generationStatus === 'generating'
        ? [`第${index + 1}关画面正在生成。`]
        : []),
    ]),
    ...(hasGeneratingLevel ? ['还有关卡画面正在生成。'] : []),
  ];

  return {
    blockers: [...new Set(blockers.filter(Boolean))],
    publishReady: blockers.filter(Boolean).length === 0,
  };
}

function PuzzleResultHeader({
  autoSaveState,
  isBusy,
  onBack,
}: {
  autoSaveState: PuzzleAutoSaveState;
  isBusy: boolean;
  onBack: () => void;
}) {
  const autoSaveBadge =
    autoSaveState === 'saving' ? (
      <div className="platform-pill platform-pill--warm px-3 py-1 text-[11px]">
        保存中
      </div>
    ) : autoSaveState === 'saved' ? (
      <div className="platform-pill platform-pill--success px-3 py-1 text-[11px]">
        已自动保存
      </div>
    ) : autoSaveState === 'error' ? (
      <div className="platform-pill platform-pill--rose px-3 py-1 text-[11px]">
        保存失败
      </div>
    ) : null;

  return (
    <div className="mb-4 flex items-center justify-between gap-3">
      <button
        type="button"
        onClick={onBack}
        disabled={isBusy}
        className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
      >
        <span className="inline-flex items-center gap-1.5">
          <ArrowLeft className="h-3.5 w-3.5" />
          返回
        </span>
      </button>
      {autoSaveBadge}
    </div>
  );
}

function PuzzleResultTabs({
  activeTab,
  onChange,
}: {
  activeTab: PuzzleResultTab;
  onChange: (tab: PuzzleResultTab) => void;
}) {
  return (
    <div className="mb-3 grid grid-cols-3 gap-2 rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/62 p-1">
      {PUZZLE_RESULT_TABS.map((tab) => (
        <button
          key={tab.id}
          type="button"
          onClick={() => onChange(tab.id)}
          className={`min-h-10 rounded-[1rem] px-3 text-sm font-bold transition ${
            activeTab === tab.id
              ? 'bg-white text-[var(--platform-text-strong)] shadow-sm'
              : 'text-[var(--platform-text-base)] hover:bg-white/60'
          }`}
          aria-pressed={activeTab === tab.id}
        >
          {tab.label}
        </button>
      ))}
    </div>
  );
}

function PuzzleAssetConfigTabs({
  activeTab,
  onChange,
}: {
  activeTab: PuzzleAssetConfigTabId;
  onChange: (tab: PuzzleAssetConfigTabId) => void;
}) {
  return (
    <div className="mb-3 grid grid-cols-2 gap-2 rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-1">
      {PUZZLE_ASSET_CONFIG_TABS.map((tab) => (
        <button
          key={tab.id}
          type="button"
          onClick={() => onChange(tab.id)}
          className={`min-h-10 rounded-[0.9rem] px-3 text-sm font-bold transition ${
            activeTab === tab.id
              ? 'bg-white text-[var(--platform-text-strong)] shadow-sm'
              : 'text-[var(--platform-text-base)] hover:bg-white/60'
          }`}
          aria-pressed={activeTab === tab.id}
        >
          {tab.label}
        </button>
      ))}
    </div>
  );
}

function PuzzleThemeTagEditor({
  editState,
  isBusy,
  error,
  onChange,
  onGenerateTags,
}: {
  editState: DraftEditState;
  isBusy: boolean;
  error: string | null;
  onChange: (nextState: DraftEditState) => void;
  onGenerateTags: () => void;
}) {
  const [newTagText, setNewTagText] = useState('');
  const [isAddingTag, setIsAddingTag] = useState(false);

  const addTags = () => {
    const nextTags = normalizeThemeTagInput(newTagText);
    if (nextTags.length <= 0) {
      setIsAddingTag(false);
      setNewTagText('');
      return;
    }

    onChange({
      ...editState,
      themeTags: [...new Set([...editState.themeTags, ...nextTags])],
    });
    setNewTagText('');
    setIsAddingTag(false);
  };

  return (
    <section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
      <div className="flex items-center justify-between gap-3">
        <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
          作品标签
        </div>
        <div className="flex items-center gap-2">
          <button
            type="button"
            disabled={isBusy}
            onClick={onGenerateTags}
            className="platform-icon-button h-9 w-9"
            aria-label="AI生成作品标签"
            title="AI生成作品标签"
          >
            {isBusy ? (
              <Loader2 className="h-4 w-4 animate-spin" />
            ) : (
              <Sparkles className="h-4 w-4" />
            )}
          </button>
          {!isAddingTag ? (
            <button
              type="button"
              disabled={isBusy}
              onClick={() => setIsAddingTag(true)}
              className="platform-icon-button h-9 w-9"
              aria-label="新增作品标签"
              title="新增作品标签"
            >
              <Plus className="h-4 w-4" />
            </button>
          ) : null}
        </div>
      </div>

      <div className="mt-3 flex flex-wrap gap-2">
        {editState.themeTags.map((tag) => (
          <span
            key={tag}
            className="inline-flex items-center gap-1.5 rounded-full border border-amber-300/35 bg-amber-100/68 px-3 py-1.5 text-xs font-semibold text-amber-700"
          >
            {tag}
            <button
              type="button"
              disabled={isBusy}
              onClick={() => {
                onChange({
                  ...editState,
                  themeTags: editState.themeTags.filter(
                    (currentTag) => currentTag !== tag,
                  ),
                });
              }}
              className="rounded-full text-amber-800/70 transition hover:text-amber-950 disabled:opacity-45"
              aria-label={`删除标签 ${tag}`}
              title="删除标签"
            >
              <X className="h-3.5 w-3.5" />
            </button>
          </span>
        ))}
        {editState.themeTags.length <= 0 ? (
          <span className="text-sm text-[var(--platform-text-soft)]">
            暂无标签
          </span>
        ) : null}
      </div>

      {isAddingTag ? (
        <div className="mt-3 flex flex-col gap-2 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-2 sm:flex-row">
          <input
            autoFocus
            value={newTagText}
            disabled={isBusy}
            onChange={(event) => setNewTagText(event.target.value)}
            onKeyDown={(event) => {
              if (event.key === 'Enter') {
                event.preventDefault();
                addTags();
              }
              if (event.key === 'Escape') {
                setIsAddingTag(false);
                setNewTagText('');
              }
            }}
            className="min-h-10 flex-1 rounded-[0.8rem] border border-transparent bg-white/92 px-3 text-sm text-[var(--platform-text-strong)] outline-none"
            placeholder="输入新标签"
            aria-label="新题材标签"
          />
          <div className="flex gap-2">
            <button
              type="button"
              disabled={isBusy}
              onClick={addTags}
              className="platform-button platform-button--primary min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
            >
              添加
            </button>
            <button
              type="button"
              disabled={isBusy}
              onClick={() => {
                setIsAddingTag(false);
                setNewTagText('');
              }}
              className="platform-button platform-button--ghost min-h-10 flex-1 px-4 py-2 text-sm sm:flex-none"
            >
              取消
            </button>
          </div>
        </div>
      ) : null}
      {error ? (
        <div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
          {error}
        </div>
      ) : null}
    </section>
  );
}

function PuzzleLevelDetailDialog({
  draft,
  generationNowMs,
  generationRuntime,
  imageRefreshKey,
  isBusy,
  level,
  onClose,
  onGenerate,
  onLevelChange,
  onStartTestRun,
}: {
  draft: PuzzleResultDraft;
  generationNowMs: number;
  generationRuntime: PuzzleLevelGenerationRuntime | null;
  imageRefreshKey: string;
  isBusy: boolean;
  level: PuzzleDraftLevel;
  onClose: () => void;
  onGenerate: (
    level: PuzzleDraftLevel,
    promptText?: string | null,
    referenceImageSrc?: string | null,
    imageModel?: PuzzleImageModelId | null,
  ) => void;
  onLevelChange: (nextLevel: PuzzleDraftLevel) => void;
  onStartTestRun?: (level: PuzzleDraftLevel) => void;
}) {
  const platformTheme = useAuthUi()?.platformTheme ?? 'light';
  const [referenceImageSrc, setReferenceImageSrc] = useState('');
  const [referenceImageLabel, setReferenceImageLabel] = useState('');
  const [referenceImageError, setReferenceImageError] = useState<string | null>(
    null,
  );
  const [isHistoryPickerOpen, setIsHistoryPickerOpen] = useState(false);
  const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);
  const [imageModel, setImageModel] = useState<PuzzleImageModelId>(
    PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
  );
  const formalImageSrc = resolveLevelFormalImageSrc(level);
  const hasFormalImage = Boolean(formalImageSrc);
  const effectiveReferenceImageSrc =
    referenceImageSrc.trim() || level.pictureReference?.trim() || '';
  const displayImageSrc = formalImageSrc || effectiveReferenceImageSrc;
  const displayImageAlt = formalImageSrc
    ? level.levelName || draft.workTitle || '拼图关卡'
    : '拼图参考图';
  const generationProgress = resolvePuzzleLevelGenerationProgress(
    level,
    generationRuntime,
    generationNowMs,
  );

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

    try {
      const dataUrl = await readPuzzleReferenceImageAsDataUrl(file);
      setReferenceImageSrc(dataUrl);
      setReferenceImageLabel(file.name.trim() || '本地参考图');
      setReferenceImageError(null);
    } catch (uploadError) {
      setReferenceImageError(
        uploadError instanceof Error
          ? uploadError.message
          : '参考图读取失败，请重试。',
      );
    }
  };

  const executeGeneration = () => {
    const nextLevel = {
      ...level,
      generationStatus: 'generating' as const,
    };
    setIsCostConfirmOpen(false);
    onGenerate(
      nextLevel,
      nextLevel.pictureDescription.trim() || undefined,
      effectiveReferenceImageSrc || undefined,
      imageModel,
    );
  };

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

  return createPortal(
    <div
      className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[138] 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="关卡详情"
        className="platform-modal-shell platform-remap-surface flex max-h-[min(94vh,50rem)] w-full max-w-2xl 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="min-w-0 truncate text-base font-semibold text-[var(--platform-text-strong)]">
            {level.levelName || '关卡详情'}
          </div>
          <button
            type="button"
            onClick={onClose}
            aria-label="关闭"
            className="platform-icon-button"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          <div className="space-y-4">
            <section className="platform-subpanel rounded-[1.35rem] p-4">
              <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                关卡名称
              </div>
              <input
                value={level.levelName}
                disabled={isBusy}
                onChange={(event) =>
                  onLevelChange({ ...level, levelName: event.target.value })
                }
                className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
                aria-label="关卡名称"
              />
            </section>

            <div className="grid gap-4 lg:grid-cols-[minmax(13rem,0.9fr)_minmax(0,1.1fr)]">
              <section className="platform-subpanel rounded-[1.35rem] p-4">
                <div className="mb-3 text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                  画面图
                </div>
                <div className="relative aspect-square overflow-hidden rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)]">
                  <input
                    id={`puzzle-level-reference-upload-${level.levelId}`}
                    type="file"
                    accept="image/png,image/jpeg,image/webp"
                    disabled={isBusy}
                    aria-label="上传参考图"
                    onChange={(event) => {
                      void handleReferenceImageChange(event);
                    }}
                    className="sr-only"
                  />
                  <label
                    htmlFor={`puzzle-level-reference-upload-${level.levelId}`}
                    className={`absolute inset-0 z-0 cursor-pointer ${isBusy ? 'cursor-not-allowed' : ''}`}
                    title={
                      effectiveReferenceImageSrc ? '更换参考图' : '上传参考图'
                    }
                  >
                    <span className="sr-only">
                      {effectiveReferenceImageSrc ? '更换参考图' : '上传参考图'}
                    </span>
                  </label>
                  {displayImageSrc ? (
                    <ResolvedAssetImage
                      src={displayImageSrc}
                      refreshKey={`${imageRefreshKey}:${level.levelId}`}
                      alt={displayImageAlt}
                      className="pointer-events-none h-full w-full object-cover"
                    />
                  ) : (
                    <span className="pointer-events-none flex h-full items-center justify-center bg-[radial-gradient(circle_at_50%_28%,rgba(255,255,255,0.92),transparent_38%),linear-gradient(135deg,rgba(255,255,255,0.96),rgba(255,241,229,0.86))]">
                      <span className="flex h-16 w-16 items-center justify-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/92 text-[var(--platform-text-strong)] shadow-sm">
                        <ImagePlus className="h-7 w-7" />
                      </span>
                    </span>
                  )}
                  {generationProgress.isGenerating ? (
                    <div className="absolute inset-0 flex items-center justify-center bg-white/68 backdrop-blur-[2px]">
                      <div className="flex items-center gap-2 rounded-full border border-white/80 bg-white/94 px-4 py-2 text-sm font-black text-[var(--platform-text-strong)] shadow-sm">
                        <Loader2 className="h-4 w-4 animate-spin text-amber-600" />
                        生成中
                      </div>
                    </div>
                  ) : null}
                  <div className="absolute bottom-3 right-3 z-10">
                    <button
                      type="button"
                      disabled={isBusy}
                      onClick={() => setIsHistoryPickerOpen(true)}
                      className={`inline-flex items-center gap-1.5 rounded-full border border-white/80 bg-white/94 px-3 py-2 text-[11px] font-black text-[var(--platform-text-strong)] shadow-sm backdrop-blur transition hover:text-[#ff4056] ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
                      aria-label="选择历史图片"
                      title="选择历史图片"
                    >
                      <History className="h-3.5 w-3.5" />
                      <span>历史</span>
                    </button>
                  </div>
                </div>
                {effectiveReferenceImageSrc ? (
                  <div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-3">
                    <div className="h-12 w-12 overflow-hidden rounded-[0.85rem] bg-[var(--platform-subpanel-fill)]">
                      <ResolvedAssetImage
                        src={effectiveReferenceImageSrc}
                        alt="拼图参考图"
                        className="h-full w-full object-cover"
                      />
                    </div>
                    <div className="min-w-0 flex-1">
                      <div className="truncate text-sm font-semibold text-[var(--platform-text-strong)]">
                        {referenceImageLabel || '已选择参考图'}
                      </div>
                    </div>
                    <button
                      type="button"
                      disabled={isBusy}
                      onClick={() => {
                        setReferenceImageSrc('');
                        setReferenceImageLabel('');
                        setReferenceImageError(null);
                        onLevelChange({ ...level, pictureReference: null });
                      }}
                      className="platform-icon-button h-9 w-9"
                      aria-label="移除参考图"
                      title="移除参考图"
                    >
                      <X className="h-4 w-4" />
                    </button>
                  </div>
                ) : null}
                {referenceImageError ? (
                  <div className="mt-2 text-xs leading-5 text-red-600">
                    {referenceImageError}
                  </div>
                ) : null}
              </section>

              <section className="platform-subpanel rounded-[1.35rem] p-4">
                <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                  画面描述
                </div>
                <div className="relative mt-3">
                  <textarea
                    value={level.pictureDescription}
                    disabled={isBusy}
                    rows={7}
                    onChange={(event) =>
                      onLevelChange({
                        ...level,
                        pictureDescription: event.target.value,
                      })
                    }
                    className="h-[12rem] min-h-[12rem] w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none sm:h-[14rem] sm:min-h-[14rem] lg:h-full lg:min-h-[18rem]"
                    aria-label="画面描述"
                  />
                  <PuzzleImageModelPicker
                    value={imageModel}
                    disabled={isBusy}
                    onChange={setImageModel}
                  />
                </div>
              </section>
            </div>
          </div>
        </div>

        <div className="shrink-0 space-y-3 border-t border-[var(--platform-subpanel-border)] bg-[var(--platform-page-fill)] px-5 py-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]">
          {onStartTestRun && hasFormalImage ? (
            <button
              type="button"
              disabled={isBusy}
              onClick={() => onStartTestRun(level)}
              className={`platform-button platform-button--secondary w-full ${isBusy ? 'opacity-55' : ''}`}
            >
              <span className="inline-flex items-center gap-2">
                <Play className="h-4 w-4" />
                关卡测试
              </span>
            </button>
          ) : null}

          {generationProgress.isGenerating ? (
            <div
              role="progressbar"
              aria-label="画面生成进度"
              aria-valuemin={0}
              aria-valuemax={100}
              aria-valuenow={generationProgress.progressPercent}
              className="platform-progress-track relative h-12 overflow-hidden rounded-full"
            >
              <div
                className="h-full rounded-full bg-amber-600 transition-[width] duration-300"
                style={{ width: `${generationProgress.progressPercent}%` }}
              />
              <div className="absolute inset-0 flex items-center justify-center gap-2 px-4 text-sm font-bold text-white">
                <Loader2 className="h-4 w-4 animate-spin" />
                预计剩余 {generationProgress.secondsLeft} 秒
              </div>
            </div>
          ) : (
            <button
              type="button"
              disabled={isBusy}
              onClick={() => setIsCostConfirmOpen(true)}
              className="inline-flex w-full flex-col items-center justify-center gap-1 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
            >
              <span className="inline-flex items-center justify-center gap-2">
                <Sparkles className="h-4 w-4" />
                <span>{hasFormalImage ? '重新生成画面' : '生成画面'}</span>
                <span className="rounded-full bg-white/24 px-2 py-0.5 text-[11px] font-bold">
                  消耗{PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
                </span>
              </span>
              <span className="text-[11px] font-semibold leading-none text-white/78">
                等待时间可以制作更多关卡哦~
              </span>
            </button>
          )}
        </div>

        {isCostConfirmOpen ? (
          <div
            className="absolute inset-0 z-20 flex items-center justify-center bg-black/45 p-4 backdrop-blur-sm"
            onClick={() => setIsCostConfirmOpen(false)}
          >
            <section
              role="dialog"
              aria-modal="true"
              aria-label="确认消耗泥点"
              className="platform-modal-shell platform-remap-surface w-full max-w-sm overflow-hidden rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.45)]"
              onClick={(event) => event.stopPropagation()}
            >
              <div className="flex items-center gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
                <span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-100 text-amber-700">
                  <Sparkles className="h-4 w-4" />
                </span>
                <div className="text-base font-semibold text-[var(--platform-text-strong)]">
                  确认消耗泥点
                </div>
              </div>
              <div className="px-5 py-4 text-sm font-semibold text-[var(--platform-text-base)]">
                消耗 {PUZZLE_IMAGE_GENERATION_POINT_COST} 泥点
              </div>
              <div className="flex items-center justify-end gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4">
                <button
                  type="button"
                  onClick={() => setIsCostConfirmOpen(false)}
                  className="platform-button platform-button--ghost min-h-10 px-4 py-2 text-sm"
                >
                  取消
                </button>
                <button
                  type="button"
                  disabled={isBusy || generationProgress.isGenerating}
                  onClick={executeGeneration}
                  className={`platform-button platform-button--primary min-h-10 px-5 py-2 text-sm ${isBusy || generationProgress.isGenerating ? 'opacity-55' : ''}`}
                >
                  确定
                </button>
              </div>
            </section>
          </div>
        ) : null}

        {isHistoryPickerOpen ? (
          <PuzzleHistoryAssetPickerDialog
            isBusy={isBusy}
            onClose={() => setIsHistoryPickerOpen(false)}
            onSelect={(asset) => {
              setReferenceImageSrc(asset.imageSrc);
              setReferenceImageLabel(
                `历史素材 · ${asset.ownerLabel || '未记录账号'}`,
              );
              setReferenceImageError(null);
              setIsHistoryPickerOpen(false);
            }}
          />
        ) : null}
      </div>
    </div>,
    document.body,
  );
}

function PuzzlePublishDialog({
  actionError,
  blockers,
  editState,
  imageRefreshKey,
  isBusy,
  publishReady,
  onClose,
  onPublish,
}: {
  actionError: string | null;
  blockers: string[];
  editState: DraftEditState;
  imageRefreshKey: string;
  isBusy: boolean;
  publishReady: boolean;
  onClose: () => void;
  onPublish: () => void;
}) {
  const platformTheme = useAuthUi()?.platformTheme ?? 'light';
  const primaryLevel = editState.levels[0] ?? null;
  const formalImageSrc = primaryLevel
    ? resolveLevelFormalImageSrc(primaryLevel)
    : '';

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

  return createPortal(
    <div
      className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[140] 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="发布拼图作品"
        className="platform-modal-shell platform-remap-surface flex max-h-[min(90vh,42rem)] w-full max-w-3xl 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>
          <button
            type="button"
            onClick={onClose}
            aria-label="关闭"
            className="platform-icon-button"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_14rem]">
            <div className="space-y-3">
              <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                发布检查
              </div>
              {actionError ? (
                <div className="platform-banner platform-banner--danger text-sm leading-6">
                  {actionError}
                </div>
              ) : publishReady ? (
                <div className="space-y-2">
                  <div className="platform-banner platform-banner--success text-sm leading-6">
                    当前作品已满足发布条件。
                  </div>
                  <div className="platform-banner platform-banner--warning text-sm font-semibold leading-6">
                    消耗 {PUZZLE_PUBLISH_POINT_COST} 泥点
                  </div>
                </div>
              ) : (
                <div className="space-y-2">
                  {blockers.map((blocker, index) => (
                    <div
                      key={`puzzle-publish-blocker-${index}-${blocker}`}
                      className="platform-banner platform-banner--warning text-sm leading-6"
                    >
                      {blocker}
                    </div>
                  ))}
                </div>
              )}
            </div>

            <div className="space-y-3">
              <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                封面关卡
              </div>
              <div className="aspect-square overflow-hidden rounded-[1.15rem] bg-[var(--platform-subpanel-fill)]">
                {formalImageSrc ? (
                  <ResolvedAssetImage
                    src={formalImageSrc}
                    refreshKey={imageRefreshKey}
                    alt={primaryLevel?.levelName || editState.workTitle}
                    className="h-full w-full object-cover"
                  />
                ) : null}
              </div>
              <div className="text-sm font-black text-[var(--platform-text-strong)]">
                {editState.workTitle}
              </div>
            </div>
          </div>
        </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">
          <button
            type="button"
            onClick={onClose}
            className="platform-button platform-button--ghost"
          >
            取消
          </button>
          <button
            type="button"
            onClick={onPublish}
            disabled={!publishReady || isBusy}
            className={`platform-button platform-button--primary ${!publishReady || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
          >
            {isBusy
              ? '发布中...'
              : `发布到广场 · ${PUZZLE_PUBLISH_POINT_COST}泥点`}
          </button>
        </div>
      </div>
    </div>,
    document.body,
  );
}

function PuzzleCreativeDraftEditBar({
  currentDraft,
  error,
  isBusy,
  onSubmit,
}: {
  currentDraft: PuzzleResultDraft;
  error: string | null;
  isBusy: boolean;
  onSubmit: (payload: {
    instruction: string;
    currentDraft: PuzzleResultDraft;
  }) => Promise<CreativeDraftEditResult | null> | void;
}) {
  const [instruction, setInstruction] = useState('');
  const trimmedInstruction = instruction.trim();
  const canSubmit = Boolean(trimmedInstruction) && !isBusy;

  const submit = () => {
    if (!canSubmit) {
      return;
    }
    void onSubmit({
      instruction: trimmedInstruction,
      currentDraft,
    });
    setInstruction('');
  };

  return (
    <section className="platform-subpanel mb-3 rounded-[1.35rem] p-3 sm:p-4">
      <div className="flex items-end gap-2">
        <span className="mb-1 hidden h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/72 text-[var(--platform-text-base)] sm:inline-flex">
          {isBusy ? (
            <Loader2 className="h-4 w-4 animate-spin" />
          ) : (
            <MessageSquareText className="h-4 w-4" />
          )}
        </span>
        <textarea
          value={instruction}
          disabled={isBusy}
          rows={2}
          onChange={(event) => setInstruction(event.target.value)}
          onKeyDown={(event) => {
            if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
              event.preventDefault();
              submit();
            }
          }}
          className="min-h-11 flex-1 resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
          placeholder="让 Agent 调整标题、标签或关卡描述"
          aria-label="智能修订拼图草稿"
        />
        <button
          type="button"
          disabled={!canSubmit}
          onClick={submit}
          className="platform-button platform-button--secondary min-h-11 px-4 py-2 text-sm"
        >
          {isBusy ? '修改中' : '修改'}
        </button>
      </div>
      {error ? (
        <div className="platform-banner platform-banner--danger mt-3 rounded-[1rem] text-sm leading-6">
          {error}
        </div>
      ) : null}
    </section>
  );
}

function PuzzleLevelListTab({
  editState,
  generationNowMs,
  generationRuntimeByLevelId,
  imageRefreshKey,
  isBusy,
  onAddLevel,
  onDeleteLevel,
  onOpenLevel,
}: {
  editState: DraftEditState;
  generationNowMs: number;
  generationRuntimeByLevelId: Record<string, PuzzleLevelGenerationRuntime>;
  imageRefreshKey: string;
  isBusy: boolean;
  onAddLevel: () => void;
  onDeleteLevel: (levelId: string) => void;
  onOpenLevel: (levelId: string) => void;
}) {
  return (
    <div className="space-y-3">
      <div
        aria-label="拼图关卡列表"
        className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3"
      >
        {editState.levels.map((level, index) => {
          const imageSrc = resolveLevelFormalImageSrc(level);
          const displayLevelName = level.levelName || `第${index + 1}关`;
          const generationProgress = resolvePuzzleLevelGenerationProgress(
            level,
            generationRuntimeByLevelId[level.levelId] ?? null,
            generationNowMs,
          );
          return (
            <div
              key={level.levelId}
              className="platform-subpanel overflow-hidden rounded-[1.35rem] p-0"
            >
              <button
                type="button"
                onClick={() => onOpenLevel(level.levelId)}
                className="block w-full text-left"
              >
                <div className="relative aspect-[4/3] overflow-hidden bg-[var(--platform-subpanel-fill)]">
                  {imageSrc ? (
                    <ResolvedAssetImage
                      src={imageSrc}
                      refreshKey={`${imageRefreshKey}:${level.levelId}`}
                      alt={displayLevelName}
                      className="h-full w-full object-cover"
                    />
                  ) : (
                    <div className="flex h-full items-center justify-center text-sm text-[var(--platform-text-soft)]">
                      暂无正式图
                    </div>
                  )}
                  {generationProgress.isGenerating ? (
                    <div className="absolute inset-0 flex items-center justify-center bg-white/68 backdrop-blur-[2px]">
                      <div className="flex items-center gap-2 rounded-full border border-white/80 bg-white/94 px-3 py-1.5 text-xs font-black text-[var(--platform-text-strong)] shadow-sm">
                        <Loader2 className="h-3.5 w-3.5 animate-spin text-amber-600" />
                        生成中
                      </div>
                    </div>
                  ) : null}
                </div>
                <div className="space-y-1 px-4 py-4">
                  <div className="flex items-center justify-between gap-2 text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
                    <span>第{index + 1}关</span>
                    {generationProgress.isGenerating ? (
                      <span className="rounded-full bg-amber-100 px-2 py-0.5 tracking-normal text-amber-700">
                        生成中
                      </span>
                    ) : null}
                  </div>
                </div>
              </button>
              <div className="flex items-end gap-2 px-4 pb-4">
                <button
                  type="button"
                  onClick={() => onOpenLevel(level.levelId)}
                  className="min-w-0 flex-1 truncate text-left text-base font-black text-[var(--platform-text-strong)]"
                >
                  {displayLevelName}
                </button>
                <button
                  type="button"
                  disabled={isBusy || editState.levels.length <= 1}
                  onClick={() => onDeleteLevel(level.levelId)}
                  className="platform-icon-button h-9 w-9 shrink-0"
                  aria-label={`删除关卡 ${displayLevelName}`}
                  title="删除关卡"
                >
                  <Trash2 className="h-4 w-4" />
                </button>
              </div>
            </div>
          );
        })}
      </div>

      <button
        type="button"
        disabled={isBusy}
        onClick={onAddLevel}
        className="platform-button platform-button--secondary w-full"
      >
        <span className="inline-flex items-center gap-2">
          <Plus className="h-4 w-4" />
          新增关卡
        </span>
        <span className="mt-1 block text-[11px] font-semibold leading-none text-[var(--platform-text-soft)]">
          获得更多积分激励
        </span>
      </button>
    </div>
  );
}

function PuzzleWorkInfoTab({
  editState,
  tagGenerationError,
  isBusy,
  onChange,
  onGenerateTags,
}: {
  editState: DraftEditState;
  tagGenerationError: string | null;
  isBusy: boolean;
  onChange: (nextState: DraftEditState) => void;
  onGenerateTags: () => void;
}) {
  return (
    <div className="space-y-3">
      <section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
        <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
          作品名称
        </div>
        <input
          value={editState.workTitle}
          disabled={isBusy}
          onChange={(event) =>
            onChange({ ...editState, workTitle: event.target.value })
          }
          className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
          aria-label="作品名称"
        />
      </section>

      <section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
        <div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
          作品描述
        </div>
        <textarea
          value={editState.workDescription}
          disabled={isBusy}
          rows={6}
          onChange={(event) =>
            onChange({ ...editState, workDescription: event.target.value })
          }
          className="mt-3 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
          aria-label="作品描述"
        />
      </section>

      <PuzzleThemeTagEditor
        editState={editState}
        error={tagGenerationError}
        isBusy={isBusy}
        onChange={onChange}
        onGenerateTags={onGenerateTags}
      />
    </div>
  );
}

function PuzzleUiAssetsTab({
  editState,
  imageRefreshKey,
  isBusy,
  onChange,
  onGenerate,
}: {
  editState: DraftEditState;
  imageRefreshKey: string;
  isBusy: boolean;
  onChange: (nextState: DraftEditState) => void;
  onGenerate: (prompt: string) => void;
}) {
  const firstLevel = editState.levels[0] ?? null;
  const formalImageSrc = firstLevel ? resolveLevelFormalImageSrc(firstLevel) : '';
  const defaultPrompt = buildDefaultPuzzleUiBackgroundPrompt(
    editState,
    firstLevel,
  );
  const prompt = firstLevel?.uiBackgroundPrompt ?? '';
  const normalizedPrompt = prompt.trim() || defaultPrompt.trim();
  const backgroundPreviewSrc =
    resolvePuzzleUiBackgroundSource(firstLevel) || PUZZLE_UI_BACKGROUND_REFERENCE_SRC;
  const hasGeneratedUiBackground = Boolean(resolvePuzzleUiBackgroundSource(firstLevel));
  const [isPreviewOpen, setIsPreviewOpen] = useState(false);
  const [isCostConfirmOpen, setIsCostConfirmOpen] = useState(false);

  const updateFirstLevel = (nextLevel: PuzzleDraftLevel) => {
    onChange({
      ...editState,
      levels: [nextLevel, ...editState.levels.slice(1)],
    });
  };

  return (
    <div className="space-y-3">
      <section className="platform-subpanel rounded-[1.35rem] p-4 sm:p-5">
        <div className="grid gap-4 lg:grid-cols-[minmax(13rem,0.78fr)_minmax(0,1fr)]">
          <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预览"
          >
            <ResolvedAssetImage
              src={backgroundPreviewSrc}
              refreshKey={`${imageRefreshKey}:ui-background`}
              alt="拼图UI背景图"
              className="h-full w-full object-cover"
            />
          </button>

          <div className="flex min-h-0 flex-col">
            <label className="block">
              <span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
                UI背景提示词
              </span>
              <textarea
                value={prompt}
                disabled={isBusy || !firstLevel}
                rows={8}
                onChange={(event) => {
                  if (!firstLevel) {
                    return;
                  }
                  updateFirstLevel({
                    ...firstLevel,
                    uiBackgroundPrompt: event.target.value,
                  });
                }}
                className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
                aria-label="拼图UI背景提示词"
              />
            </label>
            <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
              <button
                type="button"
                onClick={() => setIsPreviewOpen(true)}
                className="platform-button platform-button--ghost min-h-11 justify-center gap-2 px-4 py-3"
              >
                <Eye className="h-4 w-4" />
                预览UI
              </button>
              <button
                type="button"
                disabled={!firstLevel || !normalizedPrompt || isBusy}
                onClick={() => {
                  if (!firstLevel || !normalizedPrompt) {
                    return;
                  }
                  setIsCostConfirmOpen(true);
                }}
                className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 ${!firstLevel || !normalizedPrompt || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
              >
                {isBusy ? (
                  <Loader2 className="h-4 w-4 animate-spin" />
                ) : (
                  <Wand2 className="h-4 w-4" />
                )}
                {hasGeneratedUiBackground ? '重新生成' : '生成UI背景'} · {PUZZLE_IMAGE_GENERATION_POINT_COST}泥点
              </button>
            </div>
          </div>
        </div>
      </section>

      {isPreviewOpen ? (
        <PuzzleUiRuntimePreviewPanel
          backgroundPreviewSrc={backgroundPreviewSrc}
          imageRefreshKey={imageRefreshKey}
          puzzleImageSrc={formalImageSrc}
          title={editState.workTitle || firstLevel?.levelName || '拼图'}
          onClose={() => setIsPreviewOpen(false)}
        />
      ) : null}
      {isCostConfirmOpen ? (
        <div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
          <div
            role="dialog"
            aria-modal="true"
            aria-labelledby="puzzle-ui-point-cost-confirm-title"
            className="platform-modal-shell platform-remap-surface w-full max-w-xs rounded-[1.35rem] p-5 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
          >
            <div
              id="puzzle-ui-point-cost-confirm-title"
              className="text-base font-black text-[var(--platform-text-strong)]"
            >
              确认消耗泥点
            </div>
            <div className="mt-2 text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
              消耗 {PUZZLE_IMAGE_GENERATION_POINT_COST} 泥点
            </div>
            <div className="mt-5 grid grid-cols-2 gap-3">
              <button
                type="button"
                onClick={() => setIsCostConfirmOpen(false)}
                className="platform-button platform-button--secondary justify-center"
              >
                取消
              </button>
              <button
                type="button"
                disabled={!firstLevel || !normalizedPrompt || isBusy}
                onClick={() => {
                  if (!firstLevel || !normalizedPrompt) {
                    return;
                  }
                  updateFirstLevel({
                    ...firstLevel,
                    uiBackgroundPrompt: normalizedPrompt,
                  });
                  setIsCostConfirmOpen(false);
                  onGenerate(normalizedPrompt);
                }}
                className={`platform-button platform-button--primary justify-center ${!firstLevel || !normalizedPrompt || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
              >
                确定
              </button>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}

function PuzzleUiRuntimePreviewPanel({
  backgroundPreviewSrc,
  imageRefreshKey,
  puzzleImageSrc,
  title,
  onClose,
}: {
  backgroundPreviewSrc: string;
  imageRefreshKey: string;
  puzzleImageSrc: string;
  title: string;
  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-[139] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
      onClick={(event) => {
        if (event.target === event.currentTarget) {
          onClose();
        }
      }}
    >
      <section
        role="dialog"
        aria-modal="true"
        aria-label="UI预览"
        className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,48rem)] w-full max-w-sm 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="min-w-0 truncate text-base font-semibold text-[var(--platform-text-strong)]">
            UI预览
          </div>
          <button
            type="button"
            onClick={onClose}
            aria-label="关闭"
            className="platform-icon-button"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          <div className="mx-auto aspect-[9/16] max-h-[min(78dvh,42rem)] w-full max-w-[22rem] overflow-hidden rounded-[1.4rem] border border-white/22 bg-[#16211f] 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">
              <ResolvedAssetImage
                src={backgroundPreviewSrc}
                refreshKey={`${imageRefreshKey}:ui-runtime-preview`}
                alt=""
                data-testid="puzzle-ui-runtime-preview-background"
                aria-hidden="true"
                className="pointer-events-none absolute inset-0 h-full w-full object-cover"
              />
              <div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(15,23,42,0.18)_0%,rgba(15,23,42,0.05)_45%,rgba(15,23,42,0.24)_100%)]" />
              <header className="relative z-10 grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-2">
                <span className="flex h-10 w-10 items-center justify-center rounded-full border border-white/20 bg-black/28 backdrop-blur">
                  <ArrowLeft size={20} />
                </span>
                <span className="min-w-0 truncate rounded-full border border-white/18 bg-black/26 px-3 py-2 text-center text-sm font-black backdrop-blur">
                  {title}
                </span>
                <span className="flex h-10 w-10 items-center justify-center rounded-full border border-white/20 bg-black/28 backdrop-blur">
                  <LayoutTemplate className="h-4 w-4" />
                </span>
              </header>

              <section className="relative z-10 mt-4 flex min-h-0 flex-1 items-center justify-center">
                <div
                  className="relative aspect-square max-w-full overflow-hidden rounded-[1.25rem] border-[8px] border-white/88 bg-white/92 shadow-[0_20px_44px_rgba(15,23,42,0.32),inset_0_0_0_2px_rgba(15,23,42,0.12)]"
                  style={{ width: 'min(88%, 52dvh, 100%)' }}
                  aria-label="拼图区边界"
                >
                  {puzzleImageSrc ? (
                    <ResolvedAssetImage
                      src={puzzleImageSrc}
                      refreshKey={`${imageRefreshKey}:ui-runtime-board`}
                      alt=""
                      aria-hidden="true"
                      className="h-full w-full object-cover"
                    />
                  ) : (
                    <div className="grid h-full w-full grid-cols-3 grid-rows-3 gap-1 bg-slate-100 p-2">
                      {Array.from({ length: 9 }).map((_, index) => (
                        <span
                          key={index}
                          className="rounded-[0.45rem] bg-slate-300/70"
                        />
                      ))}
                    </div>
                  )}
                  <div className="pointer-events-none absolute inset-0 rounded-[0.82rem] border-2 border-black/18" />
                </div>
              </section>

              <footer className="relative z-10 mt-3 rounded-[1.35rem] border border-white/16 bg-black/24 p-2 shadow-[0_16px_36px_rgba(15,23,42,0.24)] backdrop-blur">
                <div className="grid grid-cols-4 gap-2">
                  {Array.from({ length: 4 }).map((_, index) => (
                    <span
                      key={index}
                      className="h-12 rounded-xl bg-white/14 sm:h-14"
                    />
                  ))}
                </div>
              </footer>
            </div>
          </div>
        </div>
      </section>
    </div>,
    document.body,
  );
}

function PuzzleAssetConfigTab({
  activeAssetConfigTab,
  editState,
  imageRefreshKey,
  isBusy,
  onAssetConfigTabChange,
  onChange,
  onGenerateUiBackground,
}: {
  activeAssetConfigTab: PuzzleAssetConfigTabId;
  editState: DraftEditState;
  imageRefreshKey: string;
  isBusy: boolean;
  onAssetConfigTabChange: (tab: PuzzleAssetConfigTabId) => void;
  onChange: (nextState: DraftEditState) => void;
  onGenerateUiBackground: (prompt: string) => void;
}) {
  return (
    <div className="min-h-0">
      <PuzzleAssetConfigTabs
        activeTab={activeAssetConfigTab}
        onChange={onAssetConfigTabChange}
      />
      {activeAssetConfigTab === 'ui' ? (
        <PuzzleUiAssetsTab
          editState={editState}
          imageRefreshKey={imageRefreshKey}
          isBusy={isBusy}
          onChange={onChange}
          onGenerate={onGenerateUiBackground}
        />
      ) : null}
    </div>
  );
}

function PuzzleResultActionBar({
  actionError,
  editState,
  imageRefreshKey,
  isBusy,
  canStartTestRun,
  publishReady,
  publishBlockers,
  onPublish,
  onStartTestRun,
}: {
  actionError: string | null;
  editState: DraftEditState;
  imageRefreshKey: string;
  isBusy: boolean;
  canStartTestRun: boolean;
  publishReady: boolean;
  publishBlockers: string[];
  onPublish: () => void;
  onStartTestRun?: () => void;
}) {
  const [showPublishDialog, setShowPublishDialog] = useState(false);
  const [hasAttemptedPublish, setHasAttemptedPublish] = useState(false);

  const closePublishDialog = () => {
    setHasAttemptedPublish(false);
    setShowPublishDialog(false);
  };

  return (
    <div className="mt-4 flex items-center justify-end gap-3 pb-[max(0.25rem,env(safe-area-inset-bottom))]">
      {onStartTestRun ? (
        <button
          type="button"
          onClick={onStartTestRun}
          disabled={isBusy || !canStartTestRun}
          className={`platform-button platform-button--ghost ${isBusy || !canStartTestRun ? 'cursor-not-allowed opacity-55' : ''}`}
        >
          <span className="inline-flex items-center gap-2">
            <Play className="h-4 w-4" />
            试玩
          </span>
        </button>
      ) : null}
      <button
        type="button"
        onClick={() => {
          setHasAttemptedPublish(false);
          setShowPublishDialog(true);
        }}
        disabled={isBusy}
        className={`platform-button platform-button--primary ${isBusy ? 'opacity-55' : ''}`}
      >
        <span className="inline-flex items-center gap-2">
          <CheckCircle2 className="h-4 w-4" />
          发布
        </span>
      </button>

      {showPublishDialog ? (
        <PuzzlePublishDialog
          actionError={hasAttemptedPublish ? actionError : null}
          blockers={publishBlockers}
          editState={editState}
          imageRefreshKey={imageRefreshKey}
          isBusy={isBusy}
          publishReady={publishReady}
          onClose={closePublishDialog}
          onPublish={() => {
            setHasAttemptedPublish(true);
            onPublish();
          }}
        />
      ) : null}
    </div>
  );
}

export function PuzzleResultView({
  session,
  profileId = null,
  isBusy = false,
  error = null,
  onBack,
  onExecuteAction,
  onStartTestRun,
  creativeDraftEdit = null,
}: PuzzleResultViewProps) {
  const draft = session.draft;
  const [activeTab, setActiveTab] = useState<PuzzleResultTab>('levels');
  const [activeAssetConfigTab, setActiveAssetConfigTab] =
    useState<PuzzleAssetConfigTabId>('ui');
  const [activeLevelId, setActiveLevelId] = useState<string | null>(null);
  const [editState, setEditState] = useState<DraftEditState | null>(
    draft ? createDraftEditState(draft) : null,
  );
  const [autoSaveState, setAutoSaveState] =
    useState<PuzzleAutoSaveState>('idle');
  const [autoSaveError, setAutoSaveError] = useState<string | null>(null);
  const [tagGenerationError, setTagGenerationError] = useState<string | null>(
    null,
  );
  const [generationRuntimeByLevelId, setGenerationRuntimeByLevelId] = useState<
    Record<string, PuzzleLevelGenerationRuntime>
  >({});
  const [generationNowMs, setGenerationNowMs] = useState(() => Date.now());
  const savedEditStateRef = useRef<DraftEditState | null>(
    draft ? createDraftEditState(draft) : null,
  );

  useEffect(() => {
    if (!draft) {
      setEditState(null);
      setActiveLevelId(null);
      setAutoSaveState('idle');
      setAutoSaveError(null);
      setTagGenerationError(null);
      return;
    }
    const nextState = createDraftEditState(draft);
    setEditState((currentState) => {
      const mergedState = mergeDraftEditStateWithIncomingState(
        currentState,
        nextState,
      );
      savedEditStateRef.current = nextState;
      return mergedState;
    });
    setGenerationRuntimeByLevelId((current) => {
      const nextRuntimes: Record<string, PuzzleLevelGenerationRuntime> = {};
      nextState.levels.forEach((level) => {
        if (level.generationStatus === 'generating') {
          nextRuntimes[level.levelId] =
            current[level.levelId] ?? {
              startedAtMs: Date.now(),
              estimateSeconds: PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS,
            };
        }
      });
      return nextRuntimes;
    });
    setActiveLevelId((currentLevelId) =>
      currentLevelId &&
      nextState.levels.some((level) => level.levelId === currentLevelId)
        ? currentLevelId
        : null,
    );
    setAutoSaveState('idle');
    setAutoSaveError(null);
    setTagGenerationError(null);
  }, [draft]);

  const syncedDraft = useMemo(() => {
    if (!draft || !editState) {
      return null;
    }
    return syncDraftFromEditState(draft, editState);
  }, [draft, editState]);
  const primaryLevel = editState?.levels[0] ?? null;
  const primaryImageSrc = primaryLevel
    ? resolveLevelFormalImageSrc(primaryLevel)
    : '';
  const imageRefreshKey = `${session.updatedAt}:${primaryImageSrc}:${editState?.levels.length ?? 0}`;
  const activeLevel =
    editState?.levels.find((level) => level.levelId === activeLevelId) ?? null;
  const hasGeneratingLevel = Boolean(
    editState?.levels.some((level) => level.generationStatus === 'generating'),
  );

  useEffect(() => {
    if (!hasGeneratingLevel) {
      return;
    }

    const timer = window.setInterval(() => {
      setGenerationNowMs(Date.now());
    }, 1000);

    return () => window.clearInterval(timer);
  }, [hasGeneratingLevel]);

  useEffect(() => {
    if (!editState) {
      return;
    }

    const activeGeneratingLevelIds = new Set(
      editState.levels
        .filter((level) => level.generationStatus === 'generating')
        .map((level) => level.levelId),
    );
    setGenerationRuntimeByLevelId((current) => {
      let changed = false;
      const nextRuntime: Record<string, PuzzleLevelGenerationRuntime> = {};
      Object.entries(current).forEach(([levelId, runtime]) => {
        if (!activeGeneratingLevelIds.has(levelId)) {
          changed = true;
          return;
        }
        nextRuntime[levelId] = runtime;
      });
      return changed ? nextRuntime : current;
    });
  }, [editState]);

  useEffect(() => {
    if (!draft || !editState || !profileId) {
      return;
    }

    const normalizedState: DraftEditState = {
      ...editState,
      workTitle: editState.workTitle.trim(),
      workDescription: editState.workDescription.trim(),
      themeTags: normalizeThemeTagInput(editState.themeTags.join('，')),
      levels: editState.levels.map((level) => ({
        ...level,
        levelName: level.levelName.trim(),
        pictureDescription: level.pictureDescription.trim(),
        pictureReference: level.pictureReference?.trim() || null,
        uiBackgroundPrompt: level.uiBackgroundPrompt?.trim() || null,
        uiBackgroundImageSrc: level.uiBackgroundImageSrc?.trim() || null,
        uiBackgroundImageObjectKey:
          level.uiBackgroundImageObjectKey?.trim() || null,
        generationStatus: level.generationStatus || 'idle',
      })),
    };
    const originalState =
      savedEditStateRef.current ?? createDraftEditState(draft);
    const changed =
      JSON.stringify(normalizedState) !== JSON.stringify(originalState);

    if (!changed || normalizedState.levels.length <= 0) {
      return;
    }

    setAutoSaveState('saving');
    setAutoSaveError(null);

    let cancelled = false;
    const timer = window.setTimeout(() => {
      const firstLevel = normalizedState.levels[0]!;
      void updatePuzzleWork(profileId, {
        workTitle: normalizedState.workTitle,
        workDescription: normalizedState.workDescription,
        levelName: firstLevel.levelName,
        summary: normalizedState.workDescription,
        themeTags: normalizedState.themeTags,
        coverImageSrc: resolveLevelFormalImageSrc(firstLevel) || null,
        coverAssetId: firstLevel.coverAssetId ?? null,
        levels: normalizedState.levels,
      })
        .then(() => {
          if (cancelled) {
            return;
          }
          // 自动保存成功后推进比较基线，避免新增后再删除回首版形态时漏同步后端。
          savedEditStateRef.current = normalizedState;
          setAutoSaveState('saved');
        })
        .catch((saveError) => {
          if (cancelled) {
            return;
          }
          setAutoSaveState('error');
          setAutoSaveError(
            saveError instanceof Error ? saveError.message : '自动保存失败。',
          );
        });
    }, PUZZLE_AUTOSAVE_DEBOUNCE_MS);

    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [draft, editState, profileId]);

  const publishState = useMemo(() => {
    if (!editState) {
      return {
        blockers: ['等待结果页草稿完成后再发布。'],
        publishReady: false,
      };
    }
    return buildPublishReady(session, editState);
  }, [editState, session]);

  if (!draft || !editState || !syncedDraft) {
    return (
      <div className="flex h-full items-center justify-center">
        <div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
          还没有可编辑的拼图草稿
        </div>
      </div>
    );
  }

  const updateLevel = (nextLevel: PuzzleDraftLevel) => {
    setGenerationRuntimeByLevelId((current) => {
      if (nextLevel.generationStatus === 'generating') {
        return {
          ...current,
          [nextLevel.levelId]:
            current[nextLevel.levelId] ?? {
              startedAtMs: Date.now(),
              estimateSeconds: PUZZLE_IMAGE_GENERATION_ESTIMATE_SECONDS,
            },
        };
      }

      if (!current[nextLevel.levelId]) {
        return current;
      }

      const nextRuntime = { ...current };
      delete nextRuntime[nextLevel.levelId];
      return nextRuntime;
    });
    setEditState((currentState) =>
      currentState
        ? {
            ...currentState,
            levels: currentState.levels.map((level) =>
              level.levelId === nextLevel.levelId ? nextLevel : level,
            ),
          }
        : currentState,
    );
  };

  const buildLevelDraft = (level: PuzzleDraftLevel): PuzzleResultDraft => ({
    ...syncedDraft,
    levelName: level.levelName,
    summary: editState.workDescription.trim(),
    candidates: level.candidates,
    selectedCandidateId: level.selectedCandidateId,
    coverImageSrc: resolveLevelFormalImageSrc(level) || level.coverImageSrc,
    coverAssetId: level.coverAssetId,
    generationStatus: level.generationStatus,
    levels: [level],
  });
  const canStartTestRun = Boolean(onStartTestRun && primaryImageSrc);

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

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

      {creativeDraftEdit ? (
        <PuzzleCreativeDraftEditBar
          currentDraft={syncedDraft}
          error={creativeDraftEdit.error}
          isBusy={creativeDraftEdit.isBusy}
          onSubmit={creativeDraftEdit.onSubmit}
        />
      ) : null}

      <div className="min-h-0 flex-1 overflow-y-auto pr-1">
        {activeTab === 'levels' ? (
          <PuzzleLevelListTab
            editState={editState}
            generationNowMs={generationNowMs}
            generationRuntimeByLevelId={generationRuntimeByLevelId}
            imageRefreshKey={imageRefreshKey}
            isBusy={isBusy}
            onAddLevel={() => {
              const nextLevel = createBlankPuzzleLevel(editState.levels);
              setEditState({
                ...editState,
                levels: [...editState.levels, nextLevel],
              });
              setActiveLevelId(nextLevel.levelId);
            }}
            onDeleteLevel={(levelId) => {
              if (editState.levels.length <= 1) {
                return;
              }
              const nextLevels = editState.levels.filter(
                (level) => level.levelId !== levelId,
              );
              setEditState({
                ...editState,
                levels: nextLevels,
              });
              if (activeLevelId === levelId) {
                setActiveLevelId(null);
              }
            }}
            onOpenLevel={setActiveLevelId}
          />
        ) : null}
        {activeTab === 'work' ? (
          <PuzzleWorkInfoTab
            editState={editState}
            tagGenerationError={tagGenerationError}
            isBusy={isBusy}
            onChange={setEditState}
            onGenerateTags={() => {
              const workTitle = editState.workTitle.trim();
              const workDescription = editState.workDescription.trim();
              if (!workTitle || !workDescription) {
                setTagGenerationError('请先填写作品名称和作品描述。');
                return;
              }
              setTagGenerationError(null);
              const firstLevel = editState.levels[0] ?? null;
              onExecuteAction({
                action: 'generate_puzzle_tags',
                workTitle,
                workDescription,
                levelName: firstLevel?.levelName.trim(),
                summary: workDescription,
                themeTags: editState.themeTags,
                levelsJson: JSON.stringify(editState.levels),
              });
            }}
          />
        ) : null}
        {activeTab === 'assets' ? (
          <PuzzleAssetConfigTab
            activeAssetConfigTab={activeAssetConfigTab}
            editState={editState}
            imageRefreshKey={imageRefreshKey}
            isBusy={isBusy}
            onAssetConfigTabChange={setActiveAssetConfigTab}
            onChange={setEditState}
            onGenerateUiBackground={(prompt) => {
              const firstLevel = editState.levels[0] ?? null;
              if (!firstLevel) {
                return;
              }
              onExecuteAction({
                action: 'generate_puzzle_ui_background',
                levelId: firstLevel.levelId,
                promptText: prompt,
                workTitle: editState.workTitle.trim(),
                workDescription: editState.workDescription.trim(),
                summary: editState.workDescription.trim(),
                themeTags: editState.themeTags,
                levelsJson: JSON.stringify(
                  editState.levels.map((level, index) =>
                    index === 0
                      ? {
                          ...level,
                          uiBackgroundPrompt: prompt,
                        }
                      : level,
                  ),
                ),
              });
            }}
          />
        ) : null}
      </div>

      {error ? (
        <div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
          {error}
        </div>
      ) : null}
      {!error && autoSaveError ? (
        <div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
          {autoSaveError}
        </div>
      ) : null}

      <PuzzleResultActionBar
        actionError={error}
        editState={editState}
        imageRefreshKey={imageRefreshKey}
        isBusy={isBusy}
        canStartTestRun={canStartTestRun}
        publishReady={publishState.publishReady}
        publishBlockers={publishState.blockers}
        onStartTestRun={
          onStartTestRun
            ? () => onStartTestRun(syncedDraft)
            : undefined
        }
        onPublish={() => {
          if (!publishState.publishReady) {
            return;
          }
          const firstLevel = editState.levels[0]!;
          onExecuteAction({
            action: 'publish_puzzle_work',
            workTitle: editState.workTitle.trim(),
            workDescription: editState.workDescription.trim(),
            levelName: firstLevel.levelName.trim(),
            summary: editState.workDescription.trim(),
            themeTags: editState.themeTags,
            levelsJson: JSON.stringify(editState.levels),
          });
        }}
      />

      {activeLevel ? (
        <PuzzleLevelDetailDialog
          draft={syncedDraft}
          generationNowMs={generationNowMs}
          generationRuntime={
            generationRuntimeByLevelId[activeLevel.levelId] ?? null
          }
          imageRefreshKey={imageRefreshKey}
          isBusy={isBusy}
          level={activeLevel}
          onClose={() => setActiveLevelId(null)}
          onGenerate={(nextLevel, promptText, referenceImageSrc, imageModel) => {
            updateLevel(nextLevel);
            onExecuteAction({
              action: 'generate_puzzle_images',
              levelId: nextLevel.levelId,
              promptText,
              referenceImageSrc,
              imageModel: imageModel ?? PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
              aiRedraw: true,
              candidateCount: 1,
              workTitle: editState.workTitle.trim(),
              workDescription: editState.workDescription.trim(),
              summary: editState.workDescription.trim(),
              themeTags: editState.themeTags,
              levelsJson: JSON.stringify(
                editState.levels.map((level) =>
                  level.levelId === nextLevel.levelId ? nextLevel : level,
                ),
              ),
            });
          }}
          onLevelChange={updateLevel}
          onStartTestRun={
            onStartTestRun
              ? (level) => onStartTestRun(buildLevelDraft(level))
              : undefined
          }
        />
      ) : null}
    </div>
  );
}

export default PuzzleResultView;
