import {
  ArrowLeft,
  ArrowRight,
  Clock,
  Eye,
  Lightbulb,
  Loader2,
  Snowflake,
  Sparkles,
  Trophy,
} from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';

import type {
  DragPuzzlePieceRequest,
  PuzzleBoardSnapshot,
  PuzzleCellPosition,
  PuzzleMergedGroupState,
  PuzzleRecommendedNextWork,
  PuzzleRunSnapshot,
  PuzzleRuntimeLevelSnapshot,
  PuzzleRuntimePropKind,
  SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../../uiAssets';
import { useAuthUi } from '../auth/AuthUiContext';
import { PixelIcon } from '../PixelIcon';
import { ResolvedAssetImage } from '../ResolvedAssetImage';

type PuzzleRuntimeShellProps = {
  run: PuzzleRunSnapshot | null;
  isBusy?: boolean;
  error?: string | null;
  onBack: () => 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 buildLocalCellKey(row: number, col: number) {
  return `${row}:${col}`;
}

export function resolveDraggedPieceCellLayer(
  pieceId: string | null | undefined,
  draggingPieceId: string | null,
  isMerged: boolean,
) {
  if (!pieceId || isMerged || pieceId !== draggingPieceId) {
    return undefined;
  }
  return 80;
}

export function resolveDraggedPieceLayer(
  pieceId: string | null | undefined,
  draggingPieceId: string | null,
  isMerged: boolean,
) {
  if (!pieceId || isMerged || pieceId !== draggingPieceId) {
    return undefined;
  }
  return 81;
}

export function resolveDraggedMergedGroupLayer(
  groupId: string,
  draggingGroupId: string | null,
) {
  return groupId === draggingGroupId ? 90 : undefined;
}

function resolveMergedPieceOutlineClass(
  group: PuzzleMergedGroupViewModel,
  piece: PuzzleMergedGroupViewModel['pieces'][number],
) {
  const groupCellKeys = new Set(
    group.pieces.map((groupPiece) =>
      buildLocalCellKey(groupPiece.localRow, groupPiece.localCol),
    ),
  );
  const hasCell = (row: number, col: number) =>
    groupCellKeys.has(buildLocalCellKey(row, col));
  const hasTopBoundary = (row: number, col: number) => !hasCell(row - 1, col);
  const hasRightBoundary = (row: number, col: number) => !hasCell(row, col + 1);
  const hasBottomBoundary = (row: number, col: number) =>
    !hasCell(row + 1, col);
  const hasLeftBoundary = (row: number, col: number) => !hasCell(row, col - 1);
  const hasTopEdge = !groupCellKeys.has(
    buildLocalCellKey(piece.localRow - 1, piece.localCol),
  );
  const hasRightEdge = !groupCellKeys.has(
    buildLocalCellKey(piece.localRow, piece.localCol + 1),
  );
  const hasBottomEdge = !groupCellKeys.has(
    buildLocalCellKey(piece.localRow + 1, piece.localCol),
  );
  const hasLeftEdge = !groupCellKeys.has(
    buildLocalCellKey(piece.localRow, piece.localCol - 1),
  );
  const topLeftRadius =
    hasTopEdge && hasLeftEdge
      ? 'rounded-tl-[0.85rem]'
      : (!hasTopEdge && !hasLeftEdge) ||
          (hasTopEdge &&
            !hasLeftEdge &&
            !hasTopBoundary(piece.localRow, piece.localCol - 1)) ||
          (hasLeftEdge &&
            !hasTopEdge &&
            !hasLeftBoundary(piece.localRow - 1, piece.localCol))
        ? 'rounded-tl-[0.35rem]'
        : 'rounded-tl-none';
  const topRightRadius =
    hasTopEdge && hasRightEdge
      ? 'rounded-tr-[0.85rem]'
      : (!hasTopEdge && !hasRightEdge) ||
          (hasTopEdge &&
            !hasRightEdge &&
            !hasTopBoundary(piece.localRow, piece.localCol + 1)) ||
          (hasRightEdge &&
            !hasTopEdge &&
            !hasRightBoundary(piece.localRow - 1, piece.localCol))
        ? 'rounded-tr-[0.35rem]'
        : 'rounded-tr-none';
  const bottomRightRadius =
    hasBottomEdge && hasRightEdge
      ? 'rounded-br-[0.85rem]'
      : (!hasBottomEdge && !hasRightEdge) ||
          (hasBottomEdge &&
            !hasRightEdge &&
            !hasBottomBoundary(piece.localRow, piece.localCol + 1)) ||
          (hasRightEdge &&
            !hasBottomEdge &&
            !hasRightBoundary(piece.localRow + 1, piece.localCol))
        ? 'rounded-br-[0.35rem]'
        : 'rounded-br-none';
  const bottomLeftRadius =
    hasBottomEdge && hasLeftEdge
      ? 'rounded-bl-[0.85rem]'
      : (!hasBottomEdge && !hasLeftEdge) ||
          (hasBottomEdge &&
            !hasLeftEdge &&
            !hasBottomBoundary(piece.localRow, piece.localCol - 1)) ||
          (hasLeftEdge &&
            !hasBottomEdge &&
            !hasLeftBoundary(piece.localRow + 1, piece.localCol))
        ? 'rounded-bl-[0.35rem]'
        : 'rounded-bl-none';
  return [
    hasTopEdge ? 'border-t-2' : 'border-t-0',
    hasRightEdge ? 'border-r-2' : 'border-r-0',
    hasBottomEdge ? 'border-b-2' : 'border-b-0',
    hasLeftEdge ? 'border-l-2' : 'border-l-0',
    topLeftRadius,
    topRightRadius,
    bottomRightRadius,
    bottomLeftRadius,
  ].join(' ');
}

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 resolveAuthorAvatarLabel(authorDisplayName: string) {
  return authorDisplayName.trim().slice(0, 1) || '玩';
}

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);
}

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;

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;
};

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]);
}

/**
 * 拼图运行时壳层。
 * 前端维护运行时即时交互：交换、拖动、合并、拆分与本关通关在前端裁决。
 * 后端继续负责开始关卡、下一关候选、道具扣费、排行榜等服务侧能力。
 */
export function PuzzleRuntimeShell({
  run,
  isBusy = false,
  error = null,
  onBack,
  onSwapPieces,
  onDragPiece,
  onAdvanceNextLevel,
  onRestartLevel,
  onPauseChange,
  onUseProp,
  onTimeExpired,
}: PuzzleRuntimeShellProps) {
  const authUi = useAuthUi();
  const [selectedPieceId, setSelectedPieceId] = useState<string | null>(null);
  const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false);
  const [propDialog, setPropDialog] = useState<PuzzlePropDialogState | null>(
    null,
  );
  const [isOriginalOverlayVisible, setIsOriginalOverlayVisible] =
    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 dragSessionRef = useRef<{
    pieceId: string;
    pointerId: number;
    dragging: boolean;
    startX: number;
    startY: number;
    currentX: number;
    currentY: number;
  } | null>(null);
  const dragVisualTargetRef = useRef<{
    pieceId: string;
    groupId: string | null;
  } | null>(null);
  const dragVisualFrameRef = useRef<number | null>(null);
  const dragOffsetRef = useRef<{ x: number; y: number } | 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 [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 boardRef = useRef<HTMLDivElement | null>(null);
  const currentLevel = run?.currentLevel ?? null;
  const board = currentLevel?.board ?? null;
  const displayRemainingMs = currentLevel
    ? resolveRuntimeRemainingMs(currentLevel, timerNowMs, uiPauseStartedAtMs)
    : 0;
  const runtimeStatus = currentLevel
    ? currentLevel.status === 'playing' && displayRemainingMs <= 0
      ? 'failed'
      : currentLevel.status
    : 'playing';
  const clearResultKey = currentLevel
    ? `${run?.runId ?? 'run'}:${currentLevel.profileId}:${currentLevel.levelIndex}`
    : null;
  const musicVolume = authUi?.musicVolume ?? DEFAULT_PUZZLE_MUSIC_VOLUME;
  const onMusicVolumeChange = authUi?.setMusicVolume ?? (() => {});
  const { resolvedUrl: resolvedCoverImage } = useResolvedAssetReadUrl(
    currentLevel?.coverImageSrc ?? null,
  );

  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],
  );

  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 = (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 = () => {
    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 = '';
    }

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

    dragVisualTargetRef.current = null;
  };

  const cancelDragVisualFrame = () => {
    if (dragVisualFrameRef.current === null) {
      return;
    }
    window.cancelAnimationFrame(dragVisualFrameRef.current);
    dragVisualFrameRef.current = null;
  };

  const resetDragInteraction = () => {
    cancelDragVisualFrame();
    dragOffsetRef.current = null;
    dragSessionRef.current = null;
    resetDragVisualTarget();
  };

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

    const piece = pieceById.get(dragSession.pieceId) ?? null;
    const groupId = piece?.mergedGroupId ?? null;
    const nextTarget = {
      pieceId: dragSession.pieceId,
      groupId,
    };
    const previousTarget = dragVisualTargetRef.current;
    if (
      previousTarget &&
      (previousTarget.pieceId !== nextTarget.pieceId ||
        previousTarget.groupId !== nextTarget.groupId)
    ) {
      resetDragVisualTarget();
    }
    dragVisualTargetRef.current = nextTarget;
    setDragRenderTarget((currentTarget) => {
      if (
        currentTarget?.pieceId === nextTarget.pieceId &&
        currentTarget.groupId === nextTarget.groupId
      ) {
        return currentTarget;
      }
      return nextTarget;
    });

    const offsetX = dragSession.currentX - dragSession.startX;
    const offsetY = dragSession.currentY - dragSession.startY;
    dragOffsetRef.current = { x: offsetX, y: offsetY };

    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';
      }
      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 = '';
      }
      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';
    }
  };

  const scheduleDragVisual = () => {
    if (dragVisualFrameRef.current !== null) {
      return;
    }
    dragVisualFrameRef.current = window.requestAnimationFrame(flushDragVisual);
  };

  useEffect(
    () => () => {
      cancelDragVisualFrame();
      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 || Boolean(propDialog) || isOriginalOverlayVisible;

  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 (!currentLevel || currentLevel.status !== 'playing') {
      return;
    }

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

    return () => window.clearInterval(timerId);
  }, [currentLevel?.levelIndex, currentLevel?.runId, currentLevel?.status]);

  useEffect(() => {
    if (!run || !currentLevel || currentLevel.status !== 'playing') {
      return;
    }
    if (displayRemainingMs > 0) {
      return;
    }

    const syncKey = `${run.runId}:${currentLevel.levelIndex}:${currentLevel.startedAtMs}`;
    if (timeExpiredSyncKeyRef.current === syncKey) {
      return;
    }
    timeExpiredSyncKeyRef.current = syncKey;
    void onTimeExpiredRef.current?.();
  }, [
    currentLevel?.levelIndex,
    currentLevel?.startedAtMs,
    currentLevel?.status,
    displayRemainingMs,
    run?.runId,
  ]);

  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;
    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]);

  if (!run || !currentLevel || !board) {
    return (
      <div className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950 text-white">
        <div className="flex items-center gap-2 rounded-full bg-white/10 px-5 py-3 text-sm">
          <Loader2 className="h-4 w-4 animate-spin" />
          正在进入拼图关卡
        </div>
      </div>
    );
  }

  const handlePieceClick = (pieceId: string) => {
    if (isInteractionLocked) {
      return;
    }

    if (!selectedPieceId) {
      setSelectedPieceId(pieceId);
      return;
    }

    if (selectedPieceId === pieceId) {
      setSelectedPieceId(null);
      return;
    }

    onSwapPieces({
      firstPieceId: selectedPieceId,
      secondPieceId: pieceId,
    });
    setSelectedPieceId(null);
  };

  const resolveBoardCellFromPointer = (clientX: number, clientY: number) => {
    const boardElement = boardRef.current;
    if (!boardElement) {
      return null;
    }

    const rect = boardElement.getBoundingClientRect();
    if (
      clientX < rect.left ||
      clientX > rect.right ||
      clientY < rect.top ||
      clientY > rect.bottom
    ) {
      return null;
    }

    const relativeX = clientX - rect.left;
    const relativeY = clientY - rect.top;
    const col = Math.min(
      board.cols - 1,
      Math.max(0, Math.floor((relativeX / rect.width) * board.cols)),
    );
    const row = Math.min(
      board.rows - 1,
      Math.max(0, Math.floor((relativeY / rect.height) * board.rows)),
    );

    return { row, col };
  };

  const handlePiecePointerUp = (
    pieceId: string,
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    const currentDragSession = dragSessionRef.current;
    if (!currentDragSession || currentDragSession.pieceId !== pieceId) {
      return;
    }

    event.currentTarget.releasePointerCapture?.(event.pointerId);

    if (currentDragSession.dragging) {
      const targetCell = resolveBoardCellFromPointer(
        event.clientX,
        event.clientY,
      );
      resetDragInteraction();
      if (targetCell) {
        onDragPiece({
          pieceId,
          targetRow: targetCell.row,
          targetCol: targetCell.col,
        });
      }
      setSelectedPieceId(null);
      return;
    }

    resetDragInteraction();
    handlePieceClick(pieceId);
  };

  const handlePiecePointerDown = (
    pieceId: string,
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    if (isInteractionLocked) {
      return;
    }
    event.preventDefault();
    resetDragInteraction();
    event.currentTarget.setPointerCapture?.(event.pointerId);
    // 按下可交互拼图片时立即给移动端短震反馈，点击选择与拖起都会有同一套手感。
    triggerPuzzlePiecePressHapticFeedback();
    dragSessionRef.current = {
      pieceId,
      pointerId: event.pointerId,
      dragging: false,
      startX: event.clientX,
      startY: event.clientY,
      currentX: event.clientX,
      currentY: event.clientY,
    };
  };

  const handlePiecePointerMove = (
    pieceId: string,
    event: React.PointerEvent<HTMLDivElement>,
  ) => {
    const dragSession = dragSessionRef.current;
    if (
      !dragSession ||
      dragSession.pieceId !== pieceId ||
      dragSession.pointerId !== event.pointerId
    ) {
      return;
    }

    event.preventDefault();
    const deltaX = event.clientX - dragSession.startX;
    const deltaY = event.clientY - dragSession.startY;
    const dragging = dragSession.dragging || Math.hypot(deltaX, deltaY) >= 8;
    dragSession.dragging = dragging;
    dragSession.currentX = event.clientX;
    dragSession.currentY = event.clientY;
    if (!dragging) {
      return;
    }

    // 首帧拖拽反馈立即落到 DOM，确保层级提升不会滞后一帧；后续仍保留 raf 兜底连续刷新。
    flushDragVisual();
    scheduleDragVisual();
  };

  const draggingPieceId = dragRenderTarget?.pieceId ?? null;
  const draggingGroupId = dragRenderTarget?.groupId ?? null;
  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 authorAvatarLabel = resolveAuthorAvatarLabel(
    currentLevel.authorDisplayName,
  );
  const leaderboardEntries =
    (currentLevel.leaderboardEntries ?? []).length > 0
      ? currentLevel.leaderboardEntries
      : (run.leaderboardEntries ?? []);
  const isClearResultOpen =
    currentLevel.status === 'cleared' &&
    dismissedClearKey !== clearResultKey &&
    isClearResultReady;
  const isInteractionLocked =
    isBusy || runtimeStatus !== 'playing' || Boolean(propDialog);

  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);
    try {
      await pauseChangePromiseRef.current;
      const 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') {
      setIsOriginalOverlayVisible(true);
    }
    if (propKind === 'freezeTime') {
      setIsFreezeEffectVisible(true);
      window.setTimeout(() => {
        setIsFreezeEffectVisible(false);
      }, 900);
    }
    if (propKind === 'extendTime') {
      setTimerNowMs(Date.now());
    }
  };

  return (
    <div className="fixed inset-0 z-[100] flex justify-center bg-slate-950 text-white">
      <div className="relative h-full w-full overflow-hidden bg-[radial-gradient(circle_at_50%_20%,rgba(251,191,36,0.18),transparent_28%),radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.16),transparent_26%),linear-gradient(180deg,#2d160e,#020617)]">
        {currentLevel.coverImageSrc ? (
          <ResolvedAssetImage
            src={currentLevel.coverImageSrc}
            alt=""
            aria-hidden="true"
            className="absolute inset-0 h-full w-full object-cover opacity-[0.16] blur-2xl"
          />
        ) : null}
        <div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:34px_34px] opacity-20" />

        <div className="absolute left-0 top-0 z-20 w-full px-4 py-4">
          <div className="grid grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] items-start gap-2 sm:gap-3">
            <button
              type="button"
              onClick={onBack}
              aria-label="返回上一页"
              className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur"
            >
              <ArrowLeft className="h-4 w-4" />
            </button>

            <div className="flex min-w-0 flex-col items-center gap-2 rounded-[1.35rem] bg-black/30 px-3 py-3 text-center backdrop-blur sm:px-5">
              <div className="line-clamp-1 max-w-full text-sm font-black text-white sm:text-base">
                {currentLevel.levelName}
              </div>
              <div
                className={`inline-flex items-center gap-2 rounded-full px-4 py-2 font-mono text-2xl font-black leading-none shadow-[0_10px_28px_rgba(0,0,0,0.24)] sm:text-3xl ${
                  displayRemainingMs <= 20_000 && runtimeStatus === 'playing'
                    ? 'bg-red-500/24 text-red-100'
                    : 'bg-white/12 text-white'
                }`}
              >
                <Clock className="h-5 w-5 sm:h-6 sm:w-6" />
                {formatTimerMs(displayRemainingMs)}
              </div>
              <div className="flex min-w-0 max-w-full items-center justify-center gap-2 text-white/82">
                <span
                  className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/16 bg-amber-200 text-xs font-black text-slate-950 shadow-[0_8px_20px_rgba(0,0,0,0.2)]"
                  aria-hidden="true"
                >
                  {authorAvatarLabel}
                </span>
                <span className="min-w-0 truncate text-xs font-semibold sm:text-sm">
                  {currentLevel.authorDisplayName}
                </span>
                <span className="shrink-0 rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-bold tracking-[0.12em] text-amber-100/90 sm:text-[11px]">
                  {levelLabel}
                </span>
              </div>
            </div>

            <button
              type="button"
              onClick={() => setIsSettingsPanelOpen(true)}
              aria-label="打开拼图设置"
              title="打开拼图设置"
              className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-black/30 backdrop-blur transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-200/60"
            >
              <PixelIcon
                src={CHROME_ICONS.settings}
                className="h-[1.4rem] w-[1.4rem] drop-shadow-[0_4px_10px_rgba(0,0,0,0.45)]"
              />
            </button>
          </div>
        </div>

        <div className="absolute inset-0 flex items-center justify-center px-1 py-3 pt-28 pb-32 sm:p-4">
          <div
            ref={boardRef}
            data-testid="puzzle-board"
            className="relative grid aspect-square w-full max-w-[min(99vw,calc(100vh_-_16.5rem))] touch-none select-none overflow-hidden rounded-[1.2rem] border border-white/16 bg-white/8 shadow-[0_30px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm sm:max-w-[min(92vw,calc(100vh_-_17rem))] sm:rounded-[1.45rem]"
            style={{
              gridTemplateColumns: `repeat(${board.cols}, minmax(0, 1fr))`,
              gridTemplateRows: `repeat(${board.rows}, minmax(0, 1fr))`,
            }}
          >
            {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 = 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={`relative flex h-full items-center justify-center overflow-hidden rounded-[0.85rem] border-2 border-white/22 text-sm font-black transition ${
                      occupied
                        ? isSelected
                          ? 'border-amber-200 bg-amber-400/84 text-slate-950 shadow-[0_12px_30px_rgba(251,191,36,0.22)]'
                          : isMerged
                            ? 'border-transparent bg-transparent text-white'
                            : 'bg-white/12 text-white'
                        : 'border-white/8 bg-black/18 text-white/20'
                    } ${
                      isMerged
                        ? 'transition-colors'
                        : 'transition-[background-color,border-color,box-shadow,opacity]'
                    }`}
                    style={{
                      zIndex: resolveDraggedPieceLayer(
                        piece?.pieceId,
                        draggingPieceId,
                        isMerged,
                      ),
                    }}
                    onPointerDown={(event) => {
                      if (!piece || isMerged) {
                        return;
                      }
                      handlePiecePointerDown(piece.pieceId, event);
                    }}
                    onPointerMove={(event) => {
                      if (!piece || isMerged) {
                        return;
                      }
                      handlePiecePointerMove(piece.pieceId, event);
                    }}
                    onPointerUp={(event) => {
                      if (piece && !isMerged) {
                        handlePiecePointerUp(piece.pieceId, 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 className="absolute inset-0 bg-black/10" />
                      </div>
                    ) : (
                      ''
                    )}
                  </div>
                </div>
              );
            })}
            {mergedGroups.map((group) => (
              <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}%`,
                }}
              >
                <div
                  className="pointer-events-none relative grid h-full w-full touch-none overflow-visible 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 overflow-hidden border-white/22 bg-emerald-300/10 shadow-[0_12px_30px_rgba(6,78,59,0.16)] ${resolveMergedPieceOutlineClass(
                        group,
                        piece,
                      )}`}
                      data-merged-piece-outline="true"
                      style={{
                        gridColumn: piece.localCol + 1,
                        gridRow: piece.localRow + 1,
                      }}
                      onPointerDown={(event) => {
                        handlePiecePointerDown(piece.pieceId, event);
                      }}
                      onPointerMove={(event) => {
                        handlePiecePointerMove(piece.pieceId, event);
                      }}
                      onPointerUp={(event) => {
                        handlePiecePointerUp(piece.pieceId, event);
                      }}
                      onPointerCancel={() => {
                        resetDragInteraction();
                      }}
                      onLostPointerCapture={() => {
                        resetDragInteraction();
                      }}
                    >
                      {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(52,211,153,0.38),rgba(6,78,59,0.68))]" />
                      )}
                      <div className="absolute inset-0 bg-black/8" />
                    </div>
                  ))}
                </div>
              </div>
            ))}
            {isOriginalOverlayVisible && resolvedCoverImage ? (
              <div
                data-testid="puzzle-original-overlay"
                className="pointer-events-none absolute inset-0 z-40 bg-black/10"
              >
                <div
                  className="absolute inset-0 opacity-70"
                  style={{
                    backgroundImage: `url("${resolvedCoverImage}")`,
                    backgroundSize: '100% 100%',
                    backgroundPosition: 'center',
                  }}
                />
              </div>
            ) : null}
            {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 ? (
            <div className="rounded-full bg-red-500/20 px-3 py-1 text-xs text-red-100">
              {error}
            </div>
          ) : null}
          {selectedPieceId && runtimeStatus === 'playing' ? (
            <div className="rounded-full bg-black/28 px-3 py-1 text-xs text-white/72 backdrop-blur">
              已选择
            </div>
          ) : null}
          {canShowNextAction ? (
            <button
              type="button"
              disabled={isBusy}
              onClick={() => {
                if (hasSimilarWorkChoices) {
                  setDismissedClearKey(null);
                  setIsClearResultReady(true);
                  return;
                }
                onAdvanceNextLevel({
                  profileId: run.nextLevelProfileId ?? undefined,
                  levelId: run.nextLevelId ?? null,
                });
              }}
              className="inline-flex min-h-11 items-center gap-2 rounded-full bg-amber-200 px-5 py-2.5 text-sm font-bold text-slate-950 shadow-[0_14px_36px_rgba(251,191,36,0.26)] transition hover:bg-amber-100 disabled:opacity-45"
            >
              {hasSimilarWorkChoices ? '换个作品' : '下一关'}
              <ArrowRight className="h-4 w-4" />
            </button>
          ) : null}

          <div className="flex items-center justify-center gap-2 rounded-full bg-black/36 p-2 backdrop-blur sm:gap-3">
            <button
              type="button"
              disabled={isInteractionLocked}
              onClick={() => openPropDialog('hint', '使用提示')}
              className="inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black text-white/88 transition hover:bg-white/10 disabled:opacity-45"
            >
              <Lightbulb className="h-6 w-6 text-amber-100" />
              提示
            </button>
            <button
              type="button"
              disabled={runtimeStatus !== 'playing'}
              aria-pressed={isOriginalOverlayVisible}
              onClick={() => {
                if (isOriginalOverlayVisible) {
                  setIsOriginalOverlayVisible(false);
                  return;
                }
                openPropDialog('reference', '查看原图');
              }}
              className={`inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black transition hover:bg-white/10 disabled:opacity-45 ${
                isOriginalOverlayVisible
                  ? 'bg-sky-200 text-slate-950'
                  : 'text-white/86'
              }`}
            >
              <Eye className="h-6 w-6" />
              原图
            </button>
            <button
              type="button"
              disabled={isInteractionLocked}
              onClick={() => openPropDialog('freezeTime', '冻结时间')}
              className="inline-flex h-16 min-w-[5.75rem] flex-col items-center justify-center gap-1 rounded-full px-4 text-sm font-black text-white/88 transition hover:bg-white/10 disabled:opacity-45"
            >
              <Snowflake className="h-6 w-6 text-cyan-100" />
              冻结
            </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}

        {propDialog ? (
          <div
            className="absolute inset-0 z-50 flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
            onClick={() => {
              if (!isPropConfirming) {
                setPropDialog(null);
              }
            }}
          >
            <section
              role="dialog"
              aria-modal="true"
              aria-labelledby="puzzle-prop-confirm-title"
              className="pixel-nine-slice pixel-modal-shell w-full max-w-[22rem] overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
              style={getNineSliceStyle(UI_CHROME.modalPanel)}
              onClick={(event) => event.stopPropagation()}
            >
              <header className="flex items-center gap-3 border-b border-white/10 px-5 py-4">
                <span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-200 text-slate-950">
                  <Sparkles className="h-4 w-4" />
                </span>
                <h2
                  id="puzzle-prop-confirm-title"
                  className="text-sm font-black text-white"
                >
                  {propDialog.title}
                </h2>
              </header>
              <div className="px-5 py-4 text-sm text-white/72">
                消耗 1 陶泥币
                {propConfirmError ? (
                  <div className="mt-3 rounded-[0.9rem] border border-red-300/20 bg-red-500/12 px-3 py-2 text-xs leading-5 text-red-100">
                    {propConfirmError}
                  </div>
                ) : null}
              </div>
              <footer className="flex items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
                <button
                  type="button"
                  onClick={() => setPropDialog(null)}
                  disabled={isPropConfirming}
                  className="rounded-full border border-white/12 bg-black/20 px-4 py-2 text-xs font-bold text-zinc-200 transition hover:text-white"
                >
                  取消
                </button>
                <button
                  type="button"
                  disabled={isPropConfirming}
                  onClick={() => {
                    void handleConfirmProp();
                  }}
                  className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-5 py-2 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:opacity-60"
                >
                  {isPropConfirming ? (
                    <Loader2 className="h-3.5 w-3.5 animate-spin" />
                  ) : null}
                  确定
                </button>
              </footer>
            </section>
          </div>
        ) : null}

        {isSettingsPanelOpen ? (
          <div
            className="absolute inset-0 z-50 flex items-center justify-center bg-black/72 p-3 backdrop-blur-sm sm:p-4"
            onClick={() => setIsSettingsPanelOpen(false)}
          >
            <section
              role="dialog"
              aria-modal="true"
              aria-labelledby="puzzle-settings-title"
              className="pixel-nine-slice pixel-modal-shell flex max-h-[min(88vh,36rem)] w-full max-w-md flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
              style={getNineSliceStyle(UI_CHROME.modalPanel)}
              onClick={(event) => event.stopPropagation()}
            >
              <header className="relative border-b border-white/10 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 text-white"
                  >
                    拼图设置
                  </h2>
                  <div className="mt-1 text-[11px] text-zinc-500">
                    调整音乐音量，查看本局进度，或返回上一页。
                  </div>
                </div>
                <button
                  type="button"
                  aria-label="关闭拼图设置"
                  onClick={() => setIsSettingsPanelOpen(false)}
                  className="absolute right-4 top-3 p-1 text-zinc-400 transition-colors hover:text-white sm:right-5 sm:top-4"
                >
                  <PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
                </button>
              </header>

              <div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-4">
                <div className="rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.14),transparent_65%),rgba(0,0,0,0.24)] p-4">
                  <div className="flex items-start justify-between gap-3">
                    <div>
                      <div className="text-[10px] tracking-[0.24em] text-sky-200/80">
                        音频
                      </div>
                      <div className="mt-2 text-sm font-semibold text-white">
                        音乐音量
                      </div>
                    </div>
                    <div className="rounded-full border border-white/10 bg-black/28 px-2 py-1 text-xs text-white/80">
                      {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-sky-400"
                    />
                  </div>
                </div>

                <div className="rounded-2xl border border-white/10 bg-black/25 px-4 py-3">
                  <div className="text-[10px] uppercase tracking-[0.18em] text-zinc-500">
                    本局进度
                  </div>
                  <div className="mt-3 space-y-2 text-sm text-white/82">
                    <div className="flex items-center justify-between gap-3">
                      <span className="text-white/56">关卡</span>
                      <span className="font-semibold text-white">
                        {levelLabel}
                      </span>
                    </div>
                    <div className="flex items-center justify-between gap-3">
                      <span className="text-white/56">已完成关卡</span>
                      <span className="font-semibold text-white">
                        {run.clearedLevelCount}
                      </span>
                    </div>
                    <div className="flex items-center justify-between gap-3">
                      <span className="text-white/56">当前状态</span>
                      <span className="font-semibold text-white">
                        {statusLabel}
                      </span>
                    </div>
                    <div className="flex items-center justify-between gap-3">
                      <span className="text-white/56">当前用时</span>
                      <span className="font-mono font-semibold text-white">
                        {formatElapsedMs(currentLevel.elapsedMs)}
                      </span>
                    </div>
                  </div>
                </div>
              </div>

              <footer className="flex items-center justify-end gap-3 border-t border-white/10 px-4 py-3 sm:px-5">
                <button
                  type="button"
                  onClick={() => setIsSettingsPanelOpen(false)}
                  className="rounded-full border border-white/12 bg-black/20 px-3 py-1.5 text-[11px] text-zinc-200 transition hover:text-white"
                >
                  继续拼图
                </button>
                <button
                  type="button"
                  onClick={() => {
                    setIsSettingsPanelOpen(false);
                    onBack();
                  }}
                  className="rounded-full bg-amber-200 px-4 py-2 text-sm font-bold text-slate-950 transition hover:bg-amber-100"
                >
                  返回上一页
                </button>
              </footer>
            </section>
          </div>
        ) : null}

        {runtimeStatus === 'failed' ? (
          <div className="absolute inset-0 z-40 flex items-center justify-center bg-slate-950/68 px-4 py-6 backdrop-blur-sm">
            <section
              role="dialog"
              aria-modal="true"
              aria-labelledby="puzzle-failed-title"
              className="flex w-full max-w-[24rem] flex-col overflow-hidden rounded-[1.5rem] border border-white/14 bg-slate-950/94 shadow-[0_28px_90px_rgba(0,0,0,0.5)]"
            >
              <header className="border-b border-white/10 px-5 py-4">
                <h2
                  id="puzzle-failed-title"
                  className="text-lg font-black text-white"
                >
                  关卡失败
                </h2>
                <div className="mt-1 text-xs text-white/62">
                  {currentLevel.levelName}
                </div>
              </header>
              <footer className="grid grid-cols-2 gap-3 border-t border-white/10 px-5 py-4">
                <button
                  type="button"
                  disabled={isBusy}
                  onClick={() => {
                    void onRestartLevel?.();
                  }}
                  className="rounded-full border border-white/14 bg-black/24 px-4 py-2.5 text-sm font-black text-white transition hover:bg-white/10 disabled:opacity-50"
                >
                  重新开始
                </button>
                <button
                  type="button"
                  disabled={isBusy}
                  onClick={() => openPropDialog('extendTime', '继续1分钟')}
                  className="rounded-full bg-amber-200 px-4 py-2.5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:opacity-50"
                >
                  继续1分钟
                </button>
              </footer>
            </section>
          </div>
        ) : null}

        {isClearResultOpen ? (
          <div className="absolute inset-0 z-40 flex items-center justify-center bg-slate-950/68 px-4 py-6 backdrop-blur-sm">
            <section
              role="dialog"
              aria-modal="true"
              aria-labelledby="puzzle-clear-result-title"
              className="flex max-h-[min(92vh,42rem)] w-full max-w-[28rem] flex-col overflow-hidden rounded-[1.5rem] border border-white/14 bg-slate-950/94 shadow-[0_28px_90px_rgba(0,0,0,0.5)]"
            >
              <header className="flex items-start justify-between gap-3 border-b border-white/10 px-5 py-4">
                <div className="min-w-0">
                  <div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-200 text-slate-950">
                    <Trophy className="h-4 w-4" />
                  </div>
                  <h2
                    id="puzzle-clear-result-title"
                    className="truncate text-lg font-black text-white"
                  >
                    通关完成
                  </h2>
                  <div className="mt-1 line-clamp-1 text-xs text-white/62">
                    {currentLevel.levelName}
                  </div>
                </div>
                <button
                  type="button"
                  aria-label="关闭通关弹窗"
                  className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/8 text-white/72 transition hover:bg-white/14 hover:text-white"
                  onClick={() => {
                    setDismissedClearKey(clearResultKey);
                  }}
                >
                  <PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
                </button>
              </header>

              <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
                <div className="flex items-center justify-between gap-4 rounded-[1rem] border border-amber-200/24 bg-amber-200/10 px-4 py-3">
                  <div className="flex items-center gap-3">
                    <span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/24 text-amber-100">
                      <Clock className="h-4 w-4" />
                    </span>
                    <span className="text-sm font-semibold text-white/72">
                      通关时间
                    </span>
                  </div>
                  <span className="font-mono text-xl font-black text-amber-100">
                    {formatElapsedMs(currentLevel.elapsedMs)}
                  </span>
                </div>

                <div className="mt-4">
                  <div className="mb-2 text-sm font-bold text-white">
                    排行榜
                  </div>
                  <div className="overflow-hidden rounded-[1rem] border border-white/10">
                    <div className="grid grid-cols-[3.5rem_minmax(0,1fr)_6rem] bg-white/6 px-3 py-2 text-[11px] font-bold text-white/48">
                      <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 grid-cols-[3.5rem_minmax(0,1fr)_6rem] items-center px-3 py-2.5 text-sm ${
                              entry.isCurrentPlayer
                                ? 'bg-amber-200/14 text-amber-50'
                                : 'border-t border-white/8 text-white/78'
                            }`}
                          >
                            <span className="font-mono font-black">
                              #{entry.rank}
                            </span>
                            <span className="truncate font-semibold">
                              {entry.nickname}
                            </span>
                            <span className="text-right font-mono text-xs font-bold">
                              {formatElapsedMs(entry.elapsedMs)}
                            </span>
                          </div>
                        ))
                      ) : (
                        <div className="flex min-h-24 items-center justify-center px-4 py-5 text-sm text-white/56">
                          {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 ? (
                <footer className="flex items-center justify-end border-t border-white/10 px-5 py-4">
                  <button
                    type="button"
                    disabled={isBusy}
                    onClick={() => {
                      onAdvanceNextLevel({
                        profileId: run.nextLevelProfileId ?? undefined,
                        levelId: run.nextLevelId ?? null,
                      });
                    }}
                    className="inline-flex items-center gap-2 rounded-full bg-amber-200 px-5 py-2.5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
                  >
                    {isBusy ? (
                      <Loader2 className="h-4 w-4 animate-spin" />
                    ) : (
                      <ArrowRight className="h-4 w-4" />
                    )}
                    下一关
                  </button>
                </footer>
              ) : null}
            </section>
          </div>
        ) : null}
      </div>
    </div>
  );
}

function PuzzleNextWorkCard({
  item,
  disabled,
  onClick,
}: {
  item: PuzzleRecommendedNextWork;
  disabled: boolean;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      disabled={disabled}
      onClick={onClick}
      className="group grid min-h-[5.75rem] grid-cols-[4.5rem_minmax(0,1fr)] overflow-hidden rounded-[1rem] border border-white/10 bg-white/6 text-left transition hover:border-amber-200/40 hover:bg-amber-200/10 disabled:cursor-not-allowed disabled:opacity-45 sm:grid-cols-1"
    >
      <div className="relative min-h-full bg-white/8 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="absolute inset-0 bg-black/10 transition group-hover:bg-black/0" />
      </div>
      <div className="min-w-0 px-3 py-2.5">
        <div className="truncate text-sm font-black text-white">
          {item.levelName}
        </div>
        <div className="mt-1 truncate text-xs font-semibold text-white/58">
          {item.authorDisplayName}
        </div>
        <div className="mt-2 flex flex-wrap gap-1">
          {item.themeTags.slice(0, 2).map((tag) => (
            <span
              key={tag}
              className="max-w-full truncate rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-bold text-white/64"
            >
              {tag}
            </span>
          ))}
        </div>
      </div>
    </button>
  );
}

export default PuzzleRuntimeShell;
