import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
  BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
  BABY_OBJECT_MATCH_TEMPLATE_ID,
  BABY_OBJECT_MATCH_TEMPLATE_NAME,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
  MocapConnectionStatus,
  MocapHandInput,
  MocapInputCommand,
  MocapPointInput,
} from '../../services/useMocapInput';
import { useMocapInput } from '../../services/useMocapInput';
import { BabyObjectMatchRuntimeShell } from '../edutainment-runtime/BabyObjectMatchRuntimeShell';
import {
  applyChildMotionWarmupCompletion,
  CHILD_MOTION_CENTER_X,
  CHILD_MOTION_FINISH_DURATION_MS,
  CHILD_MOTION_HOLD_DURATION_MS,
  CHILD_MOTION_NARRATION_DURATION_MS,
  type ChildMotionPoint,
  type ChildMotionWarmupCalibration,
  type ChildMotionWarmupStepId,
  createEmptyChildMotionCalibration,
  getChildMotionTargetX,
  getChildMotionWarmupStep,
  hasCompletedChildMotionWarmupInRuntime,
  isAvatarOnWarmupTarget,
  markChildMotionWarmupCompletedInRuntime,
  resolveNextChildMotionWarmupStep,
} from './childMotionWarmupModel';

type DragHand = 'left' | 'right';
type CameraAccessState = 'idle' | 'requesting' | 'ready' | 'blocked';
type MotionSourceState = 'connecting' | 'ready' | 'waiting' | 'offline';
type WarmupStepPhase = 'intro' | 'active' | 'complete';
type WarmupMocapGestureIntent =
  | 'greeting'
  | 'left-hand'
  | 'right-hand';
type WarmupBodyHandSide = 'left' | 'right';
type WarmupHandIndicators = Record<DragHand, ChildMotionPoint | null>;

const CHILD_MOTION_BABY_OBJECT_DEMO_DRAFT: BabyObjectMatchDraft = {
  draftId: 'child-motion-demo-baby-object-draft',
  profileId: 'child-motion-demo-baby-object-profile',
  templateId: BABY_OBJECT_MATCH_TEMPLATE_ID,
  templateName: BABY_OBJECT_MATCH_TEMPLATE_NAME,
  workTitle: '宝贝识物',
  workDescription: '苹果和香蕉识物分类',
  itemNames: ['苹果', '香蕉'],
  itemAssets: [
    {
      itemId: 'child-motion-demo-baby-object-apple',
      itemName: '苹果',
      imageSrc:
        'data:image/svg+xml;utf8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Crect width=%22512%22 height=%22512%22 rx=%2296%22 fill=%22%23fff1d6%22/%3E%3Ccircle cx=%22256%22 cy=%22266%22 r=%22122%22 fill=%22%23ef5b5b%22/%3E%3Cpath d=%22M250 148c20-50 58-66 102-54-18 45-52 70-102 54Z%22 fill=%22%2351a45f%22/%3E%3Cpath d=%22M256 150c-8-34 2-62 28-84%22 stroke=%22%23734822%22 stroke-width=%2218%22 stroke-linecap=%22round%22 fill=%22none%22/%3E%3Ccircle cx=%22216%22 cy=%22226%22 r=%2218%22 fill=%22%23fff%22 opacity=%22.65%22/%3E%3C/svg%3E',
      assetObjectId: null,
      generationProvider: 'placeholder',
      prompt: '苹果',
    },
    {
      itemId: 'child-motion-demo-baby-object-banana',
      itemName: '香蕉',
      imageSrc:
        'data:image/svg+xml;utf8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Crect width=%22512%22 height=%22512%22 rx=%2296%22 fill=%22%23e9f7ff%22/%3E%3Cpath d=%22M142 302c128 74 228 38 278-122 14 144-84 244-226 220-52-9-84-38-52-98Z%22 fill=%22%23ffd75d%22/%3E%3Cpath d=%22M406 180c6-20 18-34 38-44%22 stroke=%22%238b5b22%22 stroke-width=%2218%22 stroke-linecap=%22round%22/%3E%3Cpath d=%22M158 310c70 40 152 42 218-38%22 stroke=%22%23fff2a7%22 stroke-width=%2220%22 stroke-linecap=%22round%22 fill=%22none%22 opacity=%22.72%22/%3E%3C/svg%3E',
      assetObjectId: null,
      generationProvider: 'placeholder',
      prompt: '香蕉',
    },
  ],
  visualPackage: null,
  themeTags: [BABY_OBJECT_MATCH_EDUTAINMENT_TAG, '宝贝识物'],
  publicationStatus: 'published',
  createdAt: '2026-05-11T00:00:00.000Z',
  updatedAt: '2026-05-11T00:00:00.000Z',
  publishedAt: '2026-05-11T00:00:00.000Z',
};

const WARMUP_MOCAP_WAVE_MIN_X_RANGE = 0.055;
const WARMUP_ARM_SWING_MIN_POINTS = 5;
const WARMUP_ARM_SWING_MIN_VERTICAL_RANGE = 0.08;
const WARMUP_ARM_SWING_MIN_ANGLE_RANGE_DEG = 28;
const WARMUP_ARM_SWING_MIN_REACH = 0.12;
const WARMUP_ARM_SWING_MIN_OUTWARD_X = 0.1;
const WARMUP_ARM_SWING_DIRECTION_EPSILON = 0.012;
const WARMUP_GREETING_WAVE_MIN_POINTS = 5;
const WARMUP_GREETING_WAVE_MIN_X_RANGE = 0.075;
const WARMUP_GREETING_WAVE_MIN_DIRECTION_CHANGES = 1;
const WARMUP_GREETING_WAVE_DIRECTION_EPSILON = 0.008;
const WARMUP_GREETING_WRIST_ABOVE_ELBOW_MARGIN = 0.04;
const WARMUP_GREETING_WRIST_ABOVE_SHOULDER_MARGIN = 0.08;
const WARMUP_STEP_INTRO_DELAY_MS = 1000;
const WARMUP_SUBTITLE_LINE_DELAY_MS = 2000;
const WARMUP_STEP_COMPLETE_PAUSE_MS = 820;
const WARMUP_TOTAL_STEPS = 11;
const AVATAR_MOCAP_DEAD_ZONE = 0.012;
const AVATAR_MOCAP_SMOOTHING = 0.28;
const AVATAR_MOCAP_MAX_STEP = 0.035;

function clampMotionUnit(value: number) {
  return Math.max(0, Math.min(1, value));
}

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

function formatPercent(value: number | null) {
  if (value === null) {
    return '--';
  }

  return `${Math.round(value * 100)}%`;
}

function formatAvatarLeftPercent(value: number) {
  return `${Math.round(clampMotionUnit(value) * 1000) / 10}%`;
}

function createEmptyWarmupHandIndicators(): WarmupHandIndicators {
  return {
    left: null,
    right: null,
  };
}

function resolveMocapHandWithBodySide(
  command: MocapInputCommand,
  side: WarmupBodyHandSide,
) {
  // 本地 mocap 的 handedness 目前是摄像头视角：画面右侧手对应用户身体左手。
  return side === 'left' ? command.rightHand : command.leftHand;
}

function resolveMocapSkeletonWristWithBodySide(
  command: MocapInputCommand,
  side: WarmupBodyHandSide,
) {
  const joints = command.bodyJoints;
  return side === 'left' ? joints?.rightWrist : joints?.leftWrist;
}

function resolveWarmupHandIndicatorsFromMocap(
  command: MocapInputCommand,
): WarmupHandIndicators {
  return {
    left: mocapHandToWarmupIndicatorPoint(
      resolveMocapHandWithBodySide(command, 'left'),
      resolveMocapSkeletonWristWithBodySide(command, 'left'),
    ),
    right: mocapHandToWarmupIndicatorPoint(
      resolveMocapHandWithBodySide(command, 'right'),
      resolveMocapSkeletonWristWithBodySide(command, 'right'),
    ),
  };
}

function resolveMocapJointWithBodySide(
  command: MocapInputCommand,
  side: WarmupBodyHandSide,
  joint: 'shoulder' | 'elbow',
) {
  const joints = command.bodyJoints;
  if (side === 'left') {
    return joint === 'shoulder' ? joints?.rightShoulder : joints?.rightElbow;
  }

  return joint === 'shoulder' ? joints?.leftShoulder : joints?.leftElbow;
}

function mocapHandToChildMotionPoint(
  hand: MocapHandInput | null | undefined,
  command?: MocapInputCommand,
  bodySide?: WarmupBodyHandSide,
): ChildMotionPoint | null {
  if (!hand) {
    return null;
  }

  const armMetrics =
    command && bodySide
      ? resolveWarmupArmMetrics(hand, command, bodySide)
      : null;

  return {
    x: clampMotionUnit(hand.x),
    y: clampMotionUnit(hand.y),
    isRaised: command
      ? isWarmupGreetingHandRaised(hand, command, bodySide)
      : undefined,
    isArmExtended: armMetrics?.isExtended,
    armAngleDeg: armMetrics?.angleDeg,
    armReach: armMetrics?.reach,
  };
}

function mocapHandToWarmupIndicatorPoint(
  hand: MocapHandInput | null | undefined,
  skeletonWrist: MocapPointInput | null | undefined,
): ChildMotionPoint | null {
  // 骨架手腕节点比手掌识别结果更稳定；热身指示器优先跟随骨架手腕。
  const point = skeletonWrist ?? hand?.wrist ?? hand;
  if (!point) {
    return null;
  }

  return {
    x: clampMotionUnit(point.x),
    y: clampMotionUnit(point.y),
  };
}

function appendWarmupMocapPoint(
  points: ChildMotionPoint[],
  point: ChildMotionPoint,
) {
  return [...points, point].slice(-16);
}

function getMotionSourceState(
  mocapStatus: MocapConnectionStatus,
  latestCommand: MocapInputCommand | null,
): MotionSourceState {
  if (mocapStatus === 'connecting' || mocapStatus === 'idle') {
    return 'connecting';
  }

  if (mocapStatus === 'connected') {
    return latestCommand &&
      (Boolean(latestCommand.bodyCenter) ||
        Boolean(latestCommand.hands?.length) ||
        latestCommand.actions.length > 0)
      ? 'ready'
      : 'waiting';
  }

  return 'offline';
}

function getMotionSourceText(state: MotionSourceState) {
  if (state === 'ready') {
    return '动作数据已连接';
  }

  if (state === 'waiting') {
    return '动作数据已连接，等待识别';
  }

  if (state === 'offline') {
    return '动作数据暂不可用，已保留本地调试';
  }

  return '正在连接动作数据';
}

function countWarmupVerticalDirectionChanges(points: ChildMotionPoint[]) {
  let previousDirection = 0;
  let directionChanges = 0;

  for (let index = 1; index < points.length; index += 1) {
    const delta = points[index]!.y - points[index - 1]!.y;
    if (Math.abs(delta) < WARMUP_ARM_SWING_DIRECTION_EPSILON) {
      continue;
    }

    const direction = Math.sign(delta);
    if (previousDirection !== 0 && direction !== previousDirection) {
      directionChanges += 1;
    }
    previousDirection = direction;
  }

  return directionChanges;
}

function hasWarmupArmSwingPath(points: ChildMotionPoint[]) {
  const extendedPoints = points.filter((point) => point.isArmExtended);
  if (extendedPoints.length < WARMUP_ARM_SWING_MIN_POINTS) {
    return false;
  }

  const xValues = extendedPoints.map((point) => point.x);
  const yValues = extendedPoints.map((point) => point.y);
  const angleValues = extendedPoints
    .map((point) => point.armAngleDeg)
    .filter((angle): angle is number => typeof angle === 'number');
  const xRange = Math.max(...xValues) - Math.min(...xValues);
  const yRange = Math.max(...yValues) - Math.min(...yValues);
  const angleRange =
    angleValues.length > 0
      ? Math.max(...angleValues) - Math.min(...angleValues)
      : 0;

  return (
    xRange >= WARMUP_MOCAP_WAVE_MIN_X_RANGE &&
    yRange >= WARMUP_ARM_SWING_MIN_VERTICAL_RANGE &&
    angleRange >= WARMUP_ARM_SWING_MIN_ANGLE_RANGE_DEG &&
    countWarmupVerticalDirectionChanges(extendedPoints) >= 1
  );
}

function countWarmupHorizontalDirectionChanges(points: ChildMotionPoint[]) {
  let previousDirection = 0;
  let directionChanges = 0;

  for (let index = 1; index < points.length; index += 1) {
    const delta = points[index]!.x - points[index - 1]!.x;
    if (Math.abs(delta) < WARMUP_GREETING_WAVE_DIRECTION_EPSILON) {
      continue;
    }

    const direction = Math.sign(delta);
    if (previousDirection !== 0 && direction !== previousDirection) {
      directionChanges += 1;
    }
    previousDirection = direction;
  }

  return directionChanges;
}

function hasWarmupGreetingWavePath(points: ChildMotionPoint[]) {
  const raisedPoints = points.filter((point) => point.isRaised);
  if (raisedPoints.length < WARMUP_GREETING_WAVE_MIN_POINTS) {
    return false;
  }

  const xValues = raisedPoints.map((point) => point.x);
  const xRange = Math.max(...xValues) - Math.min(...xValues);
  return (
    xRange >= WARMUP_GREETING_WAVE_MIN_X_RANGE &&
    countWarmupHorizontalDirectionChanges(raisedPoints) >=
      WARMUP_GREETING_WAVE_MIN_DIRECTION_CHANGES
  );
}

function isWarmupGreetingHandRaised(
  hand: MocapHandInput,
  command: MocapInputCommand,
  bodySide?: WarmupBodyHandSide,
) {
  const wrist = hand.wrist ?? { x: hand.x, y: hand.y };
  const elbow = bodySide
    ? resolveMocapJointWithBodySide(command, bodySide, 'elbow')
    : hand.side === 'left'
      ? command.bodyJoints?.leftElbow
      : hand.side === 'right'
        ? command.bodyJoints?.rightElbow
        : null;
  if (elbow) {
    return wrist.y <= elbow.y + WARMUP_GREETING_WRIST_ABOVE_ELBOW_MARGIN;
  }

  const shoulder = bodySide
    ? resolveMocapJointWithBodySide(command, bodySide, 'shoulder')
    : hand.side === 'left'
      ? command.bodyJoints?.leftShoulder
      : hand.side === 'right'
        ? command.bodyJoints?.rightShoulder
        : null;
  if (shoulder) {
    return wrist.y <= shoulder.y + WARMUP_GREETING_WRIST_ABOVE_SHOULDER_MARGIN;
  }

  return false;
}

function getWarmupPointDistance(left: MocapPointInput, right: MocapPointInput) {
  return Math.hypot(left.x - right.x, left.y - right.y);
}

function resolveWarmupArmMetrics(
  hand: MocapHandInput,
  command: MocapInputCommand,
  bodySide: WarmupBodyHandSide,
) {
  const wrist = hand.wrist ?? { x: hand.x, y: hand.y };
  const shoulder = resolveMocapJointWithBodySide(command, bodySide, 'shoulder');
  if (!shoulder) {
    return null;
  }

  const elbow = resolveMocapJointWithBodySide(command, bodySide, 'elbow');
  const reach = getWarmupPointDistance(shoulder, wrist);
  const outwardX =
    bodySide === 'left' ? shoulder.x - wrist.x : wrist.x - shoulder.x;
  const upperArmReach = elbow ? getWarmupPointDistance(shoulder, elbow) : null;
  const angleDeg =
    (Math.atan2(shoulder.y - wrist.y, Math.abs(wrist.x - shoulder.x)) * 180) /
    Math.PI;
  const isNotDrooping = elbow
    ? wrist.y <= elbow.y + WARMUP_GREETING_WRIST_ABOVE_ELBOW_MARGIN
    : wrist.y <= shoulder.y + WARMUP_GREETING_WRIST_ABOVE_SHOULDER_MARGIN;
  const isExtended =
    outwardX >= WARMUP_ARM_SWING_MIN_OUTWARD_X &&
    reach >= WARMUP_ARM_SWING_MIN_REACH &&
    (!upperArmReach || reach >= upperArmReach * 1.2) &&
    isNotDrooping;

  return {
    angleDeg,
    reach,
    isExtended,
  };
}

function resolveAvatarXFromMocap(command: MocapInputCommand) {
  const bodyCenterX = command.bodyCenter?.x;
  if (typeof bodyCenterX !== 'number' || !Number.isFinite(bodyCenterX)) {
    return null;
  }

  return clampMotionUnit(bodyCenterX);
}

function resolveDampedAvatarX(current: number, target: number) {
  const clampedCurrent = clampMotionUnit(current);
  const clampedTarget = clampMotionUnit(target);
  const delta = clampedTarget - clampedCurrent;
  if (Math.abs(delta) <= AVATAR_MOCAP_DEAD_ZONE) {
    return clampedCurrent;
  }

  const smoothedDelta = delta * AVATAR_MOCAP_SMOOTHING;
  const limitedDelta =
    Math.sign(smoothedDelta) *
    Math.min(Math.abs(smoothedDelta), AVATAR_MOCAP_MAX_STEP);

  return clampMotionUnit(clampedCurrent + limitedDelta);
}

function resolveWarmupMocapGestureIntent(
  stepId: ChildMotionWarmupStepId,
  paths: {
    leftHandPath: ChildMotionPoint[];
    rightHandPath: ChildMotionPoint[];
    primaryHandPath: ChildMotionPoint[];
  },
): WarmupMocapGestureIntent | null {
  if (stepId === 'wave_greeting') {
    if (
      hasWarmupGreetingWavePath(paths.leftHandPath) ||
      hasWarmupGreetingWavePath(paths.rightHandPath) ||
      hasWarmupGreetingWavePath(paths.primaryHandPath)
    ) {
      return 'greeting';
    }
  }

  if (
    stepId === 'wave_left_hand' &&
    hasWarmupArmSwingPath(paths.leftHandPath)
  ) {
    return 'left-hand';
  }

  if (
    stepId === 'wave_right_hand' &&
    hasWarmupArmSwingPath(paths.rightHandPath)
  ) {
    return 'right-hand';
  }

  return null;
}

function getHoldProgress(
  stepId: ChildMotionWarmupStepId,
  avatarX: number,
  holdStartedAt: number | null,
  nowMs: number,
) {
  const step = getChildMotionWarmupStep(stepId);
  if (!isAvatarOnWarmupTarget(step, avatarX) || holdStartedAt === null) {
    return 0;
  }

  return Math.min(1, (nowMs - holdStartedAt) / CHILD_MOTION_HOLD_DURATION_MS);
}

function getStepIndex(stepId: ChildMotionWarmupStepId) {
  const order: ChildMotionWarmupStepId[] = [
    'center_arrive',
    'wave_greeting',
    'warmup_intro',
    'move_left',
    'return_center_1',
    'move_right',
    'return_center_2',
    'wave_left_hand',
    'wave_right_hand',
    'warmup_finish',
    'level_select',
  ];
  return Math.max(0, order.indexOf(stepId));
}

function ChildMotionAvatar({
  avatarX,
  isJumping,
}: {
  avatarX: number;
  isJumping: boolean;
}) {
  return (
    <div
      className={`child-motion-avatar ${isJumping ? 'child-motion-avatar--jumping' : ''}`}
      data-testid="child-motion-avatar"
      style={{
        left: formatAvatarLeftPercent(avatarX),
      }}
      aria-label="用户角色剪影"
    >
      <span className="child-motion-avatar__sprite" aria-hidden="true">
        <span className="child-motion-avatar__head" />
        <span className="child-motion-avatar__body" />
        <span className="child-motion-avatar__arm child-motion-avatar__arm--left" />
        <span className="child-motion-avatar__arm child-motion-avatar__arm--right" />
        <span className="child-motion-avatar__leg child-motion-avatar__leg--left" />
        <span className="child-motion-avatar__leg child-motion-avatar__leg--right" />
      </span>
    </div>
  );
}

function ChildMotionRing({
  targetX,
  progress,
}: {
  targetX: number;
  progress: number;
}) {
  return (
    <div
      className={`child-motion-ring ${progress > 0 ? 'child-motion-ring--active' : ''}`}
      data-testid="child-motion-ring"
      style={
        {
          left: `${targetX * 100}%`,
          '--child-motion-ring-progress': `${Math.round(progress * 360)}deg`,
        } as CSSProperties
      }
      aria-label="绿色圆环"
    >
      <span className="child-motion-ring__core" />
    </div>
  );
}

function ChildMotionGestureGuide({
  stepId,
  leftHandPath,
  rightHandPath,
}: {
  stepId: ChildMotionWarmupStepId;
  leftHandPath: ChildMotionPoint[];
  rightHandPath: ChildMotionPoint[];
}) {
  const isLeft = stepId === 'wave_left_hand';
  const isRight = stepId === 'wave_right_hand';
  const isGreeting = stepId === 'wave_greeting';
  const activePath = isLeft ? leftHandPath : isRight ? rightHandPath : [];

  return (
    <div
      className={`child-motion-gesture-guide ${
        isGreeting ? 'child-motion-gesture-guide--greeting' : ''
      }`}
      aria-hidden="true"
    >
      {isGreeting ? (
        <span className="child-motion-gesture-guide__wave-cat">
          <span className="child-motion-gesture-guide__wave-cat-body" />
          <span className="child-motion-gesture-guide__wave-cat-arm child-motion-gesture-guide__wave-cat-arm--left" />
          <span className="child-motion-gesture-guide__wave-cat-arm child-motion-gesture-guide__wave-cat-arm--right" />
        </span>
      ) : null}
      {isLeft || isRight ? (
        <>
          <span
            className={`child-motion-gesture-guide__arm-swing child-motion-gesture-guide__arm-swing--${isLeft ? 'left' : 'right'}`}
            data-testid={`child-motion-arm-swing-guide-${isLeft ? 'left' : 'right'}`}
          >
            <span className="child-motion-gesture-guide__arm-swing-track" />
            <span className="child-motion-gesture-guide__arm-swing-paw">
              <span className="child-motion-gesture-guide__arm-swing-paw-asset" />
            </span>
          </span>
          {activePath.map((point, index) => (
            <span
              key={`${isLeft ? 'left' : 'right'}-${index}`}
              className="child-motion-gesture-guide__trail"
              style={{
                left: `${point.x * 100}%`,
                top: `${point.y * 100}%`,
                opacity: 0.22 + (index / Math.max(1, activePath.length)) * 0.58,
              }}
            />
          ))}
        </>
      ) : null}
    </div>
  );
}

function ChildMotionHandIndicators({
  hands,
}: {
  hands: WarmupHandIndicators;
}) {
  return (
    <div
      className="baby-object-runtime__hands child-motion-hand-indicators"
      aria-hidden="true"
    >
      {(['left', 'right'] as const).map((hand) => {
        const point = hands[hand];
        if (!point) {
          return null;
        }

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

function ChildMotionCalibrationPanel({
  calibration,
}: {
  calibration: ChildMotionWarmupCalibration;
}) {
  return (
    <div className="child-motion-calibration" aria-label="热身记录">
      <div>
        <span>左边界</span>
        <strong>{formatPercent(calibration.leftBoundary)}</strong>
      </div>
      <div>
        <span>右边界</span>
        <strong>{formatPercent(calibration.rightBoundary)}</strong>
      </div>
      <div>
        <span>左手</span>
        <strong>{calibration.leftHandPath.length}</strong>
      </div>
      <div>
        <span>右手</span>
        <strong>{calibration.rightHandPath.length}</strong>
      </div>
    </div>
  );
}

export function ChildMotionWarmupDemo() {
  const [stepId, setStepId] = useState<ChildMotionWarmupStepId>(() =>
    hasCompletedChildMotionWarmupInRuntime() ? 'level_select' : 'center_arrive',
  );
  const [stepPhase, setStepPhase] = useState<WarmupStepPhase>(() =>
    hasCompletedChildMotionWarmupInRuntime() ? 'active' : 'intro',
  );
  const [isBabyObjectRuntimeOpen, setIsBabyObjectRuntimeOpen] = useState(false);
  const [avatarX, setAvatarX] = useState(CHILD_MOTION_CENTER_X);
  const [calibration, setCalibration] = useState(
    createEmptyChildMotionCalibration,
  );
  const [holdStartedAt, setHoldStartedAt] = useState<number | null>(null);
  const [nowMs, setNowMs] = useState(() => Date.now());
  const [leftHandPath, setLeftHandPath] = useState<ChildMotionPoint[]>([]);
  const [rightHandPath, setRightHandPath] = useState<ChildMotionPoint[]>([]);
  const [handIndicators, setHandIndicators] = useState(
    createEmptyWarmupHandIndicators,
  );
  const [activeHand, setActiveHand] = useState<DragHand | null>(null);
  const [isJumping, setIsJumping] = useState(false);
  const [justCompletedText, setJustCompletedText] = useState<string | null>(
    null,
  );
  const [subtitleLineIndex, setSubtitleLineIndex] = useState(0);
  const [cameraAccessState, setCameraAccessState] = useState<CameraAccessState>(
    () =>
      typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia
        ? 'blocked'
        : 'idle',
  );
  const holdCompletionRef = useRef(false);
  const cameraVideoRef = useRef<HTMLVideoElement | null>(null);
  const cameraStreamRef = useRef<MediaStream | null>(null);
  const handledMocapPacketKeyRef = useRef<string | null>(null);
  const completionTimeoutRef = useRef<number | null>(null);
  const feedbackTimeoutRef = useRef<number | null>(null);

  const step = getChildMotionWarmupStep(stepId);
  const mocapInput = useMocapInput({
    enabled:
      step.kind === 'position' ||
      step.kind === 'gesture' ||
      step.kind === 'narration' ||
      step.kind === 'finish',
  });
  const stepIndex = getStepIndex(stepId);
  const progressPercent = Math.round(
    (stepIndex / (WARMUP_TOTAL_STEPS - 1)) * 100,
  );
  const holdProgress = getHoldProgress(stepId, avatarX, holdStartedAt, nowMs);
  const isStepActive = stepPhase === 'active';
  const shouldShowStepCues = stepPhase !== 'intro';
  const displayHoldProgress =
    stepPhase === 'complete' && step.kind === 'position' ? 1 : holdProgress;
  const targetX = step.target ? getChildMotionTargetX(step.target) : null;
  const motionSourceState = getMotionSourceState(
    mocapInput.status,
    mocapInput.latestCommand,
  );
  const motionSourceText = getMotionSourceText(motionSourceState);

  const completeStep = useCallback(
    (completion: Parameters<typeof applyChildMotionWarmupCompletion>[2]) => {
      if (stepPhase !== 'active') {
        return;
      }

      setCalibration((current) =>
        applyChildMotionWarmupCompletion(stepId, current, completion),
      );

      const nextStep = resolveNextChildMotionWarmupStep(stepId);
      if (stepId === 'warmup_finish') {
        markChildMotionWarmupCompletedInRuntime();
      }

      const completionText =
        stepId === 'wave_greeting' || stepId === 'warmup_finish'
          ? null
          : '真棒';
      setJustCompletedText(completionText);
      setStepPhase('complete');
      setHoldStartedAt(null);
      holdCompletionRef.current = false;

      if (feedbackTimeoutRef.current !== null) {
        window.clearTimeout(feedbackTimeoutRef.current);
      }
      feedbackTimeoutRef.current = window.setTimeout(() => {
        feedbackTimeoutRef.current = null;
        setJustCompletedText(null);
      }, WARMUP_STEP_COMPLETE_PAUSE_MS);

      if (completionTimeoutRef.current !== null) {
        window.clearTimeout(completionTimeoutRef.current);
      }
      completionTimeoutRef.current = window.setTimeout(() => {
        completionTimeoutRef.current = null;
        setStepId(nextStep);
        setStepPhase(nextStep === 'level_select' ? 'active' : 'intro');
      }, WARMUP_STEP_COMPLETE_PAUSE_MS);
    },
    [stepId, stepPhase],
  );

  useEffect(() => {
    const timer = window.setInterval(() => setNowMs(Date.now()), 120);
    return () => window.clearInterval(timer);
  }, []);

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

  useEffect(() => {
    const videoElement = cameraVideoRef.current;
    if (
      typeof navigator === 'undefined' ||
      !navigator.mediaDevices?.getUserMedia ||
      !videoElement
    ) {
      return;
    }

    let isMounted = true;
    const startCamera = async () => {
      if (!navigator.mediaDevices?.getUserMedia) {
        if (isMounted) {
          setCameraAccessState('blocked');
        }
        return;
      }

      try {
        setCameraAccessState('requesting');
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: false,
          video: {
            facingMode: 'user',
          },
        });

        if (!isMounted) {
          stream.getTracks().forEach((track) => track.stop());
          return;
        }

        cameraStreamRef.current?.getTracks().forEach((track) => track.stop());
        cameraStreamRef.current = stream;
        videoElement.srcObject = stream;
        await videoElement.play();
        setCameraAccessState('ready');
      } catch {
        cameraStreamRef.current?.getTracks().forEach((track) => track.stop());
        cameraStreamRef.current = null;
        videoElement.srcObject = null;
        if (isMounted) {
          setCameraAccessState('blocked');
        }
      }
    };

    void startCamera();

    return () => {
      isMounted = false;
      const stream = cameraStreamRef.current;
      cameraStreamRef.current = null;
      if (stream) {
        stream.getTracks().forEach((track) => track.stop());
      }
      videoElement.srcObject = null;
    };
  }, []);

  useEffect(() => {
    const stream = cameraStreamRef.current;
    const videoElement = cameraVideoRef.current;
    if (stream && videoElement && videoElement.srcObject !== stream) {
      videoElement.srcObject = stream;
    }
  }, [cameraAccessState]);

  useEffect(() => {
    holdCompletionRef.current = false;
    setHoldStartedAt(null);
    setLeftHandPath([]);
    setRightHandPath([]);
    setSubtitleLineIndex(0);
    handledMocapPacketKeyRef.current = null;

    if (step.kind === 'levelSelect') {
      setStepPhase('active');
      return;
    }

    setStepPhase('intro');
    const timeout = window.setTimeout(
      () =>
        setStepPhase((current) => (current === 'intro' ? 'active' : current)),
      WARMUP_STEP_INTRO_DELAY_MS,
    );
    return () => window.clearTimeout(timeout);
  }, [step.kind, stepId]);

  useEffect(() => {
    if (step.spokenLines.length <= 1) {
      return;
    }

    setSubtitleLineIndex(0);
    const timeout = window.setTimeout(() => {
      setSubtitleLineIndex((current) =>
        Math.min(current + 1, step.spokenLines.length - 1),
      );
    }, WARMUP_SUBTITLE_LINE_DELAY_MS);
    return () => window.clearTimeout(timeout);
  }, [step.spokenLines, stepId]);

  useEffect(() => {
    if (step.kind !== 'position' || !isStepActive) {
      return;
    }

    if (!isAvatarOnWarmupTarget(step, avatarX)) {
      setHoldStartedAt(null);
      holdCompletionRef.current = false;
      return;
    }

    setHoldStartedAt((current) => current ?? Date.now());
  }, [avatarX, isStepActive, step]);

  useEffect(() => {
    if (
      step.kind !== 'position' ||
      !isStepActive ||
      holdStartedAt === null ||
      holdCompletionRef.current ||
      nowMs - holdStartedAt < CHILD_MOTION_HOLD_DURATION_MS
    ) {
      return;
    }

    holdCompletionRef.current = true;
    completeStep({ type: 'position', avatarX });
  }, [avatarX, completeStep, holdStartedAt, isStepActive, nowMs, step.kind]);

  useEffect(() => {
    if (
      !isStepActive ||
      (step.kind !== 'narration' && step.kind !== 'finish')
    ) {
      return;
    }

    const timeout = window.setTimeout(
      () => completeStep({ type: 'narration' }),
      step.kind === 'finish'
        ? CHILD_MOTION_FINISH_DURATION_MS
        : CHILD_MOTION_NARRATION_DURATION_MS,
    );
    return () => window.clearTimeout(timeout);
  }, [completeStep, isStepActive, step.kind]);

  useEffect(() => {
    if (step.kind !== 'gesture' || !isStepActive || !mocapInput.latestCommand) {
      return;
    }

    const command = mocapInput.latestCommand;
    const packetKey =
      mocapInput.rawPacketPreview?.receivedAtMs !== undefined
        ? `${mocapInput.rawPacketPreview.receivedAtMs}:${mocapInput.rawPacketPreview.text}`
        : JSON.stringify(command);
    if (handledMocapPacketKeyRef.current === packetKey) {
      return;
    }

    const leftBodyHand = resolveMocapHandWithBodySide(command, 'left');
    const rightBodyHand = resolveMocapHandWithBodySide(command, 'right');
    const primaryBodySide =
      command.primaryHand === leftBodyHand
        ? 'left'
        : command.primaryHand === rightBodyHand
          ? 'right'
          : undefined;
    const primaryPoint = mocapHandToChildMotionPoint(
      command.primaryHand,
      command,
      primaryBodySide,
    );
    const primaryHandSide = command.primaryHand?.side ?? 'unknown';
    const fallbackPrimaryToLeft =
      Boolean(primaryPoint) &&
      !leftBodyHand &&
      (primaryBodySide === 'left' ||
        (primaryHandSide === 'unknown' && stepId === 'wave_greeting'));
    const fallbackPrimaryToRight =
      Boolean(primaryPoint) && !rightBodyHand && primaryBodySide === 'right';
    const leftPoint =
      mocapHandToChildMotionPoint(leftBodyHand, command, 'left') ??
      (fallbackPrimaryToLeft ? primaryPoint : null);
    const rightPoint =
      mocapHandToChildMotionPoint(rightBodyHand, command, 'right') ??
      (fallbackPrimaryToRight ? primaryPoint : null);
    const nextLeftHandPath = leftPoint
      ? appendWarmupMocapPoint(leftHandPath, leftPoint)
      : leftHandPath;
    const nextRightHandPath = rightPoint
      ? appendWarmupMocapPoint(rightHandPath, rightPoint)
      : rightHandPath;
    const nextPrimaryHandPath = primaryPoint
      ? primaryBodySide === 'right'
        ? nextRightHandPath
        : nextLeftHandPath
      : [];
    handledMocapPacketKeyRef.current = packetKey;
    if (leftPoint) {
      setLeftHandPath(nextLeftHandPath);
    }
    if (rightPoint) {
      setRightHandPath(nextRightHandPath);
    }

    const intent = resolveWarmupMocapGestureIntent(stepId, {
      leftHandPath: nextLeftHandPath,
      rightHandPath: nextRightHandPath,
      primaryHandPath: nextPrimaryHandPath,
    });
    if (!intent) {
      return;
    }

    if (intent === 'right-hand') {
      const path = [...nextRightHandPath, rightPoint].filter(
        (point): point is ChildMotionPoint => Boolean(point),
      );
      completeStep({ type: 'right-hand', path: path.slice(-16) });
      return;
    }

    const path = [...nextLeftHandPath, leftPoint].filter(
      (point): point is ChildMotionPoint => Boolean(point),
    );
    completeStep({ type: 'left-hand', path: path.slice(-16) });
  }, [
    completeStep,
    leftHandPath,
    mocapInput.latestCommand,
    mocapInput.rawPacketPreview?.receivedAtMs,
    mocapInput.rawPacketPreview?.text,
    rightHandPath,
    isStepActive,
    step.kind,
    stepId,
  ]);

  useEffect(() => {
    if (stepPhase === 'complete' || !mocapInput.latestCommand) {
      return;
    }

    setHandIndicators(
      resolveWarmupHandIndicatorsFromMocap(mocapInput.latestCommand),
    );
  }, [
    mocapInput.latestCommand,
    mocapInput.rawPacketPreview?.receivedAtMs,
    mocapInput.rawPacketPreview?.text,
    stepPhase,
  ]);

  useEffect(() => {
    if (stepPhase === 'complete' || !mocapInput.latestCommand) {
      return;
    }

    const nextAvatarX = resolveAvatarXFromMocap(mocapInput.latestCommand);
    if (nextAvatarX === null) {
      return;
    }

    setAvatarX((current) => resolveDampedAvatarX(current, nextAvatarX));
  }, [
    mocapInput.latestCommand,
    mocapInput.rawPacketPreview?.receivedAtMs,
    mocapInput.rawPacketPreview?.text,
    stepPhase,
  ]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.repeat) {
        return;
      }

      if (stepPhase === 'complete') {
        return;
      }

      const key = event.key.toLowerCase();
      if (key === 'a') {
        setAvatarX(0.34);
        return;
      }

      if (key === 'd') {
        setAvatarX(0.66);
        return;
      }

      if (event.code === 'Space') {
        event.preventDefault();
        setIsJumping(true);
        window.setTimeout(() => setIsJumping(false), 360);
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [stepPhase]);

  useEffect(() => {
    const handleKeyUp = (event: KeyboardEvent) => {
      const key = event.key.toLowerCase();
      if (
        key === 'a' ||
        key === 'd' ||
        event.code === 'KeyA' ||
        event.code === 'KeyD'
      ) {
        setAvatarX(CHILD_MOTION_CENTER_X);
      }
    };

    window.addEventListener('keyup', handleKeyUp);
    return () => window.removeEventListener('keyup', handleKeyUp);
  }, []);

  const handleStagePointerDown = (event: ReactPointerEvent<HTMLElement>) => {
    if (!isStepActive) {
      return;
    }

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

    event.preventDefault();
    const nextHand: DragHand =
      event.button === 2 || event.buttons === 2 ? 'right' : 'left';
    setActiveHand(nextHand);
    const point = normalizePointerPoint(event, event.currentTarget);
    setHandIndicators((current) => ({ ...current, [nextHand]: point }));
    if (nextHand === 'left') {
      setLeftHandPath([point]);
    } else {
      setRightHandPath([point]);
    }
    if (typeof event.currentTarget.setPointerCapture === 'function') {
      event.currentTarget.setPointerCapture(event.pointerId);
    }
  };

  const handleStagePointerMove = (event: ReactPointerEvent<HTMLElement>) => {
    if (!activeHand) {
      return;
    }

    const point = normalizePointerPoint(event, event.currentTarget);
    setHandIndicators((current) => ({ ...current, [activeHand]: point }));
    const appendPoint = (points: ChildMotionPoint[]) =>
      [...points, point].slice(-16);
    if (activeHand === 'left') {
      setLeftHandPath(appendPoint);
    } else {
      setRightHandPath(appendPoint);
    }
  };

  const handleStagePointerUp = (event: ReactPointerEvent<HTMLElement>) => {
    if (!activeHand) {
      return;
    }

    if (
      typeof event.currentTarget.hasPointerCapture === 'function' &&
      event.currentTarget.hasPointerCapture(event.pointerId)
    ) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
    const hand = activeHand;
    const point = normalizePointerPoint(event, event.currentTarget);
    const completedPath =
      hand === 'left'
        ? [...leftHandPath, point].slice(-16)
        : [...rightHandPath, point].slice(-16);
    setActiveHand(null);

    if (!isStepActive) {
      return;
    }

    if (stepId === 'wave_greeting') {
      completeStep({ type: 'left-hand', path: completedPath });
      return;
    }

    if (stepId === 'wave_left_hand' && hand === 'left') {
      completeStep({ type: 'left-hand', path: completedPath });
      return;
    }

    if (stepId === 'wave_right_hand' && hand === 'right') {
      completeStep({ type: 'right-hand', path: completedPath });
    }
  };

  const handleStartBabyObjectLevel = () => {
    setIsBabyObjectRuntimeOpen(true);
  };

  const shouldHideStepTitle = stepId === 'center_arrive';
  const subtitleText = step.spokenLines[subtitleLineIndex] ?? step.spokenLines[0];

  if (isBabyObjectRuntimeOpen) {
    return (
      <BabyObjectMatchRuntimeShell
        draft={CHILD_MOTION_BABY_OBJECT_DEMO_DRAFT}
        onBack={() => {
          setIsBabyObjectRuntimeOpen(false);
          setStepId('level_select');
        }}
      />
    );
  }

  return (
    <main className="child-motion-demo" data-testid="child-motion-demo">
      <div className="child-motion-orientation-tip" role="status">
        请横屏体验
      </div>

      <section
        className={`child-motion-stage child-motion-stage--${stepPhase}`}
        data-testid="child-motion-stage"
        data-step-phase={stepPhase}
        onPointerDown={handleStagePointerDown}
        onPointerMove={handleStagePointerMove}
        onPointerUp={handleStagePointerUp}
        onPointerCancel={handleStagePointerUp}
        onContextMenu={(event) => event.preventDefault()}
      >
        <video
          ref={cameraVideoRef}
          className="child-motion-camera-layer"
          aria-hidden="true"
          autoPlay
          muted
          playsInline
        />
        {motionSourceState !== 'ready' ? (
          <div
            className={`child-motion-camera-state child-motion-camera-state--${motionSourceState}`}
            aria-live="polite"
          >
            {motionSourceText}
          </div>
        ) : null}
        <div className="child-motion-floor" aria-hidden="true" />
        {shouldShowStepCues && targetX !== null && step.kind === 'position' ? (
          <ChildMotionRing targetX={targetX} progress={displayHoldProgress} />
        ) : null}
        {shouldShowStepCues && step.kind === 'gesture' ? (
          <ChildMotionGestureGuide
            stepId={stepId}
            leftHandPath={leftHandPath}
            rightHandPath={rightHandPath}
          />
        ) : null}
        <ChildMotionHandIndicators hands={handIndicators} />
        <ChildMotionAvatar avatarX={avatarX} isJumping={isJumping} />
        {justCompletedText ? (
          <div className="child-motion-floating-reward">
            {justCompletedText}
          </div>
        ) : null}

        <div className="child-motion-hud child-motion-hud--top">
          <span className="child-motion-step-count">{`${Math.min(stepIndex + 1, WARMUP_TOTAL_STEPS)}/${WARMUP_TOTAL_STEPS}`}</span>
          <div
            className={
              shouldHideStepTitle
                ? 'child-motion-hud__copy child-motion-hud__copy--subtitle-only'
                : 'child-motion-hud__copy'
            }
          >
            {shouldHideStepTitle ? null : <h1>{step.title}</h1>}
            <p>{subtitleText}</p>
          </div>
          <span className="child-motion-progress">{progressPercent}%</span>
        </div>

        {step.kind === 'levelSelect' ? (
          <div className="child-motion-start-panel">
            <button type="button" onClick={handleStartBabyObjectLevel}>
              开始游戏
            </button>
          </div>
        ) : null}

        <ChildMotionCalibrationPanel calibration={calibration} />
      </section>
    </main>
  );
}

export default ChildMotionWarmupDemo;
