import {
  ArrowLeft,
  ArrowRight,
  Clock,
  Loader2,
  Settings,
  Sparkles,
  Trophy,
  X,
} from 'lucide-react';
import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react';
import { createPortal } from 'react-dom';

import puzzleLevelLogo from '../../../media/logo-runtime-hud.webp';
import type {
  DragPuzzlePieceRequest,
  PuzzleBoardSnapshot,
  PuzzleCellPosition,
  PuzzleMergedGroupState,
  PuzzleRecommendedNextWork,
  PuzzleRunSnapshot,
  PuzzleRuntimeLevelSnapshot,
  PuzzleRuntimePropKind,
  SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
import {
  createRuntimeDragInputController,
  createRuntimeInputPointFromClient,
  readRuntimeInputElementBounds,
  resolveRuntimeInputGridCell,
  type RuntimeDragInputSession,
  type RuntimeInputPoint,
} from '../../services/input-devices';
import { resolvePuzzleUiBackgroundSource } from '../../services/puzzle-runtime/puzzleUiBackgroundSource';
import {
  buildPuzzleUiSpriteBackgroundStyle,
  buildPuzzleUiSpriteHitZoneStyle,
  loadPuzzleUiSpritesheetLayout,
  type PuzzleUiSpriteKind,
  type PuzzleUiSpritesheetLayout,
} from '../../services/puzzle-runtime/puzzleUiSpritesheetParser';
import {
  DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG,
  playRuntimeClickSound,
  playRuntimeCountdownSound,
  playRuntimeLevelClearSound,
  resolveRuntimeCountdownSecondBucket,
} from '../../services/runtimeAudioFeedback';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast';
import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import {
  PuzzleRuntimeDialogButton,
  PuzzleRuntimeDialogFooter,
  PuzzleRuntimeModalShell,
} from './PuzzleRuntimeModalShell';
import {
  buildMergedGroupOutlinePath,
  buildRoundedGridCellClipPath,
  resolveDraggedMergedGroupLayer,
  resolveDraggedPieceCellLayer,
  resolveDraggedPieceLayer,
  sanitizeSvgId,
} from './puzzleRuntimeShape';

type PuzzleRuntimeShellProps = {
  run: PuzzleRunSnapshot | null;
  isBusy?: boolean;
  error?: string | null;
  hideBackButton?: boolean;
  hideExitControls?: boolean;
  embedded?: boolean;
  onBack: () => void;
  onRemodelWork?: (profileId: string) => void | Promise<void>;
  onSwapPieces: (payload: SwapPuzzlePiecesRequest) => void;
  onDragPiece: (payload: DragPuzzlePieceRequest) => void;
  onAdvanceNextLevel: (target?: PuzzleNextLevelTarget) => void;
  onRestartLevel?: () => void | Promise<void>;
  onPauseChange?: (paused: boolean) => void | Promise<void>;
  onUseProp?: (
    propKind: PuzzleRuntimePropKind,
  ) => Promise<PuzzleRunSnapshot | null | void>;
  onTimeExpired?: () => void | Promise<void>;
};

export type PuzzleNextLevelTarget = {
  profileId?: string;
  levelId?: string | null;
};

type PuzzleBoardPieceViewModel = {
  pieceId: string;
  row: number;
  col: number;
  correctRow: number;
  correctCol: number;
  mergedGroupId: string | null;
};

type PuzzleMergedGroupViewModel = {
  groupId: string;
  pieceIds: string[];
  anchorPieceId: string;
  minRow: number;
  minCol: number;
  rowSpan: number;
  colSpan: number;
  pieces: Array<
    PuzzleBoardPieceViewModel & {
      localRow: number;
      localCol: number;
    }
  >;
};

function boardCellKey(position: PuzzleCellPosition) {
  return `${position.row}:${position.col}`;
}

function buildBoardCells(board: PuzzleBoardSnapshot) {
  return Array.from({ length: board.rows * board.cols }, (_, index) => ({
    row: Math.floor(index / board.cols),
    col: index % board.cols,
  }));
}

function PuzzleUiSprite({
  src,
  kind,
  layout,
  className = '',
  withHitZone = false,
}: {
  src: string | null;
  kind: PuzzleUiSpriteKind;
  layout: PuzzleUiSpritesheetLayout | null;
  className?: string;
  withHitZone?: boolean;
}) {
  if (!src) {
    return null;
  }

  return (
    <span
      aria-hidden="true"
      data-puzzle-ui-sprite={kind}
      className={`relative inline-block shrink-0 bg-no-repeat ${className}`}
      style={buildPuzzleUiSpriteBackgroundStyle({ src, kind, layout })}
    >
      {withHitZone ? (
        <span
          aria-hidden="true"
          data-puzzle-ui-sprite-hit-zone={kind}
          className="puzzle-runtime-ui-sprite-hit-zone absolute"
          style={buildPuzzleUiSpriteHitZoneStyle({ kind, layout })}
        />
      ) : null}
    </span>
  );
}

function resolvePuzzleUiSpriteAspectRatio(
  kind: PuzzleUiSpriteKind,
  layout: PuzzleUiSpritesheetLayout | null,
  fallback: string,
) {
  const region = layout?.regions[kind];
  return region ? `${region.width} / ${region.height}` : fallback;
}

function buildMergedGroupViewModels(
  groups: PuzzleMergedGroupState[],
  pieces: PuzzleBoardPieceViewModel[],
) {
  const pieceById = new Map(pieces.map((piece) => [piece.pieceId, piece]));
  return groups
    .map<PuzzleMergedGroupViewModel | null>((group) => {
      const groupPieces = group.pieceIds
        .map((pieceId) => pieceById.get(pieceId) ?? null)
        .filter((piece): piece is PuzzleBoardPieceViewModel => Boolean(piece));
      if (groupPieces.length <= 1) {
        return null;
      }
      const rows = groupPieces.map((piece) => piece.row);
      const cols = groupPieces.map((piece) => piece.col);
      const minRow = Math.min(...rows);
      const maxRow = Math.max(...rows);
      const minCol = Math.min(...cols);
      const maxCol = Math.max(...cols);
      const anchorPiece = groupPieces[0];
      if (!anchorPiece) {
        return null;
      }

      return {
        groupId: group.groupId,
        pieceIds: group.pieceIds,
        anchorPieceId: anchorPiece.pieceId,
        minRow,
        minCol,
        rowSpan: maxRow - minRow + 1,
        colSpan: maxCol - minCol + 1,
        pieces: groupPieces.map((piece) => ({
          ...piece,
          localRow: piece.row - minRow,
          localCol: piece.col - minCol,
        })),
      };
    })
    .filter((group): group is PuzzleMergedGroupViewModel => Boolean(group));
}

function formatElapsedMs(elapsedMs: number | null | undefined) {
  const normalizedMs = Math.max(0, Math.round(elapsedMs ?? 0));
  const totalSeconds = Math.floor(normalizedMs / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  const centiseconds = Math.floor((normalizedMs % 1000) / 10);
  return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds
    .toString()
    .padStart(2, '0')}`;
}

function formatTimerMs(value: number | null | undefined) {
  const normalizedMs = Math.max(0, Math.ceil((value ?? 0) / 1000) * 1000);
  const totalSeconds = Math.floor(normalizedMs / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

function resolveActiveFreezeElapsedMs(
  level: PuzzleRuntimeLevelSnapshot,
  nowMs: number,
) {
  if (!level.freezeStartedAtMs || !level.freezeUntilMs) {
    return 0;
  }
  return Math.max(
    0,
    Math.min(nowMs, level.freezeUntilMs) - level.freezeStartedAtMs,
  );
}

function resolveRuntimeRemainingMs(
  level: PuzzleRuntimeLevelSnapshot,
  nowMs: number,
  uiPauseStartedAtMs: number | null,
) {
  if (level.status !== 'playing') {
    return level.remainingMs;
  }

  const timeLimitMs = level.timeLimitMs || level.remainingMs;
  const snapshotPauseElapsedMs = level.pauseStartedAtMs
    ? Math.max(0, nowMs - level.pauseStartedAtMs)
    : 0;
  const optimisticPauseElapsedMs =
    !level.pauseStartedAtMs && uiPauseStartedAtMs
      ? Math.max(0, nowMs - uiPauseStartedAtMs)
      : 0;
  const effectiveElapsedMs = Math.max(
    0,
    nowMs -
      level.startedAtMs -
      level.pausedAccumulatedMs -
      snapshotPauseElapsedMs -
      optimisticPauseElapsedMs -
      level.freezeAccumulatedMs -
      resolveActiveFreezeElapsedMs(level, nowMs),
  );

  return Math.max(0, timeLimitMs - effectiveElapsedMs);
}

function resolveRuntimeElapsedMs(
  level: PuzzleRuntimeLevelSnapshot,
  nowMs: number,
  uiPauseStartedAtMs: number | null,
) {
  // 进行中关卡的 elapsedMs 只在通关结算后写入，设置面板需要实时派生。
  if (level.status !== 'playing') {
    return (
      level.elapsedMs ?? Math.max(0, level.timeLimitMs - level.remainingMs)
    );
  }

  const timeLimitMs = level.timeLimitMs || level.remainingMs;
  const remainingMs = resolveRuntimeRemainingMs(
    level,
    nowMs,
    uiPauseStartedAtMs,
  );
  return Math.max(0, timeLimitMs - remainingMs);
}

const DEFAULT_PUZZLE_MUSIC_VOLUME = 0.6;
const PUZZLE_CLEAR_FLASH_DURATION_MS = 900;
const PUZZLE_CLEAR_DIALOG_DELAY_MS = 500;
const PUZZLE_MERGE_FLASH_DURATION_MS = 720;
const PUZZLE_HINT_DEMO_DURATION_MS = 1_250;
const PUZZLE_PIECE_PRESS_HAPTIC_PATTERN_MS = 12;
const PUZZLE_EXIT_REMODEL_PROMPT_STORAGE_PREFIX =
  'genarrative.puzzle-runtime.exit-remodel-prompt.v1';

const shownExitRemodelPromptProfileIds = new Set<string>();

function buildExitRemodelPromptStorageKey(profileId: string) {
  return `${PUZZLE_EXIT_REMODEL_PROMPT_STORAGE_PREFIX}:${encodeURIComponent(
    profileId,
  )}`;
}

function hasSeenExitRemodelPrompt(profileId: string) {
  const normalizedProfileId = profileId.trim();
  if (!normalizedProfileId) {
    return true;
  }
  if (shownExitRemodelPromptProfileIds.has(normalizedProfileId)) {
    if (typeof window === 'undefined') {
      return true;
    }
  }

  try {
    const seen =
      window.localStorage.getItem(
        buildExitRemodelPromptStorageKey(normalizedProfileId),
      ) === '1';
    if (seen) {
      shownExitRemodelPromptProfileIds.add(normalizedProfileId);
    }
    return seen;
  } catch {
    return shownExitRemodelPromptProfileIds.has(normalizedProfileId);
  }
}

function markExitRemodelPromptSeen(profileId: string) {
  const normalizedProfileId = profileId.trim();
  if (!normalizedProfileId) {
    return;
  }
  shownExitRemodelPromptProfileIds.add(normalizedProfileId);
  if (typeof window === 'undefined') {
    return;
  }

  try {
    window.localStorage.setItem(
      buildExitRemodelPromptStorageKey(normalizedProfileId),
      '1',
    );
  } catch {
    // 中文注释：隐私模式下 localStorage 可能不可写，内存集合足够兜底本次挂载周期。
  }
}

type PuzzlePropDialogState = {
  propKind: PuzzleRuntimePropKind;
  title: string;
};

type PuzzleMergeFlashState = {
  key: string;
  groupId: string;
  leftPercent: number;
  topPercent: number;
};

type PuzzleHintDemoState = {
  key: string;
  pieceIds: string[];
  offsetXPercent: number;
  offsetYPercent: number;
};

type PuzzleRuntimeDragTargetState = {
  pieceId: string;
  groupId: string | null;
};

function triggerPuzzlePiecePressHapticFeedback() {
  if (typeof navigator === 'undefined') {
    return;
  }

  const vibrate = navigator.vibrate;
  if (typeof vibrate !== 'function') {
    return;
  }

  vibrate.call(navigator, [PUZZLE_PIECE_PRESS_HAPTIC_PATTERN_MS]);
}

function triggerPuzzlePiecePressFeedback(volume: number) {
  triggerPuzzlePiecePressHapticFeedback();
  playRuntimeClickSound(undefined, volume);
}

/**
 * 拼图运行时壳层。
 * 前端维护运行时即时交互：交换、拖动、合并、拆分与本关通关在前端裁决。
 * 后端继续负责开始关卡、下一关候选、道具扣费、排行榜等服务侧能力。
 */
export function PuzzleRuntimeShell({
  run,
  isBusy = false,
  error = null,
  hideBackButton = false,
  hideExitControls = false,
  embedded = false,
  onBack,
  onRemodelWork,
  onSwapPieces,
  onDragPiece,
  onAdvanceNextLevel,
  onRestartLevel,
  onPauseChange,
  onUseProp,
  onTimeExpired,
}: PuzzleRuntimeShellProps) {
  const runtimeSvgClipId = useId();
  const authUi = useAuthUi();
  const [selectedPieceId, setSelectedPieceId] = useState<string | null>(null);
  const selectedPieceIdRef = useRef<string | null>(null);
  const selectedPieceBeforeInputRef = useRef<string | null>(null);
  const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false);
  const [isExitRemodelPromptOpen, setIsExitRemodelPromptOpen] = useState(false);
  const [propDialog, setPropDialog] = useState<PuzzlePropDialogState | null>(
    null,
  );
  const [isOriginalImageViewerVisible, setIsOriginalImageViewerVisible] =
    useState(false);
  const [isFreezeEffectVisible, setIsFreezeEffectVisible] = useState(false);
  const [isPropConfirming, setIsPropConfirming] = useState(false);
  const [propConfirmError, setPropConfirmError] = useState<string | null>(null);
  const [hintDemo, setHintDemo] = useState<PuzzleHintDemoState | null>(null);
  const [mergeFlash, setMergeFlash] = useState<PuzzleMergeFlashState | null>(
    null,
  );
  const [timerNowMs, setTimerNowMs] = useState(() => Date.now());
  const [uiPauseStartedAtMs, setUiPauseStartedAtMs] = useState<number | null>(
    null,
  );
  const onPauseChangeRef = useRef(onPauseChange);
  const onTimeExpiredRef = useRef(onTimeExpired);
  const previousUiPauseActiveRef = useRef(false);
  const pauseChangePromiseRef = useRef<Promise<void>>(Promise.resolve());
  const timeExpiredSyncKeyRef = useRef<string | null>(null);
  const clearSoundKeyRef = useRef<string | null>(null);
  const countdownSoundKeyRef = useRef<string | null>(null);
  const dragSessionRef = useRef<{
    pieceId: string;
    inputId: string;
    dragging: boolean;
    startX: number;
    startY: number;
    currentX: number;
    currentY: number;
  } | null>(null);
  const dragVisualTargetRef = useRef<{
    pieceId: string;
    groupId: string | null;
  } | null>(null);
  const pieceCellElementRefMap = useRef(new Map<string, HTMLDivElement>());
  const pieceElementRefMap = useRef(new Map<string, HTMLDivElement>());
  const groupElementRefMap = useRef(new Map<string, HTMLDivElement>());
  const [dragRenderTarget, setDragRenderTarget] = useState<{
    pieceId: string;
    groupId: string | null;
  } | null>(null);
  const [uiSpritesheetLayout, setUiSpritesheetLayout] =
    useState<PuzzleUiSpritesheetLayout | null>(null);
  const [isUiSpritesheetLayoutResolving, setIsUiSpritesheetLayoutResolving] =
    useState(false);
  const runtimeDragInputControllerRef = useRef(
    createRuntimeDragInputController<string>(),
  );
  const draggingTargetRef = useRef<PuzzleRuntimeDragTargetState | null>(null);
  const [dismissedClearKey, setDismissedClearKey] = useState<string | null>(
    null,
  );
  const [isClearFlashVisible, setIsClearFlashVisible] = useState(false);
  const [isClearResultReady, setIsClearResultReady] = useState(false);
  const clearPresentationKeyRef = useRef<string | null>(null);
  const clearPresentationTimeoutIdsRef = useRef<number[]>([]);
  const mergeGroupSignatureRef = useRef<string | null>(null);
  const hintDemoTimeoutRef = useRef<number | null>(null);
  const mergeFlashTimeoutRef = useRef<number | null>(null);
  const backgroundAudioRef = useRef<HTMLAudioElement | null>(null);
  const boardRef = useRef<HTMLDivElement | null>(null);
  const currentLevel = run?.currentLevel ?? null;
  const currentLevelRef = useRef(currentLevel);
  const board = currentLevel?.board ?? null;
  const displayRemainingMs = currentLevel
    ? resolveRuntimeRemainingMs(currentLevel, timerNowMs, uiPauseStartedAtMs)
    : 0;
  const displayElapsedMs = currentLevel
    ? resolveRuntimeElapsedMs(currentLevel, timerNowMs, uiPauseStartedAtMs)
    : 0;
  const runtimeStatus = currentLevel
    ? currentLevel.status === 'playing' && displayRemainingMs <= 0
      ? 'failed'
      : currentLevel.status
    : 'playing';
  const platformThemeClass =
    authUi?.platformTheme === 'dark'
      ? 'platform-theme--dark'
      : 'platform-theme--light';
  const isInteractionLocked =
    isBusy ||
    runtimeStatus !== 'playing' ||
    Boolean(propDialog) ||
    isOriginalImageViewerVisible;
  const clearResultKey = currentLevel
    ? `${run?.runId ?? 'run'}:${currentLevel.profileId}:${currentLevel.levelIndex}`
    : null;
  const runtimeRunId = run?.runId ?? null;
  const currentLevelIndex = currentLevel?.levelIndex ?? null;
  const currentLevelStartedAtMs = currentLevel?.startedAtMs ?? null;
  const currentLevelStatus = currentLevel?.status ?? null;
  const musicVolume = authUi?.musicVolume ?? DEFAULT_PUZZLE_MUSIC_VOLUME;
  const backgroundMusicSrc =
    currentLevel?.backgroundMusic?.audioSrc?.trim() || null;
  const levelAudioConfig = DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG;
  const onMusicVolumeChange = authUi?.setMusicVolume ?? (() => {});
  const {
    resolvedUrl: resolvedBackgroundMusicSrc,
    isResolving: isBackgroundMusicResolving,
  } = useResolvedAssetReadUrl(backgroundMusicSrc);
  const { resolvedUrl: resolvedCoverImage, isResolving: isCoverImageResolving } =
    useResolvedAssetReadUrl(
    currentLevel?.coverImageSrc ?? null,
  );
  const rawUiBackgroundImage = resolvePuzzleUiBackgroundSource(currentLevel);
  const {
    resolvedUrl: resolvedUiBackgroundImage,
    isResolving: isUiBackgroundResolving,
  } = useResolvedAssetReadUrl(rawUiBackgroundImage ?? null);
  const rawUiSpritesheetImage =
    currentLevel?.uiSpritesheetImageSrc?.trim() ||
    (currentLevel?.uiSpritesheetImageObjectKey?.trim()
      ? `/${currentLevel.uiSpritesheetImageObjectKey.trim().replace(/^\/+/u, '')}`
      : null);
  const {
    resolvedUrl: resolvedUiSpritesheetImage,
    isResolving: isUiSpritesheetResolving,
  } = useResolvedAssetReadUrl(rawUiSpritesheetImage);
  const hasUiSpritesheet = Boolean(resolvedUiSpritesheetImage);
  const tryPlayBackgroundMusic = useCallback(() => {
    const audio = backgroundAudioRef.current;
    if (!audio || !resolvedBackgroundMusicSrc || runtimeStatus !== 'playing') {
      if (audio) {
        audio.pause();
      }
      return;
    }
    audio.volume = Math.max(0, Math.min(1, musicVolume));
    void audio.play().catch(() => {});
  }, [musicVolume, resolvedBackgroundMusicSrc, runtimeStatus]);

  useEffect(() => {
    if (!rawUiSpritesheetImage) {
      setUiSpritesheetLayout(null);
      setIsUiSpritesheetLayoutResolving(false);
      return;
    }

    const controller = new AbortController();
    setUiSpritesheetLayout(null);
    setIsUiSpritesheetLayoutResolving(true);
    void loadPuzzleUiSpritesheetLayout(rawUiSpritesheetImage, {
      signal: controller.signal,
    })
      .then((layout) => {
        if (!controller.signal.aborted) {
          setUiSpritesheetLayout(layout);
          setIsUiSpritesheetLayoutResolving(false);
        }
      })
      .catch(() => {
        if (!controller.signal.aborted) {
          // 中文注释：私有图读取或 canvas 解析失败时回退旧固定六宫格，避免运行态按钮空白。
          setUiSpritesheetLayout(null);
          setIsUiSpritesheetLayoutResolving(false);
        }
      });

    return () => {
      controller.abort();
    };
  }, [rawUiSpritesheetImage]);

  useEffect(() => {
    currentLevelRef.current = currentLevel;
  }, [currentLevel]);

  useEffect(() => {
    tryPlayBackgroundMusic();
  }, [tryPlayBackgroundMusic]);

  const commitSelectedPieceId = (pieceId: string | null) => {
    selectedPieceIdRef.current = pieceId;
    setSelectedPieceId(pieceId);
  };

  const pieces = useMemo<PuzzleBoardPieceViewModel[]>(() => {
    if (!board) {
      return [];
    }
    return board.pieces.map((piece) => ({
      pieceId: piece.pieceId,
      row: piece.currentRow,
      col: piece.currentCol,
      correctRow: piece.correctRow,
      correctCol: piece.correctCol,
      mergedGroupId: piece.mergedGroupId,
    }));
  }, [board]);

  const mergedGroups = useMemo(() => {
    if (!board) {
      return [];
    }
    return buildMergedGroupViewModels(board.mergedGroups, pieces);
  }, [board, pieces]);

  const largestMovableGroup = useMemo(() => {
    const groups = mergedGroups.filter((group) =>
      group.pieces.some(
        (piece) =>
          piece.row !== piece.correctRow || piece.col !== piece.correctCol,
      ),
    );
    return (
      groups.sort(
        (left, right) =>
          right.pieceIds.length - left.pieceIds.length ||
          left.minRow - right.minRow ||
          left.minCol - right.minCol,
      )[0] ?? null
    );
  }, [mergedGroups]);

  const mergedCellKeys = useMemo(
    () =>
      new Set(
        mergedGroups.flatMap((group) =>
          group.pieces.map((piece) => boardCellKey(piece)),
        ),
      ),
    [mergedGroups],
  );

  const pieceByCell = useMemo(() => {
    const map = new Map<string, PuzzleBoardPieceViewModel>();
    for (const piece of pieces) {
      map.set(`${piece.row}:${piece.col}`, piece);
    }
    return map;
  }, [pieces]);
  const pieceById = useMemo(
    () => new Map(pieces.map((piece) => [piece.pieceId, piece])),
    [pieces],
  );
  const singlePieceClipId = sanitizeSvgId(
    `puzzle-single-piece-${runtimeSvgClipId}`,
  );
  const singlePieceClipUrl = `url(#${singlePieceClipId})`;

  useEffect(() => {
    const signature =
      board?.mergedGroups
        .map(
          (group) =>
            `${group.groupId}:${group.pieceIds.slice().sort().join(',')}`,
        )
        .sort()
        .join('|') ?? '';
    const previousSignature = mergeGroupSignatureRef.current;
    mergeGroupSignatureRef.current = signature;
    if (!previousSignature || !board || currentLevel?.status !== 'playing') {
      return;
    }

    const previousGroupSizes = new Map(
      previousSignature
        .split('|')
        .filter(Boolean)
        .map((entry) => {
          const [groupId, pieceIds = ''] = entry.split(':');
          return [groupId, pieceIds.split(',').filter(Boolean).length] as const;
        }),
    );
    const newGroup = mergedGroups.find(
      (group) =>
        group.pieceIds.length > 1 &&
        group.pieceIds.length > (previousGroupSizes.get(group.groupId) ?? 0),
    );
    if (!newGroup) {
      return;
    }

    if (mergeFlashTimeoutRef.current !== null) {
      window.clearTimeout(mergeFlashTimeoutRef.current);
    }
    setMergeFlash({
      key: `${newGroup.groupId}:${Date.now()}`,
      groupId: newGroup.groupId,
      leftPercent:
        ((newGroup.minCol + newGroup.colSpan / 2) / board.cols) * 100,
      topPercent: ((newGroup.minRow + newGroup.rowSpan / 2) / board.rows) * 100,
    });
    mergeFlashTimeoutRef.current = window.setTimeout(() => {
      setMergeFlash(null);
    }, PUZZLE_MERGE_FLASH_DURATION_MS);
  }, [board, currentLevel?.status, mergedGroups]);

  const resolvePieceCellElement = useCallback((pieceId: string) => {
    const pieceElement = pieceElementRefMap.current.get(pieceId) ?? null;
    const pieceCellElement =
      (pieceElement?.parentElement as HTMLDivElement | null) ??
      pieceCellElementRefMap.current.get(pieceId) ??
      null;
    return pieceCellElement;
  }, []);

  const resetDragVisualTarget = useCallback(() => {
    const dragVisualTarget = dragVisualTargetRef.current;
    setDragRenderTarget(null);
    if (!dragVisualTarget) {
      return;
    }

    const pieceElement = pieceElementRefMap.current.get(
      dragVisualTarget.pieceId,
    );
    const pieceCellElement = resolvePieceCellElement(dragVisualTarget.pieceId);
    if (pieceCellElement) {
      pieceCellElement.style.zIndex = '';
    }
    if (pieceElement) {
      pieceElement.style.transform = '';
      pieceElement.style.willChange = '';
      pieceElement.style.zIndex = '';
      pieceElement.style.opacity = '';
      pieceElement.style.transition = '';
    }

    if (dragVisualTarget.groupId) {
      const groupElement = groupElementRefMap.current.get(
        dragVisualTarget.groupId,
      );
      if (groupElement) {
        groupElement.style.transform = '';
        groupElement.style.willChange = '';
        groupElement.style.zIndex = '';
        groupElement.style.opacity = '';
        groupElement.style.transition = '';
      }
    }

    dragVisualTargetRef.current = null;
  }, [resolvePieceCellElement]);

  const resetDragInteractionState = () => {
    dragSessionRef.current = null;
    draggingTargetRef.current = null;
    resetDragVisualTarget();
  };

  const resetDragInteraction = () => {
    runtimeDragInputControllerRef.current.cancel();
  };

  const flushDragVisual = () => {
    const dragSession = dragSessionRef.current;
    if (!dragSession || !dragSession.dragging) {
      resetDragVisualTarget();
      return;
    }

    const piece = pieceById.get(dragSession.pieceId) ?? null;
    const groupId =
      draggingTargetRef.current?.groupId ?? piece?.mergedGroupId ?? null;
    const nextTarget = {
      pieceId: dragSession.pieceId,
      groupId,
    };
    dragVisualTargetRef.current = nextTarget;

    const offsetX = dragSession.currentX - dragSession.startX;
    const offsetY = dragSession.currentY - dragSession.startY;

    if (groupId) {
      const groupElement = groupElementRefMap.current.get(groupId);
      if (groupElement) {
        // 合并块拖动时直接提升整个组容器层级，确保完整拼块永远压在单块之上。
        groupElement.style.willChange = 'transform';
        groupElement.style.transform = `translate3d(${offsetX}px, ${offsetY}px, 0) scale(1.02)`;
        groupElement.style.zIndex = '90';
        groupElement.style.opacity = '0.95';
        groupElement.style.transition = 'none';
      }
      const pieceCellElement = resolvePieceCellElement(dragSession.pieceId);
      if (pieceCellElement) {
        pieceCellElement.style.zIndex = '';
      }
      const pieceElement = pieceElementRefMap.current.get(dragSession.pieceId);
      if (pieceElement) {
        pieceElement.style.transform = '';
        pieceElement.style.willChange = '';
        pieceElement.style.zIndex = '';
        pieceElement.style.opacity = '';
        pieceElement.style.transition = '';
      }
      return;
    }

    const pieceCellElement = resolvePieceCellElement(dragSession.pieceId);
    if (pieceCellElement) {
      // 单块拖动时提升所属格子的堆叠层级，避免被后绘制的拼块或合并块遮住。
      pieceCellElement.style.zIndex = '80';
    }
    const pieceElement = pieceElementRefMap.current.get(dragSession.pieceId);
    if (pieceElement) {
      pieceElement.style.willChange = 'transform';
      pieceElement.style.transform = `translate3d(${offsetX}px, ${offsetY}px, 0) scale(1.03)`;
      pieceElement.style.zIndex = '81';
      pieceElement.style.opacity = '0.95';
      pieceElement.style.transition = 'none';
    }
  };

  useEffect(
    () => () => {
      resetDragVisualTarget();
    },
    [resetDragVisualTarget],
  );

  const clearPresentationTimeouts = () => {
    for (const timeoutId of clearPresentationTimeoutIdsRef.current) {
      window.clearTimeout(timeoutId);
    }
    clearPresentationTimeoutIdsRef.current = [];
  };

  useEffect(
    () => () => {
      clearPresentationTimeouts();
    },
    [],
  );

  useEffect(() => {
    onPauseChangeRef.current = onPauseChange;
  }, [onPauseChange]);

  useEffect(() => {
    onTimeExpiredRef.current = onTimeExpired;
  }, [onTimeExpired]);

  const isUiPauseActive =
    isSettingsPanelOpen ||
    isExitRemodelPromptOpen ||
    Boolean(propDialog) ||
    isOriginalImageViewerVisible;

  useEffect(() => {
    if (previousUiPauseActiveRef.current === isUiPauseActive) {
      return;
    }
    previousUiPauseActiveRef.current = isUiPauseActive;
    setUiPauseStartedAtMs((currentValue) =>
      isUiPauseActive ? (currentValue ?? Date.now()) : null,
    );
    pauseChangePromiseRef.current = Promise.resolve(
      onPauseChangeRef.current?.(isUiPauseActive),
    ).catch(() => undefined);
  }, [isUiPauseActive]);

  useEffect(() => {
    if (currentLevelStatus !== 'playing') {
      return;
    }

    const timerId = window.setInterval(() => {
      setTimerNowMs(Date.now());
    }, 250);

    return () => window.clearInterval(timerId);
  }, [currentLevelIndex, currentLevelStatus, runtimeRunId]);

  useEffect(() => {
    if (
      !runtimeRunId ||
      currentLevelIndex === null ||
      currentLevelStartedAtMs === null ||
      currentLevelStatus === 'cleared'
    ) {
      return;
    }
    if (displayRemainingMs > 0) {
      return;
    }

    const syncKey = `${runtimeRunId}:${currentLevelIndex}:${currentLevelStartedAtMs}`;
    if (timeExpiredSyncKeyRef.current === syncKey) {
      return;
    }
    timeExpiredSyncKeyRef.current = syncKey;
    void onTimeExpiredRef.current?.();
  }, [
    currentLevelIndex,
    currentLevelStartedAtMs,
    currentLevelStatus,
    displayRemainingMs,
    runtimeRunId,
  ]);

  useEffect(() => {
    if (
      !runtimeRunId ||
      currentLevelStatus !== 'playing' ||
      currentLevelIndex === null ||
      currentLevelStartedAtMs === null
    ) {
      countdownSoundKeyRef.current = null;
      return;
    }
    const secondBucket =
      displayRemainingMs <= levelAudioConfig.countdownWarningThresholdMs
        ? resolveRuntimeCountdownSecondBucket(displayRemainingMs)
        : null;
    if (secondBucket === null) {
      countdownSoundKeyRef.current = null;
      return;
    }

    const soundKey = `${runtimeRunId}:${currentLevelIndex}:${currentLevelStartedAtMs}:${secondBucket}`;
    if (countdownSoundKeyRef.current === soundKey) {
      return;
    }
    countdownSoundKeyRef.current = soundKey;
    playRuntimeCountdownSound(musicVolume);
  }, [
    currentLevelIndex,
    currentLevelStartedAtMs,
    currentLevelStatus,
    displayRemainingMs,
    levelAudioConfig.countdownWarningThresholdMs,
    musicVolume,
    runtimeRunId,
  ]);

  useEffect(
    () => () => {
      if (hintDemoTimeoutRef.current !== null) {
        window.clearTimeout(hintDemoTimeoutRef.current);
      }
      if (mergeFlashTimeoutRef.current !== null) {
        window.clearTimeout(mergeFlashTimeoutRef.current);
      }
    },
    [],
  );

  useEffect(() => {
    if (!currentLevel || !clearResultKey) {
      clearPresentationKeyRef.current = null;
      clearPresentationTimeouts();
      setIsClearFlashVisible(false);
      setIsClearResultReady(false);
      return;
    }

    if (currentLevel.status !== 'cleared') {
      clearPresentationKeyRef.current = null;
      clearPresentationTimeouts();
      setIsClearFlashVisible(false);
      setIsClearResultReady(false);
      return;
    }

    if (
      dismissedClearKey === clearResultKey ||
      clearPresentationKeyRef.current === clearResultKey
    ) {
      return;
    }

    // 通关后先保留完整画面，再播放对角线闪光，最后延迟弹出结算弹窗。
    clearPresentationKeyRef.current = clearResultKey;
    if (clearSoundKeyRef.current !== clearResultKey) {
      clearSoundKeyRef.current = clearResultKey;
      playRuntimeLevelClearSound(musicVolume);
    }
    clearPresentationTimeouts();
    setIsClearFlashVisible(true);
    setIsClearResultReady(false);
    clearPresentationTimeoutIdsRef.current = [
      window.setTimeout(() => {
        setIsClearFlashVisible(false);
      }, PUZZLE_CLEAR_FLASH_DURATION_MS),
      window.setTimeout(() => {
        setIsClearResultReady(true);
      }, PUZZLE_CLEAR_FLASH_DURATION_MS + PUZZLE_CLEAR_DIALOG_DELAY_MS),
    ];
  }, [clearResultKey, currentLevel, dismissedClearKey, musicVolume]);

  const handlePieceTap = (
    pieceId: string,
    selectedPieceIdBeforeInput: string | null,
  ) => {
    if (isInteractionLocked) {
      return;
    }
    tryPlayBackgroundMusic();

    if (!selectedPieceIdBeforeInput) {
      commitSelectedPieceId(pieceId);
      return;
    }

    if (selectedPieceIdBeforeInput === pieceId) {
      commitSelectedPieceId(null);
      return;
    }

    onSwapPieces({
      firstPieceId: selectedPieceIdBeforeInput,
      secondPieceId: pieceId,
    });
    commitSelectedPieceId(null);
  };

  const resolvePuzzleRuntimeDragTarget = (
    pieceId: string,
  ): PuzzleRuntimeDragTargetState | null => {
    const sourcePiece = pieceById.get(pieceId) ?? null;
    if (!sourcePiece) {
      return null;
    }

    return {
      pieceId: sourcePiece.pieceId,
      groupId: sourcePiece.mergedGroupId ?? null,
    };
  };

  const commitPuzzleRuntimeDrag = (
    target: PuzzleRuntimeDragTargetState | null,
    point: RuntimeInputPoint,
  ) => {
    const dragSession = dragSessionRef.current;
    if (!target || !dragSession) {
      return;
    }

    const targetCell = board ? resolveRuntimeInputGridCell(point, board) : null;
    if (!targetCell) {
      return;
    }

    onDragPiece({
      pieceId: target.pieceId,
      targetRow: targetCell.row,
      targetCol: targetCell.col,
    });
  };

  const resolveBoardInputPointFromClient = (clientX: number, clientY: number) =>
    createRuntimeInputPointFromClient(
      clientX,
      clientY,
      readRuntimeInputElementBounds(boardRef.current),
    );

  const syncRuntimeDragFromController = (
    session: RuntimeDragInputSession<string> | null,
  ) => {
    if (!session) {
      return;
    }

    draggingTargetRef.current = resolvePuzzleRuntimeDragTarget(
      session.targetId,
    );
    dragSessionRef.current = {
      pieceId: session.targetId,
      inputId: session.inputId,
      dragging: session.dragging,
      startX: session.startPoint.clientX,
      startY: session.startPoint.clientY,
      currentX: session.currentPoint.clientX,
      currentY: session.currentPoint.clientY,
    };

    if (session.dragging) {
      flushDragVisual();
    }
  };

  runtimeDragInputControllerRef.current.setOptions({
    dragThresholdPx: 8,
    onPress: (session) => {
      draggingTargetRef.current = resolvePuzzleRuntimeDragTarget(
        session.targetId,
      );
      syncRuntimeDragFromController(session);
      selectedPieceBeforeInputRef.current = selectedPieceIdRef.current;
      commitSelectedPieceId(session.targetId);
      triggerPuzzlePiecePressFeedback(musicVolume);
    },
    onDragStart: (session) => {
      draggingTargetRef.current = resolvePuzzleRuntimeDragTarget(
        session.targetId,
      );
      syncRuntimeDragFromController(session);
      setDragRenderTarget({
        pieceId: session.targetId,
        groupId: draggingTargetRef.current?.groupId ?? null,
      });
      flushDragVisual();
    },
    onDragMove: (session) => {
      syncRuntimeDragFromController(session);
    },
    onDrop: (session) => {
      draggingTargetRef.current = resolvePuzzleRuntimeDragTarget(
        session.targetId,
      );
      syncRuntimeDragFromController(session);
      commitPuzzleRuntimeDrag(draggingTargetRef.current, session.currentPoint);
      commitSelectedPieceId(null);
      selectedPieceBeforeInputRef.current = null;
      resetDragInteractionState();
    },
    onTap: (session) => {
      handlePieceTap(session.targetId, selectedPieceBeforeInputRef.current);
      selectedPieceBeforeInputRef.current = null;
      resetDragInteractionState();
    },
    onCancel: () => {
      commitSelectedPieceId(selectedPieceBeforeInputRef.current);
      selectedPieceBeforeInputRef.current = null;
      resetDragInteractionState();
    },
  });

  if (!run || !currentLevel || !board) {
    return (
      <div
        className={`platform-ui-shell platform-theme ${platformThemeClass} puzzle-runtime-shell ${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex items-center justify-center`}
      >
        <div className="puzzle-runtime-pill flex items-center gap-2 rounded-full px-5 py-3 text-sm">
          <Loader2 className="h-4 w-4 animate-spin" />
          正在进入拼图关卡
        </div>
      </div>
    );
  }

  const handlePiecePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
    event.currentTarget.releasePointerCapture?.(event.pointerId);
    runtimeDragInputControllerRef.current.release({
      inputId: `pointer:${event.pointerId}`,
      point: resolveBoardInputPointFromClient(event.clientX, event.clientY),
    });
  };

  const handlePiecePointerDown = (
    pieceId: string,
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    if (isInteractionLocked) {
      return;
    }
    tryPlayBackgroundMusic();
    event.preventDefault();
    resetDragInteraction();
    event.currentTarget.setPointerCapture?.(event.pointerId);
    runtimeDragInputControllerRef.current.press({
      targetId: pieceId,
      inputId: `pointer:${event.pointerId}`,
      deviceKind: 'pointer',
      point: resolveBoardInputPointFromClient(event.clientX, event.clientY),
    });
  };

  const handlePiecePointerMove = (
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    event.preventDefault();
    runtimeDragInputControllerRef.current.move({
      inputId: `pointer:${event.pointerId}`,
      point: resolveBoardInputPointFromClient(event.clientX, event.clientY),
    });
  };

  const draggingPieceId = dragRenderTarget?.pieceId ?? null;
  const draggingGroupId = dragRenderTarget?.groupId ?? null;
  const shouldDisplaySelectedState = !dragRenderTarget;
  const freezeRemainingMs =
    currentLevel.freezeUntilMs && currentLevel.status === 'playing'
      ? Math.max(0, currentLevel.freezeUntilMs - timerNowMs)
      : 0;
  const statusLabel =
    runtimeStatus === 'cleared'
      ? '已通关'
      : runtimeStatus === 'failed'
        ? '失败'
        : '进行中';
  const nextLevelMode = run.nextLevelMode ?? 'none';
  const recommendedNextWorks = run.recommendedNextWorks ?? [];
  const hasSimilarWorkChoices =
    nextLevelMode === 'similarWorks' && recommendedNextWorks.length > 0;
  const canAdvanceDefaultNextLevel =
    currentLevel.status === 'cleared' &&
    (nextLevelMode === 'sameWork' ||
      (nextLevelMode === 'similarWorks'
        ? Boolean(run.nextLevelProfileId ?? run.recommendedNextProfileId) &&
          !hasSimilarWorkChoices
        : Boolean(run.recommendedNextProfileId)));
  const canShowNextAction = canAdvanceDefaultNextLevel || hasSimilarWorkChoices;
  const levelLabel = `第 ${currentLevel.levelIndex} 关`;
  const exitPromptProfileId = currentLevel.profileId.trim();
  const shouldHideBackButton = hideBackButton || hideExitControls;
  const leaderboardEntries =
    (currentLevel.leaderboardEntries ?? []).length > 0
      ? currentLevel.leaderboardEntries
      : (run.leaderboardEntries ?? []);
  const isClearResultOpen =
    currentLevel.status === 'cleared' &&
    dismissedClearKey !== clearResultKey &&
    isClearResultReady;
  const clearResultOverlayClassName = embedded
    ? `platform-ui-shell platform-theme ${platformThemeClass} puzzle-runtime-shell puzzle-runtime-modal-overlay puzzle-runtime-modal-overlay--fixed flex items-center justify-center px-4 py-6 backdrop-blur-sm`
    : 'puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm';
  const nextSpriteButtonClassName =
    'inline-flex h-12 appearance-none items-center justify-center border-0 bg-transparent p-0 leading-none shadow-none transition hover:brightness-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-button-primary-border)] disabled:cursor-not-allowed disabled:opacity-45';
  const nextSpriteButtonStyle = hasUiSpritesheet
    ? {
        ...buildPuzzleUiSpriteBackgroundStyle({
          src: resolvedUiSpritesheetImage,
          kind: 'next',
          layout: uiSpritesheetLayout,
        }),
        aspectRatio: resolvePuzzleUiSpriteAspectRatio(
          'next',
          uiSpritesheetLayout,
          '2 / 1',
        ),
      }
    : undefined;
  const handleBackRequest = () => {
    if (hideExitControls) {
      return;
    }

    if (
      onRemodelWork &&
      exitPromptProfileId &&
      !hasSeenExitRemodelPrompt(exitPromptProfileId)
    ) {
      markExitRemodelPromptSeen(exitPromptProfileId);
      setIsExitRemodelPromptOpen(true);
      return;
    }

    onBack();
  };

  const openPropDialog = (propKind: PuzzleRuntimePropKind, title: string) => {
    const canOpen =
      propKind === 'extendTime'
        ? runtimeStatus === 'failed'
        : runtimeStatus === 'playing';
    if (!canOpen) {
      return;
    }
    setPropConfirmError(null);
    setPropDialog({ propKind, title });
  };

  const playHintDemo = () => {
    const targetGroup = largestMovableGroup;
    const targetPieces = targetGroup?.pieces ?? [];
    const fallbackPiece = pieces.find(
      (piece) =>
        piece.row !== piece.correctRow || piece.col !== piece.correctCol,
    );
    const anchorPiece = targetPieces[0] ?? fallbackPiece ?? null;
    if (!anchorPiece) {
      return;
    }
    const pieceIds =
      targetPieces.length > 0
        ? targetPieces.map((piece) => piece.pieceId)
        : [anchorPiece.pieceId];
    const offsetXPercent =
      ((anchorPiece.correctCol - anchorPiece.col) / board.cols) * 100;
    const offsetYPercent =
      ((anchorPiece.correctRow - anchorPiece.row) / board.rows) * 100;
    setHintDemo({
      key: `${anchorPiece.pieceId}:${Date.now()}`,
      pieceIds,
      offsetXPercent,
      offsetYPercent,
    });
    if (hintDemoTimeoutRef.current !== null) {
      window.clearTimeout(hintDemoTimeoutRef.current);
    }
    hintDemoTimeoutRef.current = window.setTimeout(() => {
      setHintDemo(null);
    }, PUZZLE_HINT_DEMO_DURATION_MS);
  };

  const handleConfirmProp = async () => {
    if (!propDialog) {
      return;
    }
    const propKind = propDialog.propKind;
    setIsPropConfirming(true);
    setPropConfirmError(null);
    let useResult: PuzzleRunSnapshot | null | void = null;
    try {
      await pauseChangePromiseRef.current;
      useResult = await onUseProp?.(propKind);
      if (useResult === null) {
        return;
      }
      setPropDialog(null);
    } catch (error) {
      setPropConfirmError(
        error instanceof Error ? error.message : '使用拼图道具失败',
      );
      return;
    } finally {
      setIsPropConfirming(false);
    }
    if (propKind === 'hint') {
      playHintDemo();
    }
    if (propKind === 'reference') {
      setIsOriginalImageViewerVisible(true);
    }
    if (propKind === 'freezeTime') {
      // 中文注释：正式 run 可能在冻结确认期间已被服务端结算为失败态；
      // 这种边界同步只关闭确认窗，不再播放冻结成功反馈。
      const resultLevel =
        useResult && typeof useResult === 'object'
          ? useResult.currentLevel
          : currentLevelRef.current;
      if (resultLevel?.status === 'playing') {
        setIsFreezeEffectVisible(true);
        window.setTimeout(() => {
          setIsFreezeEffectVisible(false);
        }, 900);
      }
    }
    if (propKind === 'extendTime') {
      setTimerNowMs(Date.now());
    }
  };

  const clearResultDialog = isClearResultOpen ? (
    <PuzzleRuntimeModalShell
      titleId="puzzle-clear-result-title"
      overlayClassName={clearResultOverlayClassName}
      dialogClassName="puzzle-runtime-dialog flex max-h-[min(92vh,42rem)] w-full max-w-[28rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
    >
        <header className="puzzle-runtime-dialog__line flex items-start justify-between gap-3 border-b px-5 py-4">
          <div className="min-w-0">
            <div className="puzzle-runtime-primary-button mb-2 inline-flex h-9 w-9 items-center justify-center rounded-full">
              <Trophy className="h-4 w-4" />
            </div>
            <h2
              id="puzzle-clear-result-title"
              className="truncate text-lg font-black"
            >
              通关完成
            </h2>
            <div className="puzzle-runtime-dialog__soft mt-1 line-clamp-1 text-xs">
              {currentLevel.levelName}
            </div>
          </div>
          <PuzzleRuntimeDialogButton
            type="button"
            tone="secondary"
            aria-label="关闭通关弹窗"
            className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition hover:brightness-105"
            onClick={() => {
              setDismissedClearKey(clearResultKey);
            }}
          >
            <X className="h-4 w-4" />
          </PuzzleRuntimeDialogButton>
        </header>

        <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
          <div className="puzzle-runtime-stat-card flex items-center justify-between gap-4 rounded-[1rem] px-4 py-3">
            <div className="flex items-center gap-3">
              <span className="puzzle-runtime-pill inline-flex h-9 w-9 items-center justify-center rounded-full">
                <Clock className="h-4 w-4" />
              </span>
              <span className="puzzle-runtime-dialog__soft text-sm font-semibold">
                通关时间
              </span>
            </div>
            <span className="puzzle-runtime-tool-button__warm font-mono text-xl font-black">
              {formatElapsedMs(currentLevel.elapsedMs)}
            </span>
          </div>

          <div className="mt-4">
            <div className="mb-2 text-sm font-bold">排行榜</div>
            <div className="puzzle-runtime-dialog__line overflow-hidden rounded-[1rem] border">
              <div className="puzzle-runtime-leaderboard-head grid grid-cols-[3rem_minmax(0,1fr)_5.75rem] px-3 py-2 text-[11px] font-bold">
                <span>名次</span>
                <span>昵称</span>
                <span className="text-right">通关时间</span>
              </div>
              <div className="max-h-56 overflow-y-auto">
                {leaderboardEntries.length > 0 ? (
                  leaderboardEntries.map((entry) => (
                    <div
                      key={`${entry.rank}:${entry.nickname}:${entry.elapsedMs}`}
                      className={`grid min-h-[3.25rem] grid-cols-[3rem_minmax(0,1fr)_5.75rem] items-center gap-x-2 px-3 py-2.5 text-sm ${
                        entry.isCurrentPlayer
                          ? 'puzzle-runtime-leaderboard-row--active'
                          : 'puzzle-runtime-leaderboard-row border-t'
                      }`}
                    >
                      <span className="font-mono font-black">
                        #{entry.rank}
                      </span>
                      <span className="min-w-0">
                        <span className="block truncate font-semibold leading-tight">
                          {entry.nickname}
                        </span>
                        {entry.visibleTags?.length ? (
                          <span className="puzzle-runtime-leaderboard-tags">
                            {entry.visibleTags.map((tag) => (
                              <span
                                className="puzzle-runtime-leaderboard-tag"
                                key={tag}
                              >
                                {tag}
                              </span>
                            ))}
                          </span>
                        ) : null}
                      </span>
                      <span className="text-right font-mono text-xs font-bold">
                        {formatElapsedMs(entry.elapsedMs)}
                      </span>
                    </div>
                  ))
                ) : (
                  <div className="puzzle-runtime-dialog__soft flex min-h-24 items-center justify-center px-4 py-5 text-sm">
                    {isBusy ? '正在同步真实排行榜…' : '暂无真实排行榜成绩'}
                  </div>
                )}
              </div>
            </div>
          </div>

          {hasSimilarWorkChoices ? (
            <div className="mt-4">
              <div className="grid gap-2 sm:grid-cols-3">
                {recommendedNextWorks.slice(0, 3).map((item) => (
                  <PuzzleNextWorkCard
                    key={item.profileId}
                    item={item}
                    disabled={isBusy}
                    onClick={() => {
                      onAdvanceNextLevel({ profileId: item.profileId });
                    }}
                  />
                ))}
              </div>
            </div>
          ) : null}
        </div>

        {canAdvanceDefaultNextLevel ? (
          <PuzzleRuntimeDialogFooter className="flex items-center justify-end px-5 py-4">
            <button
              type="button"
              aria-label="下一关"
              data-puzzle-ui-sprite={hasUiSpritesheet ? 'next' : undefined}
              disabled={isBusy}
              onClick={() => {
                onAdvanceNextLevel({
                  profileId: run.nextLevelProfileId ?? undefined,
                  levelId: run.nextLevelId ?? null,
                });
              }}
              style={nextSpriteButtonStyle}
              className={
                hasUiSpritesheet
                  ? nextSpriteButtonClassName
                  : 'puzzle-runtime-primary-button inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-black transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-45'
              }
            >
              {hasUiSpritesheet ? null : (
                <>
                  {isBusy ? (
                    <Loader2 className="h-4 w-4 animate-spin" />
                  ) : (
                    <ArrowRight className="h-4 w-4" />
                  )}
                  下一关
                </>
              )}
            </button>
          </PuzzleRuntimeDialogFooter>
        ) : null}
    </PuzzleRuntimeModalShell>
  ) : null;
  const clearResultLayer =
    embedded && clearResultDialog && typeof document !== 'undefined'
      ? createPortal(clearResultDialog, document.body)
      : clearResultDialog;

  return (
    <div
      className={`platform-ui-shell platform-theme ${platformThemeClass} puzzle-runtime-shell ${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex justify-center`}
    >
      <RuntimeResourcePendingMarker
        source={backgroundMusicSrc}
        kind="audio"
        isPending={isBackgroundMusicResolving}
      />
      <RuntimeResourcePendingMarker
        source={currentLevel.coverImageSrc}
        kind="image"
        isPending={isCoverImageResolving}
      />
      <RuntimeResourcePendingMarker
        source={rawUiBackgroundImage}
        kind="image"
        isPending={isUiBackgroundResolving}
      />
      <RuntimeResourcePendingMarker
        source={rawUiSpritesheetImage}
        kind="image"
        isPending={isUiSpritesheetResolving || isUiSpritesheetLayoutResolving}
      />
      {resolvedBackgroundMusicSrc ? (
        <audio
          ref={backgroundAudioRef}
          src={resolvedBackgroundMusicSrc}
          loop
          preload="auto"
        />
      ) : null}
      <div className="puzzle-runtime-stage relative h-full w-full overflow-hidden">
        {resolvedUiBackgroundImage ? (
          <ResolvedAssetImage
            src={resolvedUiBackgroundImage}
            alt=""
            aria-hidden="true"
            className="absolute inset-0 h-full w-full object-cover"
          />
        ) : null}
        {currentLevel.coverImageSrc ? (
          <ResolvedAssetImage
            src={currentLevel.coverImageSrc}
            alt=""
            aria-hidden="true"
            className={`absolute inset-0 h-full w-full object-cover blur-2xl ${
              resolvedUiBackgroundImage ? 'opacity-[0.06]' : 'opacity-[0.16]'
            }`}
          />
        ) : null}
        <div
          className={`puzzle-runtime-stage__grid ${
            resolvedUiBackgroundImage ? 'opacity-20' : ''
          }`}
        />

        <div className="absolute left-0 top-0 z-20 w-full px-3 py-3 sm:px-4">
          <div className="grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-start gap-2 sm:grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] sm:gap-3">
            <button
              type="button"
              onClick={handleBackRequest}
              aria-label="返回上一页"
              disabled={shouldHideBackButton}
              className={`puzzle-runtime-icon-button h-10 w-10 items-center justify-center sm:h-11 sm:w-11 ${
                hasUiSpritesheet
                  ? 'puzzle-runtime-icon-button--sprite'
                  : 'rounded-full'
              } ${
                hasUiSpritesheet ? 'puzzle-runtime-icon-button--precise-hit' : ''
              } ${shouldHideBackButton ? 'invisible pointer-events-none' : 'inline-flex'}`}
            >
              <PuzzleUiSprite
                src={resolvedUiSpritesheetImage}
                kind="back"
                layout={uiSpritesheetLayout}
                className={`${
                  hasUiSpritesheet
                    ? 'puzzle-runtime-top-ui-sprite'
                    : 'h-7 w-7 rounded-full'
              }`}
                withHitZone
              />
              {resolvedUiSpritesheetImage ? null : (
                <ArrowLeft className="h-4 w-4" />
              )}
            </button>

            <div className="puzzle-runtime-header-card mx-auto flex max-w-[min(18.5rem,calc(100vw_-_6.5rem))] min-w-0 flex-col items-center text-center sm:max-w-[22rem]">
              <div className="puzzle-runtime-level-title-card flex max-w-full items-center justify-center gap-2 px-3.5 py-1.5 pr-4 sm:px-4 sm:pr-5">
                <span aria-hidden="true" className="puzzle-runtime-level-logo">
                  <img
                    src={puzzleLevelLogo}
                    alt=""
                    data-testid="puzzle-runtime-level-logo"
                    className="puzzle-runtime-level-logo__image"
                    draggable={false}
                  />
                </span>
                <span className="puzzle-runtime-level-badge shrink-0 text-[0.92rem] font-black sm:text-base">
                  {levelLabel}
                </span>
                <span className="min-w-0 truncate text-[0.92rem] font-black sm:text-base">
                  {currentLevel.levelName}
                </span>
              </div>
              <div
                className={`puzzle-runtime-timer-card -mt-px inline-flex items-center gap-1.5 px-3.5 py-1.5 font-mono text-lg font-black leading-none sm:text-xl ${
                  displayRemainingMs <= 20_000 && runtimeStatus === 'playing'
                    ? 'puzzle-runtime-timer--urgent'
                    : 'puzzle-runtime-timer'
                }`}
              >
                <Clock className="h-4 w-4 sm:h-5 sm:w-5" />
                {formatTimerMs(displayRemainingMs)}
              </div>
            </div>

            <button
              type="button"
              onClick={() => setIsSettingsPanelOpen(true)}
              aria-label="打开拼图设置"
              title="打开拼图设置"
              className={`puzzle-runtime-icon-button inline-flex h-10 w-10 items-center justify-center transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-button-primary-border)] sm:h-11 sm:w-11 ${
                hasUiSpritesheet
                  ? 'puzzle-runtime-icon-button--sprite'
                  : 'rounded-full'
              } ${
                hasUiSpritesheet ? 'puzzle-runtime-icon-button--precise-hit' : ''
              }`}
            >
              <PuzzleUiSprite
                src={resolvedUiSpritesheetImage}
                kind="settings"
                layout={uiSpritesheetLayout}
                className={`${
                  hasUiSpritesheet
                    ? 'puzzle-runtime-top-ui-sprite'
                    : 'h-7 w-7 rounded-full'
                }`}
                withHitZone
              />
              {resolvedUiSpritesheetImage ? null : (
                <Settings className="h-4 w-4" />
              )}
            </button>
          </div>
        </div>

        <div className="absolute inset-0 flex items-center justify-center px-1 py-3 pt-24 pb-32 sm:px-4 sm:py-4 sm:pt-24 sm:pb-28">
          <div
            ref={boardRef}
            data-testid="puzzle-board"
            className="puzzle-runtime-board relative grid aspect-square w-full max-w-[min(99vw,calc(100vh_-_14rem))] touch-none select-none overflow-hidden rounded-[1.2rem] border backdrop-blur-sm sm:max-w-[min(92vw,calc(100vh_-_13rem))] sm:rounded-[1.45rem]"
            style={{
              gridTemplateColumns: `repeat(${board.cols}, minmax(0, 1fr))`,
              gridTemplateRows: `repeat(${board.rows}, minmax(0, 1fr))`,
            }}
          >
            <svg
              aria-hidden="true"
              className="pointer-events-none absolute h-0 w-0 overflow-hidden"
              focusable="false"
            >
              <defs>
                <clipPath
                  id={singlePieceClipId}
                  clipPathUnits="objectBoundingBox"
                >
                  <path d={buildRoundedGridCellClipPath()} />
                </clipPath>
              </defs>
            </svg>
            {buildBoardCells(board).map((cell) => {
              const piece = pieceByCell.get(`${cell.row}:${cell.col}`) ?? null;
              const occupied = Boolean(piece);
              const isMerged = mergedCellKeys.has(boardCellKey(cell));
              const isSelected =
                shouldDisplaySelectedState &&
                !isMerged &&
                piece?.pieceId === selectedPieceId;

              return (
                <div
                  key={`${cell.row}:${cell.col}`}
                  ref={(node) => {
                    if (!piece) {
                      return;
                    }
                    if (node) {
                      pieceCellElementRefMap.current.set(piece.pieceId, node);
                      return;
                    }
                    pieceCellElementRefMap.current.delete(piece.pieceId);
                  }}
                  data-piece-cell-id={piece?.pieceId ?? undefined}
                  className="relative"
                  style={{
                    zIndex: resolveDraggedPieceCellLayer(
                      piece?.pieceId,
                      draggingPieceId,
                      isMerged,
                    ),
                    transform:
                      piece && hintDemo?.pieceIds.includes(piece.pieceId)
                        ? `translate(${hintDemo.offsetXPercent}%, ${hintDemo.offsetYPercent}%) scale(1.03)`
                        : undefined,
                    transition: hintDemo?.pieceIds.includes(
                      piece?.pieceId ?? '',
                    )
                      ? `transform ${PUZZLE_HINT_DEMO_DURATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)`
                      : undefined,
                  }}
                >
                  <div
                    ref={(node) => {
                      if (!piece) {
                        return;
                      }
                      if (node) {
                        pieceElementRefMap.current.set(piece.pieceId, node);
                        return;
                      }
                      pieceElementRefMap.current.delete(piece.pieceId);
                    }}
                    data-piece-id={piece?.pieceId ?? undefined}
                    className={`puzzle-runtime-piece relative flex h-full items-center justify-center overflow-hidden border-0 text-sm font-black ${
                      occupied
                        ? isSelected
                          ? 'puzzle-runtime-piece--selected'
                          : isMerged
                            ? 'puzzle-runtime-piece--merged'
                            : 'puzzle-runtime-piece--filled'
                        : 'puzzle-runtime-piece--empty'
                    } ${
                      isMerged
                        ? 'transition-opacity'
                        : 'transition-[opacity,transform]'
                    }`}
                    style={{
                      clipPath: isMerged ? undefined : singlePieceClipUrl,
                      WebkitClipPath: isMerged ? undefined : singlePieceClipUrl,
                      zIndex: resolveDraggedPieceLayer(
                        piece?.pieceId,
                        draggingPieceId,
                        isMerged,
                      ),
                    }}
                    onPointerDown={(event) => {
                      if (!piece || isMerged) {
                        return;
                      }
                      handlePiecePointerDown(piece.pieceId, event);
                    }}
                    onPointerMove={(event) => {
                      if (!piece || isMerged) {
                        return;
                      }
                      handlePiecePointerMove(event);
                    }}
                    onPointerUp={(event) => {
                      if (piece && !isMerged) {
                        handlePiecePointerUp(event);
                      }
                    }}
                    onPointerCancel={() => {
                      resetDragInteraction();
                    }}
                    onLostPointerCapture={() => {
                      resetDragInteraction();
                    }}
                  >
                    {piece ? (
                      <div className="relative h-full w-full overflow-hidden">
                        {isMerged ? null : resolvedCoverImage ? (
                          <div
                            className="absolute inset-0"
                            style={{
                              backgroundImage: `url("${resolvedCoverImage}")`,
                              backgroundSize: `${board.cols * 100}% ${board.rows * 100}%`,
                              backgroundPosition: `${
                                board.cols > 1
                                  ? (piece.correctCol / (board.cols - 1)) * 100
                                  : 0
                              }% ${
                                board.rows > 1
                                  ? (piece.correctRow / (board.rows - 1)) * 100
                                  : 0
                              }%`,
                            }}
                          />
                        ) : (
                          <div className="absolute inset-0 bg-[linear-gradient(145deg,rgba(251,191,36,0.4),rgba(76,29,19,0.72))]" />
                        )}
                      </div>
                    ) : (
                      ''
                    )}
                  </div>
                </div>
              );
            })}
            {mergedGroups.map((group) => {
              const mergedGroupClipId = sanitizeSvgId(
                `${runtimeSvgClipId}-${group.groupId}`,
              );
              const mergedGroupClipPath = buildMergedGroupOutlinePath(group);
              return (
                <div
                  key={group.groupId}
                  ref={(node) => {
                    if (node) {
                      groupElementRefMap.current.set(group.groupId, node);
                      return;
                    }
                    groupElementRefMap.current.delete(group.groupId);
                  }}
                  data-merged-group-id={group.groupId}
                  className="pointer-events-none absolute z-10"
                  style={{
                    zIndex: resolveDraggedMergedGroupLayer(
                      group.groupId,
                      draggingGroupId,
                    ),
                    transform: hintDemo?.pieceIds.some((pieceId) =>
                      group.pieceIds.includes(pieceId),
                    )
                      ? `translate(${hintDemo.offsetXPercent}%, ${hintDemo.offsetYPercent}%) scale(1.02)`
                      : undefined,
                    transition: hintDemo?.pieceIds.some((pieceId) =>
                      group.pieceIds.includes(pieceId),
                    )
                      ? `transform ${PUZZLE_HINT_DEMO_DURATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)`
                      : undefined,
                    left: `${(group.minCol / board.cols) * 100}%`,
                    top: `${(group.minRow / board.rows) * 100}%`,
                    width: `${(group.colSpan / board.cols) * 100}%`,
                    height: `${(group.rowSpan / board.rows) * 100}%`,
                  }}
                >
                  <svg
                    aria-hidden="true"
                    className="pointer-events-none absolute inset-0 h-full w-full overflow-visible"
                    data-merged-group-clip="true"
                    viewBox={`0 0 ${group.colSpan} ${group.rowSpan}`}
                    preserveAspectRatio="none"
                  >
                    <defs>
                      <clipPath
                        id={mergedGroupClipId}
                        clipPathUnits="userSpaceOnUse"
                      >
                        <path d={mergedGroupClipPath} />
                      </clipPath>
                    </defs>
                    <g clipPath={`url(#${mergedGroupClipId})`}>
                      {group.pieces.map((piece) => (
                        <g key={piece.pieceId} data-merged-piece-visual="true">
                          <clipPath
                            id={sanitizeSvgId(
                              `${runtimeSvgClipId}-${group.groupId}-${piece.pieceId}`,
                            )}
                            clipPathUnits="userSpaceOnUse"
                          >
                            <rect
                              x={piece.localCol}
                              y={piece.localRow}
                              width={1}
                              height={1}
                            />
                          </clipPath>
                          <g
                            clipPath={`url(#${sanitizeSvgId(
                              `${runtimeSvgClipId}-${group.groupId}-${piece.pieceId}`,
                            )})`}
                          >
                            {resolvedCoverImage ? (
                              <image
                                href={resolvedCoverImage}
                                xlinkHref={resolvedCoverImage}
                                x={piece.localCol - piece.correctCol}
                                y={piece.localRow - piece.correctRow}
                                width={board.cols}
                                height={board.rows}
                                preserveAspectRatio="none"
                              />
                            ) : (
                              <rect
                                x={piece.localCol}
                                y={piece.localRow}
                                width={1}
                                height={1}
                                fill="rgba(16,185,129,0.42)"
                              />
                            )}
                          </g>
                        </g>
                      ))}
                    </g>
                  </svg>
                  <div
                    className="pointer-events-none relative z-10 grid h-full w-full touch-none active:scale-[0.992]"
                    style={{
                      gridTemplateColumns: `repeat(${group.colSpan}, minmax(0, 1fr))`,
                      gridTemplateRows: `repeat(${group.rowSpan}, minmax(0, 1fr))`,
                    }}
                  >
                    {group.pieces.map((piece) => (
                      <div
                        key={piece.pieceId}
                        className="pointer-events-auto relative touch-none"
                        data-merged-piece-outline="true"
                        style={{
                          gridColumn: piece.localCol + 1,
                          gridRow: piece.localRow + 1,
                        }}
                        onPointerDown={(event) => {
                          handlePiecePointerDown(piece.pieceId, event);
                        }}
                        onPointerMove={(event) => {
                          handlePiecePointerMove(event);
                        }}
                        onPointerUp={(event) => {
                          handlePiecePointerUp(event);
                        }}
                        onPointerCancel={() => {
                          resetDragInteraction();
                        }}
                        onLostPointerCapture={() => {
                          resetDragInteraction();
                        }}
                      />
                    ))}
                  </div>
                </div>
              );
            })}
            {mergeFlash ? (
              <div
                key={mergeFlash.key}
                data-testid="puzzle-merge-flash"
                className="pointer-events-none absolute z-50"
                style={{
                  left: `${mergeFlash.leftPercent}%`,
                  top: `${mergeFlash.topPercent}%`,
                }}
              >
                <div className="puzzle-merge-center-flash" />
              </div>
            ) : null}
          </div>
        </div>

        <div className="absolute bottom-0 left-0 z-20 flex w-full flex-col items-center gap-2 px-3 py-3 sm:px-4 sm:py-4">
          {error ? (
            <PlatformRuntimeStatusToast tone="error" size="xs">
              {error}
            </PlatformRuntimeStatusToast>
          ) : null}
          {selectedPieceId &&
          shouldDisplaySelectedState &&
          runtimeStatus === 'playing' ? (
            <div className="puzzle-runtime-status-chip rounded-full px-3 py-1 text-xs">
              已选择
            </div>
          ) : null}
          {canShowNextAction ? (
            <button
              type="button"
              disabled={isBusy}
              data-puzzle-ui-sprite={hasUiSpritesheet ? 'next' : undefined}
              aria-label={hasSimilarWorkChoices ? '换个作品' : '下一关'}
              onClick={() => {
                if (hasSimilarWorkChoices) {
                  setDismissedClearKey(null);
                  setIsClearResultReady(true);
                  return;
                }
                onAdvanceNextLevel({
                  profileId: run.nextLevelProfileId ?? undefined,
                  levelId: run.nextLevelId ?? null,
                });
              }}
              style={nextSpriteButtonStyle}
              className={
                hasUiSpritesheet
                  ? nextSpriteButtonClassName
                  : 'puzzle-runtime-primary-button inline-flex min-h-11 items-center justify-center rounded-full px-5 py-2.5 text-sm font-bold transition hover:brightness-105 disabled:opacity-45'
              }
            >
              {hasUiSpritesheet ? null : (
                <>
                  <ArrowRight className="h-4 w-4" />
                  {hasSimilarWorkChoices ? '换个作品' : '下一关'}
                </>
              )}
            </button>
          ) : null}

          <div className="grid w-full max-w-[23rem] grid-cols-3 items-center justify-items-center gap-3 px-1 sm:max-w-[26rem] sm:gap-4">
            <button
              type="button"
              disabled={isInteractionLocked}
              aria-label="提示"
              onClick={() => openPropDialog('hint', '使用提示')}
              className={`puzzle-runtime-sprite-tool-button inline-flex h-16 w-full items-center justify-center transition disabled:opacity-45 sm:h-[4.5rem] ${
                resolvedUiSpritesheetImage
                  ? 'puzzle-runtime-sprite-tool-button--precise-hit'
                  : ''
              }`}
            >
              <PuzzleUiSprite
                src={resolvedUiSpritesheetImage}
                kind="hint"
                layout={uiSpritesheetLayout}
                className="puzzle-runtime-bottom-ui-sprite"
                withHitZone
              />
              {resolvedUiSpritesheetImage ? null : (
                <span className="puzzle-runtime-tool-button__warm text-lg font-black">
                  ?
                </span>
              )}
            </button>
            <button
              type="button"
              disabled={runtimeStatus !== 'playing' || !resolvedCoverImage}
              aria-label="原图"
              aria-pressed={isOriginalImageViewerVisible}
              onClick={() => {
                if (isOriginalImageViewerVisible) {
                  setIsOriginalImageViewerVisible(false);
                  return;
                }
                openPropDialog('reference', '查看原图');
              }}
              className={`puzzle-runtime-sprite-tool-button inline-flex h-16 w-full items-center justify-center transition disabled:opacity-45 sm:h-[4.5rem] ${
                isOriginalImageViewerVisible
                  ? 'puzzle-runtime-tool-button--active'
                  : 'puzzle-runtime-tool-button'
              } ${
                resolvedUiSpritesheetImage
                  ? 'puzzle-runtime-sprite-tool-button--precise-hit'
                  : ''
              }`}
            >
              <PuzzleUiSprite
                src={resolvedUiSpritesheetImage}
                kind="reference"
                layout={uiSpritesheetLayout}
                className="puzzle-runtime-bottom-ui-sprite"
                withHitZone
              />
              {resolvedUiSpritesheetImage ? null : (
                <span className="text-lg font-black">□</span>
              )}
            </button>
            <button
              type="button"
              disabled={isInteractionLocked}
              aria-label="冻结"
              onClick={() => openPropDialog('freezeTime', '冻结时间')}
              className={`puzzle-runtime-sprite-tool-button inline-flex h-16 w-full items-center justify-center transition disabled:opacity-45 sm:h-[4.5rem] ${
                resolvedUiSpritesheetImage
                  ? 'puzzle-runtime-sprite-tool-button--precise-hit'
                  : ''
              }`}
            >
              <PuzzleUiSprite
                src={resolvedUiSpritesheetImage}
                kind="freezeTime"
                layout={uiSpritesheetLayout}
                className="puzzle-runtime-bottom-ui-sprite"
                withHitZone
              />
              {resolvedUiSpritesheetImage ? null : (
                <span className="puzzle-runtime-tool-button__cool text-lg font-black">
                  *
                </span>
              )}
            </button>
          </div>
        </div>

        {isClearFlashVisible ? (
          <div
            data-testid="puzzle-clear-flash"
            aria-hidden="true"
            className="pointer-events-none absolute inset-0 z-30 overflow-hidden"
          >
            <div className="puzzle-clear-flash-overlay absolute inset-0" />
            <div className="puzzle-clear-flash-beam" />
          </div>
        ) : null}

        {freezeRemainingMs > 0 || isFreezeEffectVisible ? (
          <div
            data-testid="puzzle-freeze-effect"
            className="pointer-events-none absolute inset-0 z-30"
          >
            <div className="puzzle-freeze-effect-layer absolute inset-0 backdrop-saturate-150" />
            <div className="absolute left-1/2 top-28 -translate-x-1/2 rounded-full border border-cyan-100/30 bg-cyan-950/50 px-3 py-1.5 font-mono text-xs font-black text-cyan-50 backdrop-blur">
              {formatTimerMs(freezeRemainingMs)}
            </div>
          </div>
        ) : null}

        {isOriginalImageViewerVisible && resolvedCoverImage ? (
          <div
            data-testid="puzzle-original-viewer"
            className="puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-4 backdrop-blur-sm"
            style={{ background: 'rgba(2, 6, 23, 0.94)' }}
            onClick={() => {
              setIsOriginalImageViewerVisible(false);
            }}
          >
            <button
              type="button"
              aria-label="关闭原图"
              className="puzzle-runtime-secondary-button absolute right-4 top-4 inline-flex h-10 w-10 items-center justify-center rounded-full transition hover:brightness-105"
              onClick={(event) => {
                event.stopPropagation();
                setIsOriginalImageViewerVisible(false);
              }}
            >
              <X className="h-4 w-4" />
            </button>
            <div
              className="flex h-full w-full items-center justify-center"
              onClick={(event) => event.stopPropagation()}
            >
              <img
                src={resolvedCoverImage}
                alt={`${currentLevel.levelName} 原图`}
                className="max-h-[calc(100vh-2rem)] max-w-[calc(100vw-2rem)] object-contain"
              />
            </div>
          </div>
        ) : null}

        {propDialog ? (
          <PuzzleRuntimeModalShell
            titleId="puzzle-prop-confirm-title"
            onOverlayClick={() => {
              if (!isPropConfirming) {
                setPropDialog(null);
              }
            }}
          >
            <header className="puzzle-runtime-dialog__line flex items-center gap-3 border-b px-5 py-4">
              <span className="puzzle-runtime-primary-button inline-flex h-9 w-9 items-center justify-center rounded-full">
                <Sparkles className="h-4 w-4" />
              </span>
              <h2
                id="puzzle-prop-confirm-title"
                className="text-sm font-black"
              >
                {propDialog.title}
              </h2>
            </header>
            <div className="puzzle-runtime-dialog__body px-5 py-4 text-sm">
              消耗 1 泥点
              {propConfirmError ? (
                <PlatformRuntimeStatusToast
                  tone="error"
                  size="xs"
                  shape="rounded"
                  className="mt-3"
                >
                  {propConfirmError}
                </PlatformRuntimeStatusToast>
              ) : null}
            </div>
            <PuzzleRuntimeDialogFooter className="flex items-center justify-end gap-3 px-5 py-4">
              <PuzzleRuntimeDialogButton
                tone="secondary"
                onClick={() => setPropDialog(null)}
                disabled={isPropConfirming}
                className="rounded-full px-4 py-2 text-xs font-bold transition hover:brightness-105"
              >
                取消
              </PuzzleRuntimeDialogButton>
              <PuzzleRuntimeDialogButton
                tone="primary"
                disabled={isPropConfirming}
                onClick={() => {
                  void handleConfirmProp();
                }}
                className="inline-flex items-center gap-2 rounded-full px-5 py-2 text-sm font-black transition hover:brightness-105 disabled:opacity-60"
              >
                {isPropConfirming ? (
                  <Loader2 className="h-3.5 w-3.5 animate-spin" />
                ) : null}
                确定
              </PuzzleRuntimeDialogButton>
            </PuzzleRuntimeDialogFooter>
          </PuzzleRuntimeModalShell>
        ) : null}

        {isSettingsPanelOpen ? (
          <PuzzleRuntimeModalShell
            titleId="puzzle-settings-title"
            overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center p-3 backdrop-blur-sm sm:p-4"
            dialogClassName="puzzle-runtime-dialog flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.24)]"
            onOverlayClick={() => setIsSettingsPanelOpen(false)}
          >
            <header className="puzzle-runtime-dialog__line relative border-b px-4 py-3 sm:px-5 sm:py-4">
              <div className="min-w-0 pr-10">
                <h2 id="puzzle-settings-title" className="text-sm font-semibold">
                  拼图设置
                </h2>
                <div className="puzzle-runtime-dialog__soft mt-1 text-[11px]">
                  {hideExitControls
                    ? '调整音乐音量，查看本局进度。'
                    : '调整音乐音量，查看本局进度，或返回上一页。'}
                </div>
              </div>
              <button
                type="button"
                aria-label="关闭拼图设置"
                onClick={() => setIsSettingsPanelOpen(false)}
                className="puzzle-runtime-dialog__soft absolute right-4 top-3 p-1 transition-colors hover:brightness-75 sm:right-5 sm:top-4"
              >
                <X className="h-4 w-4" />
              </button>
            </header>

            <div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
              <div className="puzzle-runtime-settings-card rounded-2xl p-4">
                <div className="flex items-start justify-between gap-3">
                  <div>
                    <div className="puzzle-runtime-tool-button__cool text-[10px] tracking-[0.24em]">
                      音频
                    </div>
                    <div className="mt-2 text-sm font-semibold">音乐音量</div>
                  </div>
                  <div className="puzzle-runtime-pill rounded-full px-2 py-1 text-xs">
                    {Math.round(musicVolume * 100)}%
                  </div>
                </div>

                <div className="mt-4 flex items-center gap-3">
                  <input
                    type="range"
                    min={0}
                    max={100}
                    step={1}
                    aria-label="拼图音乐音量"
                    value={Math.round(musicVolume * 100)}
                    onChange={(event) =>
                      onMusicVolumeChange(Number(event.currentTarget.value) / 100)
                    }
                    className="h-2 w-full cursor-pointer accent-[var(--puzzle-runtime-accent-text)]"
                  />
                </div>
              </div>

              <div className="puzzle-runtime-settings-card rounded-2xl px-4 py-3">
                <div className="puzzle-runtime-dialog__soft text-[10px] uppercase tracking-[0.18em]">
                  本局进度
                </div>
                <div className="puzzle-runtime-dialog__body mt-3 space-y-2 text-sm">
                  <div className="flex items-center justify-between gap-3">
                    <span className="puzzle-runtime-dialog__soft">关卡</span>
                    <span className="font-semibold">{levelLabel}</span>
                  </div>
                  <div className="flex items-center justify-between gap-3">
                    <span className="puzzle-runtime-dialog__soft">
                      已完成关卡
                    </span>
                    <span className="font-semibold">{run.clearedLevelCount}</span>
                  </div>
                  <div className="flex items-center justify-between gap-3">
                    <span className="puzzle-runtime-dialog__soft">当前状态</span>
                    <span className="font-semibold">{statusLabel}</span>
                  </div>
                  <div className="flex items-center justify-between gap-3">
                    <span className="puzzle-runtime-dialog__soft">当前用时</span>
                    <span className="font-mono font-semibold">
                      {formatElapsedMs(displayElapsedMs)}
                    </span>
                  </div>
                </div>
              </div>
            </div>

            <PuzzleRuntimeDialogFooter className="flex items-center justify-end gap-3 px-4 py-3 sm:px-5">
              <PuzzleRuntimeDialogButton
                tone="secondary"
                onClick={() => setIsSettingsPanelOpen(false)}
                className="rounded-full px-3 py-1.5 text-[11px] transition hover:brightness-105"
              >
                继续拼图
              </PuzzleRuntimeDialogButton>
              {!hideExitControls ? (
                <PuzzleRuntimeDialogButton
                  tone="primary"
                  onClick={() => {
                    setIsSettingsPanelOpen(false);
                    onBack();
                  }}
                  className={`rounded-full px-4 py-2 text-sm font-bold transition hover:brightness-105 ${
                    shouldHideBackButton ? 'hidden' : ''
                  }`}
                >
                  返回上一页
                </PuzzleRuntimeDialogButton>
              ) : null}
            </PuzzleRuntimeDialogFooter>
          </PuzzleRuntimeModalShell>
        ) : null}

        {isExitRemodelPromptOpen && !hideExitControls ? (
          <PuzzleRuntimeModalShell
            titleId="puzzle-exit-remodel-title"
            overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-50 flex items-center justify-center px-4 py-6 backdrop-blur-md"
            dialogClassName="puzzle-runtime-dialog relative flex w-full max-w-[21rem] flex-col overflow-hidden rounded-[1.35rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
          >
            <div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-[var(--puzzle-runtime-accent-text)] to-transparent" />
            <header className="flex flex-col items-center px-6 pt-7 text-center">
              <div className="puzzle-runtime-stat-card mb-4 grid h-14 w-14 place-items-center rounded-2xl">
                <Sparkles className="puzzle-runtime-tool-button__warm h-7 w-7" />
              </div>
              <h2
                id="puzzle-exit-remodel-title"
                className="text-[1.75rem] font-black leading-[1.08]"
              >
                体验不佳？
                <br />
                试试改造功能!
              </h2>
            </header>
            <PuzzleRuntimeDialogFooter
              className="grid gap-3 px-5 pb-5 pt-6"
              framed={false}
            >
              <PuzzleRuntimeDialogButton
                tone="primary"
                disabled={isBusy}
                onClick={() => {
                  setIsExitRemodelPromptOpen(false);
                  void onRemodelWork?.(exitPromptProfileId);
                }}
                className="min-h-[3.25rem] rounded-2xl px-5 text-sm font-black transition hover:brightness-105 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50"
              >
                作品改造
              </PuzzleRuntimeDialogButton>
              <PuzzleRuntimeDialogButton
                tone="secondary"
                onClick={() => {
                  setIsExitRemodelPromptOpen(false);
                  onBack();
                }}
                className="min-h-[3rem] rounded-2xl px-5 text-sm font-bold transition hover:brightness-105 active:translate-y-px"
              >
                保存并退出
              </PuzzleRuntimeDialogButton>
            </PuzzleRuntimeDialogFooter>
          </PuzzleRuntimeModalShell>
        ) : null}

        {runtimeStatus === 'failed' ? (
          <PuzzleRuntimeModalShell
            titleId="puzzle-failed-title"
            overlayClassName="puzzle-runtime-modal-overlay absolute inset-0 z-40 flex items-center justify-center px-4 py-6 backdrop-blur-sm"
            dialogClassName="puzzle-runtime-dialog flex w-full max-w-[24rem] flex-col overflow-hidden rounded-[1.5rem] shadow-[0_28px_90px_rgba(0,0,0,0.28)]"
          >
            <header className="puzzle-runtime-dialog__line border-b px-5 py-4">
              <h2 id="puzzle-failed-title" className="text-lg font-black">
                关卡失败
              </h2>
              <div className="puzzle-runtime-dialog__soft mt-1 text-xs">
                {currentLevel.levelName}
              </div>
            </header>
            <PuzzleRuntimeDialogFooter className="grid grid-cols-2 gap-3 px-5 py-4">
              <PuzzleRuntimeDialogButton
                tone="secondary"
                disabled={isBusy}
                onClick={() => {
                  void onRestartLevel?.();
                }}
                className="rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
              >
                重新开始
              </PuzzleRuntimeDialogButton>
              <PuzzleRuntimeDialogButton
                tone="primary"
                disabled={isBusy}
                onClick={() => openPropDialog('extendTime', '继续1分钟')}
                className="rounded-full px-4 py-2.5 text-sm font-black transition hover:brightness-105 disabled:opacity-50"
              >
                继续1分钟
              </PuzzleRuntimeDialogButton>
            </PuzzleRuntimeDialogFooter>
          </PuzzleRuntimeModalShell>
        ) : null}

        {clearResultLayer}
      </div>
    </div>
  );
}

function PuzzleNextWorkCard({
  item,
  disabled,
  onClick,
}: {
  item: PuzzleRecommendedNextWork;
  disabled: boolean;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      disabled={disabled}
      onClick={onClick}
      className="puzzle-runtime-next-card group grid min-h-[5.75rem] grid-cols-[4.5rem_minmax(0,1fr)] overflow-hidden rounded-[1rem] text-left transition disabled:cursor-not-allowed disabled:opacity-45 sm:grid-cols-1"
    >
      <div className="puzzle-runtime-next-card__media relative min-h-full sm:aspect-[1.35]">
        {item.coverImageSrc ? (
          <ResolvedAssetImage
            src={item.coverImageSrc}
            alt={item.levelName}
            className="h-full w-full object-cover"
          />
        ) : (
          <div className="h-full w-full bg-[linear-gradient(145deg,rgba(20,184,166,0.34),rgba(15,23,42,0.88))]" />
        )}
        <div className="puzzle-runtime-next-card-overlay absolute inset-0 transition group-hover:opacity-0" />
      </div>
      <div className="min-w-0 px-3 py-2.5">
        <div className="truncate text-sm font-black">{item.levelName}</div>
        <div className="puzzle-runtime-dialog__soft mt-1 truncate text-xs font-semibold">
          {item.authorDisplayName}
        </div>
        <div className="mt-2 flex flex-wrap gap-1">
          {item.themeTags.slice(0, 2).map((tag) => (
            <span
              key={tag}
              className="puzzle-runtime-next-card__tag max-w-full truncate rounded-full px-2 py-0.5 text-[10px] font-bold"
            >
              {tag}
            </span>
          ))}
        </div>
      </div>
    </button>
  );
}

export default PuzzleRuntimeShell;
