import {
  ArrowLeft,
  Gift,
  PartyPopper,
  RotateCcw,
  SkipForward,
} from 'lucide-react';
import {
  type CSSProperties,
  type PointerEvent as ReactPointerEvent,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';

import type {
  BabyObjectMatchDraft,
  BabyObjectMatchItemAsset,
  BabyObjectMatchVisualAsset,
  BabyObjectMatchVisualAssetKind,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
  MocapHandInput,
  MocapInputCommand,
  UseMocapInputResult,
} from '../../services/useMocapInput';
import { useMocapInput } from '../../services/useMocapInput';
import { ResolvedAssetImage } from '../ResolvedAssetImage';

const BABY_OBJECT_MATCH_SUCCESS_TARGET = 20;
const BABY_OBJECT_MATCH_GIFT_APPEAR_DURATION_MS = 620;
const BABY_OBJECT_MATCH_GIFT_OPEN_DURATION_MS = 640;
const BABY_OBJECT_MATCH_ITEM_APPEAR_DURATION_MS = 620;
const BABY_OBJECT_MATCH_FEEDBACK_DURATION_MS = 1180;
const BABY_OBJECT_MATCH_INTRO_SHOW_DURATION_MS = 2000;
const BABY_OBJECT_MATCH_INTRO_FLY_DURATION_MS = 720;
const BABY_OBJECT_MATCH_INTRO_READY_PAUSE_MS = 1000;
const BABY_OBJECT_MATCH_ITEM_CENTER: RuntimeHandPoint = { x: 0.5, y: 0.37 };
const BABY_OBJECT_MATCH_ITEM_GRAB_RADIUS = 0.14;
// 篮子仍只认主体附近，但在上一版核心区基础上扩大约 50%，避免贴近篮子后仍难以命中。
const BABY_OBJECT_MATCH_BASKET_DROP_Y = 0.62;
const BABY_OBJECT_MATCH_LEFT_BASKET_MAX_X = 0.36;
const BABY_OBJECT_MATCH_RIGHT_BASKET_MIN_X = 0.64;

type BabyObjectMatchRuntimeShellProps = {
  draft: BabyObjectMatchDraft;
  embedded?: boolean;
  enableMocapInput?: boolean;
  mocapInput?: UseMocapInputResult | null;
  random?: BabyObjectMatchRandom;
  onBack?: () => void;
  onNextLevel?: () => void;
};

type BasketSide = 'left' | 'right';
type RuntimePhase =
  | 'intro-left-showing'
  | 'intro-left-flying'
  | 'intro-left-ready'
  | 'intro-right-showing'
  | 'intro-right-flying'
  | 'intro-right-ready'
  | 'gift-entering'
  | 'gift-opening'
  | 'item-appearing'
  | 'active'
  | 'correct'
  | 'wrong'
  | 'complete';

type RuntimeRound = {
  item: BabyObjectMatchItemAsset;
  baskets: Record<BasketSide, BabyObjectMatchItemAsset>;
};

type RuntimeIntroShowcase = {
  side: BasketSide;
  item: BabyObjectMatchItemAsset;
  isFlying: boolean;
};

type RuntimeHandPoint = {
  x: number;
  y: number;
};

type RuntimeHandRole = 'left' | 'right';

type RuntimeHands = Record<RuntimeHandRole, RuntimeHandPoint | null>;

type HeldItemState = {
  hand: RuntimeHandRole;
};

type BabyObjectMatchRandom = () => number;
type BabyObjectMatchStageStyle = CSSProperties &
  Partial<
    Record<
      | '--baby-object-ui-frame-image'
      | '--baby-object-gift-box-image'
      | '--baby-object-basket-image'
      | '--baby-object-smoke-image',
      string
    >
  >;

function pickRandomIndex(length: number, random: BabyObjectMatchRandom) {
  if (length <= 1) {
    return 0;
  }

  return Math.min(length - 1, Math.floor(random() * length));
}

function buildRuntimeRound(
  draft: BabyObjectMatchDraft,
  random: BabyObjectMatchRandom,
): RuntimeRound {
  const items = draft.itemAssets;
  const item = items[pickRandomIndex(items.length, random)] ?? items[0]!;

  return {
    item,
    baskets: {
      left: items[0]!,
      right: items[1]!,
    },
  };
}

function mocapHandToRuntimePoint(
  hand: MocapHandInput | null | undefined,
  skeletonWrist: RuntimeHandPoint | null | undefined,
): RuntimeHandPoint | null {
  if (skeletonWrist) {
    return clampRuntimePoint(skeletonWrist);
  }

  if (!hand) {
    return null;
  }

  // 骨架 wrist 缺失时再回退到手部 landmarks 的 wrist，最后才使用手部派生点。
  const point = hand.wrist ?? hand;
  return clampRuntimePoint({ x: point.x, y: point.y });
}

function clampRuntimePoint(point: RuntimeHandPoint): RuntimeHandPoint {
  return {
    x: Math.max(0, Math.min(1, point.x)),
    y: Math.max(0, Math.min(1, point.y)),
  };
}

function isRuntimePointTouchingItem(point: RuntimeHandPoint) {
  const dx = point.x - BABY_OBJECT_MATCH_ITEM_CENTER.x;
  const dy = point.y - BABY_OBJECT_MATCH_ITEM_CENTER.y;
  return Math.sqrt(dx * dx + dy * dy) <= BABY_OBJECT_MATCH_ITEM_GRAB_RADIUS;
}

function isRuntimeControlPointerTarget(target: EventTarget | null) {
  return (
    target instanceof Element &&
    target.closest(
      'button, a, input, select, textarea, [role="button"], [data-baby-object-runtime-control="true"]',
    ) !== null
  );
}

function resolveBasketSideForPoint(point: RuntimeHandPoint): BasketSide | null {
  if (point.y < BABY_OBJECT_MATCH_BASKET_DROP_Y) {
    return null;
  }
  if (point.x <= BABY_OBJECT_MATCH_LEFT_BASKET_MAX_X) {
    return 'left';
  }
  if (point.x >= BABY_OBJECT_MATCH_RIGHT_BASKET_MIN_X) {
    return 'right';
  }
  return null;
}

function resolveMocapRuntimeHands(command: MocapInputCommand): RuntimeHands {
  // 本地 mocap 当前按摄像头视角输出 handedness，这里换回用户身体视角用于显示双手。
  return {
    left: mocapHandToRuntimePoint(
      command.rightHand,
      command.bodyJoints?.rightWrist,
    ),
    right: mocapHandToRuntimePoint(
      command.leftHand,
      command.bodyJoints?.leftWrist,
    ),
  };
}

function buildMocapPacketKey(
  command: MocapInputCommand,
  rawPacketPreview: UseMocapInputResult['rawPacketPreview'],
) {
  return rawPacketPreview?.receivedAtMs !== undefined
    ? `${rawPacketPreview.receivedAtMs}:${rawPacketPreview.text}`
    : JSON.stringify(command);
}

function findVisualAsset(
  draft: BabyObjectMatchDraft,
  kind: BabyObjectMatchVisualAssetKind,
): BabyObjectMatchVisualAsset | null {
  return (
    draft.visualPackage?.assets.find((asset) => asset.assetKind === kind) ??
    null
  );
}

function buildCssImageValue(src: string) {
  return `url("${src.replace(/"/gu, '\\"')}")`;
}

function resolveIntroShowcase(
  phase: RuntimePhase,
  draft: BabyObjectMatchDraft,
): RuntimeIntroShowcase | null {
  if (phase === 'intro-left-showing' || phase === 'intro-left-flying') {
    const item = draft.itemAssets[0];
    return item
      ? { side: 'left', item, isFlying: phase === 'intro-left-flying' }
      : null;
  }

  if (phase === 'intro-right-showing' || phase === 'intro-right-flying') {
    const item = draft.itemAssets[1];
    return item
      ? { side: 'right', item, isFlying: phase === 'intro-right-flying' }
      : null;
  }

  return null;
}

function isBasketOptionReadyInIntro(side: BasketSide, phase: RuntimePhase) {
  if (!phase.startsWith('intro-')) {
    return true;
  }

  if (side === 'left') {
    return (
      phase === 'intro-left-ready' ||
      phase === 'intro-right-showing' ||
      phase === 'intro-right-flying' ||
      phase === 'intro-right-ready'
    );
  }

  return phase === 'intro-right-ready';
}

export function BabyObjectMatchRuntimeShell({
  draft,
  embedded = false,
  enableMocapInput = true,
  mocapInput = null,
  random,
  onBack,
  onNextLevel,
}: BabyObjectMatchRuntimeShellProps) {
  const randomRef = useRef<BabyObjectMatchRandom>(
    random ?? (() => Math.random()),
  );
  const introTimerRef = useRef<number | null>(null);
  const feedbackTimerRef = useRef<number | null>(null);
  const handledMocapPacketKeyRef = useRef<string | null>(null);
  const latestMocapPacketKeyRef = useRef<string | null>(null);
  const [phase, setPhase] = useState<RuntimePhase>('intro-left-showing');
  const [successCount, setSuccessCount] = useState(0);
  const [round, setRound] = useState<RuntimeRound | null>(() =>
    buildRuntimeRound(draft, randomRef.current),
  );
  const [feedbackText, setFeedbackText] = useState<string | null>(null);
  const [lastTargetSide, setLastTargetSide] = useState<BasketSide | null>(null);
  const [runtimeHands, setRuntimeHands] = useState<RuntimeHands>({
    left: null,
    right: null,
  });
  const [heldItem, setHeldItem] = useState<HeldItemState | null>(null);
  const liveMocapInput = useMocapInput({
    enabled: enableMocapInput && !mocapInput,
  });
  const resolvedMocapInput = mocapInput ?? liveMocapInput;
  const backgroundAsset = findVisualAsset(draft, 'background');
  const uiFrameAsset = findVisualAsset(draft, 'ui-frame');
  const giftBoxAsset = findVisualAsset(draft, 'gift-box');
  const basketAsset = findVisualAsset(draft, 'basket');
  const smokeAsset = findVisualAsset(draft, 'smoke-puff');
  const stageStyle: BabyObjectMatchStageStyle = {
    ...(uiFrameAsset
      ? {
          '--baby-object-ui-frame-image': buildCssImageValue(
            uiFrameAsset.imageSrc,
          ),
        }
      : {}),
    ...(giftBoxAsset
      ? {
          '--baby-object-gift-box-image': buildCssImageValue(
            giftBoxAsset.imageSrc,
          ),
        }
      : {}),
    ...(basketAsset
      ? {
          '--baby-object-basket-image': buildCssImageValue(
            basketAsset.imageSrc,
          ),
        }
      : {}),
    ...(smokeAsset
      ? {
          '--baby-object-smoke-image': buildCssImageValue(
            smokeAsset.imageSrc,
          ),
        }
      : {}),
  };

  const progressText = `${successCount}/${BABY_OBJECT_MATCH_SUCCESS_TARGET}`;
  const isComplete = phase === 'complete';
  const currentItem = round?.item ?? null;
  const isJudgementOpen = phase === 'active';
  const introShowcase = resolveIntroShowcase(phase, draft);
  const heldPoint = heldItem ? runtimeHands[heldItem.hand] : null;
  const shouldShowCurrentItem =
    currentItem &&
    (phase === 'item-appearing' ||
      phase === 'active' ||
      phase === 'correct' ||
      phase === 'wrong');
  const shouldShowGift = phase === 'gift-entering' || phase === 'gift-opening';
  const shouldShowSmoke =
    phase === 'gift-opening' || phase === 'item-appearing';

  useEffect(() => {
    randomRef.current = random ?? (() => Math.random());
  }, [random]);

  useEffect(() => {
    latestMocapPacketKeyRef.current = resolvedMocapInput.latestCommand
      ? buildMocapPacketKey(
          resolvedMocapInput.latestCommand,
          resolvedMocapInput.rawPacketPreview,
        )
      : null;
  }, [resolvedMocapInput.latestCommand, resolvedMocapInput.rawPacketPreview]);

  const clearFeedbackTimer = useCallback(() => {
    if (feedbackTimerRef.current !== null) {
      window.clearTimeout(feedbackTimerRef.current);
      feedbackTimerRef.current = null;
    }
  }, []);

  const clearIntroTimer = useCallback(() => {
    if (introTimerRef.current !== null) {
      window.clearTimeout(introTimerRef.current);
      introTimerRef.current = null;
    }
  }, []);

  const resetInputPaths = useCallback(() => {
    handledMocapPacketKeyRef.current = null;
    setHeldItem(null);
  }, []);

  useEffect(() => {
    clearIntroTimer();

    if (phase === 'intro-left-showing') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('intro-left-flying');
      }, BABY_OBJECT_MATCH_INTRO_SHOW_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'intro-left-flying') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('intro-left-ready');
      }, BABY_OBJECT_MATCH_INTRO_FLY_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'intro-left-ready') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('intro-right-showing');
      }, BABY_OBJECT_MATCH_INTRO_READY_PAUSE_MS);
      return clearIntroTimer;
    }

    if (phase === 'intro-right-showing') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('intro-right-flying');
      }, BABY_OBJECT_MATCH_INTRO_SHOW_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'intro-right-flying') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('intro-right-ready');
      }, BABY_OBJECT_MATCH_INTRO_FLY_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'intro-right-ready') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('gift-entering');
      }, BABY_OBJECT_MATCH_INTRO_READY_PAUSE_MS);
      return clearIntroTimer;
    }

    if (phase === 'gift-entering') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('gift-opening');
      }, BABY_OBJECT_MATCH_GIFT_APPEAR_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'gift-opening') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        setPhase('item-appearing');
      }, BABY_OBJECT_MATCH_GIFT_OPEN_DURATION_MS);
      return clearIntroTimer;
    }

    if (phase === 'item-appearing') {
      introTimerRef.current = window.setTimeout(() => {
        introTimerRef.current = null;
        resetInputPaths();
        handledMocapPacketKeyRef.current = latestMocapPacketKeyRef.current;
        setPhase('active');
      }, BABY_OBJECT_MATCH_ITEM_APPEAR_DURATION_MS);
      return clearIntroTimer;
    }

    return clearIntroTimer;
  }, [clearIntroTimer, phase, resetInputPaths]);

  const resetRuntime = useCallback(() => {
    clearIntroTimer();
    clearFeedbackTimer();
    resetInputPaths();
    setSuccessCount(0);
    setRound(buildRuntimeRound(draft, randomRef.current));
    setFeedbackText(null);
    setLastTargetSide(null);
    setPhase('intro-left-showing');
  }, [clearFeedbackTimer, clearIntroTimer, draft, resetInputPaths]);

  const finishFeedback = useCallback(
    (nextSuccessCount: number, wasCorrect: boolean) => {
      clearIntroTimer();
      clearFeedbackTimer();
      feedbackTimerRef.current = window.setTimeout(() => {
        feedbackTimerRef.current = null;
        if (wasCorrect) {
          if (nextSuccessCount >= BABY_OBJECT_MATCH_SUCCESS_TARGET) {
            setFeedbackText('恭喜你！小朋友！');
            setRound(null);
            setPhase('complete');
            return;
          }

          setRound(buildRuntimeRound(draft, randomRef.current));
          setFeedbackText(null);
          setLastTargetSide(null);
          resetInputPaths();
          setPhase('gift-entering');
          return;
        }

        setFeedbackText(null);
        setLastTargetSide(null);
        resetInputPaths();
        setPhase('active');
      }, BABY_OBJECT_MATCH_FEEDBACK_DURATION_MS);
    },
    [clearFeedbackTimer, clearIntroTimer, draft, resetInputPaths],
  );

  const sendItemToBasket = useCallback(
    (side: BasketSide) => {
      if (!isJudgementOpen || !round) {
        return;
      }

      const isCorrect = round.baskets[side].itemId === round.item.itemId;
      setLastTargetSide(side);
      if (isCorrect) {
        const nextSuccessCount = successCount + 1;
        setSuccessCount(nextSuccessCount);
        setFeedbackText('真棒');
        setPhase('correct');
        finishFeedback(nextSuccessCount, true);
        return;
      }

      setFeedbackText('再想一想吧');
      setPhase('wrong');
      finishFeedback(successCount, false);
    },
    [finishFeedback, isJudgementOpen, round, successCount],
  );

  useEffect(
    () => () => {
      clearIntroTimer();
      clearFeedbackTimer();
    },
    [clearFeedbackTimer, clearIntroTimer],
  );

  useEffect(() => {
    const command = resolvedMocapInput.latestCommand;
    if (!command || isComplete) {
      return;
    }

    const packetKey = buildMocapPacketKey(
      command,
      resolvedMocapInput.rawPacketPreview,
    );
    if (handledMocapPacketKeyRef.current === packetKey) {
      return;
    }
    handledMocapPacketKeyRef.current = packetKey;

    const nextHands = resolveMocapRuntimeHands(command);
    setRuntimeHands(nextHands);

    if (!isJudgementOpen) {
      resetInputPaths();
      return;
    }

    const currentHeldItem = heldItem;
    if (currentHeldItem) {
      const heldHandPoint = nextHands[currentHeldItem.hand];
      const targetSide = heldHandPoint
        ? resolveBasketSideForPoint(heldHandPoint)
        : null;
      if (!targetSide) {
        return;
      }
      sendItemToBasket(targetSide);
      resetInputPaths();
      return;
    }

    for (const hand of ['left', 'right'] as const) {
      const point = nextHands[hand];
      if (!point || !isRuntimePointTouchingItem(point)) {
        continue;
      }
      setHeldItem({ hand });
      return;
    }
  }, [
    heldItem,
    isComplete,
    isJudgementOpen,
    resetInputPaths,
    resolvedMocapInput.latestCommand,
    resolvedMocapInput.rawPacketPreview,
    sendItemToBasket,
  ]);

  const getPointerUnitPoint = (
    event: ReactPointerEvent<HTMLElement>,
    element: HTMLElement,
  ): RuntimeHandPoint => {
    const rect = element.getBoundingClientRect();
    const width = rect.width || 1;
    const height = rect.height || 1;
    return clampRuntimePoint({
      x: (event.clientX - rect.left) / width,
      y: (event.clientY - rect.top) / height,
    });
  };

  const handlePointerDown = (event: ReactPointerEvent<HTMLElement>) => {
    if (isRuntimeControlPointerTarget(event.target)) {
      return;
    }

    if (!isJudgementOpen) {
      return;
    }

    if (event.button !== 0 && event.button !== 2) {
      return;
    }

    const hand: RuntimeHandRole = event.button === 2 ? 'right' : 'left';
    const point = getPointerUnitPoint(event, event.currentTarget);
    setRuntimeHands((current) => ({ ...current, [hand]: point }));
    if (isRuntimePointTouchingItem(point)) {
      setHeldItem({ hand });
    }
    event.preventDefault();
    if (typeof event.currentTarget.setPointerCapture === 'function') {
      event.currentTarget.setPointerCapture(event.pointerId);
    }
  };

  const handlePointerMove = (event: ReactPointerEvent<HTMLElement>) => {
    if (isRuntimeControlPointerTarget(event.target)) {
      return;
    }

    if (!isJudgementOpen) {
      return;
    }

    if (event.buttons !== 1 && event.buttons !== 2) {
      return;
    }

    const hand: RuntimeHandRole = event.buttons === 2 ? 'right' : 'left';
    const point = getPointerUnitPoint(event, event.currentTarget);
    setRuntimeHands((current) => ({ ...current, [hand]: point }));
    if (!heldItem && isRuntimePointTouchingItem(point)) {
      setHeldItem({ hand });
      return;
    }

    if (!heldItem || heldItem.hand !== hand) {
      return;
    }

    const targetSide = resolveBasketSideForPoint(point);
    if (targetSide) {
      sendItemToBasket(targetSide);
      resetInputPaths();
    }
  };

  const handlePointerUp = (event: ReactPointerEvent<HTMLElement>) => {
    if (
      typeof event.currentTarget.hasPointerCapture === 'function' &&
      event.currentTarget.hasPointerCapture(event.pointerId)
    ) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
  };

  return (
    <main
      className={`baby-object-runtime${embedded ? ' baby-object-runtime--embedded' : ''}`}
      data-testid="baby-object-match-runtime"
    >
      <section
        className={`baby-object-runtime__stage${
          backgroundAsset ? ' baby-object-runtime__stage--skinned' : ''
        }`}
        style={stageStyle}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
        onContextMenu={(event) => event.preventDefault()}
      >
        {backgroundAsset ? (
          <ResolvedAssetImage
            src={backgroundAsset.imageSrc}
            alt=""
            className="baby-object-runtime__background-image"
            data-testid="baby-object-background-image"
            aria-hidden="true"
          />
        ) : null}

        {onBack ? (
          <button
            type="button"
            className="baby-object-runtime__back"
            data-baby-object-runtime-control="true"
            onClick={onBack}
            aria-label="返回"
            title="返回"
          >
            <ArrowLeft className="h-5 w-5" />
          </button>
        ) : null}

        <div className="baby-object-runtime__subtitle" role="status">
          将物品放入对应的篮子里
        </div>

        <div className="baby-object-runtime__counter" aria-label="成功次数">
          {progressText}
        </div>

        {shouldShowGift ? (
          <div
            className={`baby-object-runtime__gift${
              giftBoxAsset ? ' baby-object-runtime__gift--skinned' : ''
            }${
              phase === 'gift-entering'
                ? ' baby-object-runtime__gift--entering'
                : ''
            }${
              phase === 'gift-opening'
                ? ' baby-object-runtime__gift--opening baby-object-runtime__gift--open'
                : ''
            }`}
            aria-label="礼物盒"
          >
            {giftBoxAsset ? (
              <ResolvedAssetImage
                src={giftBoxAsset.imageSrc}
                alt="礼物盒"
                className="baby-object-runtime__gift-image"
              />
            ) : (
              <Gift className="baby-object-runtime__gift-icon" />
            )}
          </div>
        ) : null}

        {shouldShowSmoke ? (
          <div
            className={`baby-object-runtime__smoke${
              smokeAsset ? ' baby-object-runtime__smoke--skinned' : ''
            }${
              phase === 'item-appearing'
                ? ' baby-object-runtime__smoke--releasing'
                : ''
            }`}
            data-testid="baby-object-smoke-effect"
            aria-hidden="true"
          />
        ) : null}

        {introShowcase ? (
          <div
            className={`baby-object-runtime__intro-item baby-object-runtime__intro-item--${introShowcase.side}${
              introShowcase.isFlying
                ? ' baby-object-runtime__intro-item--flying'
                : ''
            }`}
            data-testid="baby-object-intro-item"
            aria-live="polite"
          >
            <ResolvedAssetImage
              src={introShowcase.item.imageSrc}
              alt={introShowcase.item.itemName}
              className="baby-object-runtime__intro-item-image"
            />
            <span className="baby-object-runtime__intro-item-name">
              {introShowcase.item.itemName}
            </span>
          </div>
        ) : null}

        <div
          className={`baby-object-runtime__item${
            shouldShowCurrentItem ? ' baby-object-runtime__item--visible' : ''
          }${heldPoint ? ' baby-object-runtime__item--held' : ''}${
            phase === 'item-appearing'
              ? ' baby-object-runtime__item--appearing'
              : ''
          }${
            phase === 'correct'
              ? ` baby-object-runtime__item--to-${lastTargetSide ?? 'left'}`
              : phase === 'wrong'
                ? ` baby-object-runtime__item--wrong-${lastTargetSide ?? 'left'}`
                : ''
          }`}
          data-testid="baby-object-current-item"
          aria-live="polite"
          style={
            heldPoint
              ? ({
                  '--baby-object-held-x': `${heldPoint.x * 100}%`,
                  '--baby-object-held-y': `${heldPoint.y * 100}%`,
                } as CSSProperties)
              : undefined
          }
        >
          {shouldShowCurrentItem ? (
            <>
              <ResolvedAssetImage
                src={currentItem.imageSrc}
                alt={currentItem.itemName}
                className="baby-object-runtime__item-image"
              />
              <span className="baby-object-runtime__item-name">
                {currentItem.itemName}
              </span>
            </>
          ) : null}
        </div>

        <div className="baby-object-runtime__hands" aria-hidden="true">
          {(['left', 'right'] as const).map((hand) => {
            const point = runtimeHands[hand];
            const isHoldingHand = heldItem?.hand === hand;
            if (!point) {
              return null;
            }

            return (
              <div
                key={hand}
                className={`baby-object-runtime__hand baby-object-runtime__hand--${hand}${
                  isHoldingHand
                    ? ` baby-object-runtime__hand--holding baby-object-runtime__hand--holding-${hand}-corner`
                    : ''
                }`}
                data-testid={`baby-object-${hand}-hand`}
                style={
                  {
                    '--baby-object-hand-x': `${point.x * 100}%`,
                    '--baby-object-hand-y': `${point.y * 100}%`,
                  } as CSSProperties
                }
              />
            );
          })}
        </div>

        {feedbackText ? (
          <div
            className={`baby-object-runtime__feedback baby-object-runtime__feedback--${phase}`}
          >
            {feedbackText}
          </div>
        ) : null}

        {isComplete ? (
          <div className="baby-object-runtime__complete" role="dialog">
            <PartyPopper className="h-8 w-8" />
            <div>恭喜你！小朋友！</div>
            <div className="baby-object-runtime__complete-actions">
              <button type="button" onClick={resetRuntime}>
                <RotateCcw className="h-4 w-4" />
                再来一次
              </button>
              <button type="button" onClick={onNextLevel}>
                <SkipForward className="h-4 w-4" />
                下一关
              </button>
            </div>
          </div>
        ) : null}

        <div className="baby-object-runtime__baskets">
          {(['left', 'right'] as const).map((side) => {
            const basketItem =
              round?.baskets[side] ?? draft.itemAssets[side === 'left' ? 0 : 1];
            const isOptionReady = isBasketOptionReadyInIntro(side, phase);

            return (
              <div
                key={side}
                className={`baby-object-runtime__basket baby-object-runtime__basket--${side}${
                  phase === 'correct' && lastTargetSide === side
                    ? ' baby-object-runtime__basket--correct'
                    : ''
                }`}
                aria-label={`${side === 'left' ? '左侧' : '右侧'}篮子 ${basketItem.itemName}`}
              >
                <div
                  className={`baby-object-runtime__basket-option${
                    isOptionReady
                      ? ' baby-object-runtime__basket-option--ready'
                      : ''
                  }`}
                >
                  <div className="baby-object-runtime__basket-icon">
                    <ResolvedAssetImage
                      src={basketItem.imageSrc}
                      alt={basketItem.itemName}
                      className="baby-object-runtime__basket-image"
                    />
                  </div>
                  <span className="baby-object-runtime__basket-name">
                    {basketItem.itemName}
                  </span>
                </div>
                <div
                  className={`baby-object-runtime__basket-body${
                    basketAsset
                      ? ' baby-object-runtime__basket-body--skinned'
                      : ''
                  }`}
                >
                  {basketAsset ? (
                    <ResolvedAssetImage
                      src={basketAsset.imageSrc}
                      alt=""
                      className="baby-object-runtime__basket-shell-image"
                    />
                  ) : null}
                </div>
              </div>
            );
          })}
        </div>
      </section>
    </main>
  );
}

export default BabyObjectMatchRuntimeShell;
