import { useAuth } from "@/_core/hooks/useAuth";
import { trpc } from "@/lib/trpc";
import {
  createMediaMarker,
  createMediaSession,
  finalizeMediaSession,
  formatRecordingTime,
  getMediaSession,
  pickBitrate,
  pickRecorderMimeType,
  signalMediaSession,
  type MediaMarker,
  type MediaSession,
  uploadMediaSegment,
} from "@/lib/media";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
import { Slider } from "@/components/ui/slider";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useBackgroundTask } from "@/hooks/useBackgroundTask";
import { toast } from "sonner";
import {
  ACTION_LABELS as RECOGNIZED_ACTION_LABELS,
  type ActionObservation,
  type ActionType,
  type TrackingState,
  recognizeActionFrame,
  stabilizeActionFrame,
} from "@/lib/actionRecognition";
import { applyTrackZoom, getCameraVideoConstraints, readTrackZoomState } from "@/lib/camera";
import { formatDateTimeShanghai } from "@/lib/time";
import {
  Activity,
  Camera,
  Circle,
  CloudUpload,
  Download,
  FlipHorizontal,
  Loader2,
  Maximize2,
  Minus,
  Minimize2,
  MonitorUp,
  Plus,
  Scissors,
  ShieldAlert,
  Smartphone,
  Sparkles,
  ListTodo,
  Video,
  VideoOff,
  Wifi,
  WifiOff,
  Zap,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

type RecorderMode =
  | "idle"
  | "preparing"
  | "recording"
  | "reconnecting"
  | "finalizing"
  | "archived";

type PendingSegment = {
  sequence: number;
  durationMs: number;
  blob: Blob;
};

const SEGMENT_LENGTH_MS = 60_000;
const MOTION_SAMPLE_MS = 1_500;
const MOTION_THRESHOLD = 18;
const MOTION_COOLDOWN_MS = 8_000;
const ACTION_SAMPLE_MS = 2_500;
const INVALID_RECORDING_WINDOW_MS = 60_000;

const QUALITY_PRESETS = {
  economy: {
    label: "节省流量",
    subtitle: "1.0 Mbps · 适合移动网络",
    description: "优先降低上传流量，保留基础训练画面",
  },
  balanced: {
    label: "均衡模式",
    subtitle: "1.4-1.9 Mbps · 推荐",
    description: "Chrome 录制与推流的默认平衡配置",
  },
  clarity: {
    label: "清晰优先",
    subtitle: "2.5 Mbps · Wi-Fi 推荐",
    description: "保留更多动作细节，适合回放和分析",
  },
} as const;

function waitForIceGathering(peer: RTCPeerConnection) {
  if (peer.iceGatheringState === "complete") {
    return Promise.resolve();
  }

  return new Promise<void>((resolve) => {
    const handleStateChange = () => {
      if (peer.iceGatheringState === "complete") {
        peer.removeEventListener("icegatheringstatechange", handleStateChange);
        resolve();
      }
    };

    peer.addEventListener("icegatheringstatechange", handleStateChange);
  });
}

function isMobileDevice() {
  return (
    /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
    window.matchMedia("(max-width: 768px)").matches
  );
}

function getArchiveProgress(session: MediaSession | null) {
  if (!session) return 0;
  switch (session.archiveStatus) {
    case "queued":
      return 45;
    case "processing":
      return 78;
    case "completed":
      return 100;
    case "failed":
      return 100;
    default:
      return 20;
  }
}

function getArchivePhaseLabel(mode: RecorderMode, session: MediaSession | null, taskProgress?: number | null) {
  if (mode === "finalizing" && !taskProgress) {
    return "正在提交归档任务";
  }
  if (session?.archiveStatus === "completed") {
    return "回放已生成";
  }
  if (session?.archiveStatus === "failed") {
    return "归档失败";
  }
  if (session?.archiveStatus === "processing") {
    return "正在生成回放";
  }
  if (session?.archiveStatus === "queued") {
    return "正在合并分段";
  }
  return "等待归档";
}

function formatFileSize(bytes: number) {
  if (!bytes) return "0 MB";
  return `${(bytes / 1024 / 1024).toFixed(bytes > 20 * 1024 * 1024 ? 1 : 2)} MB`;
}

function createActionSummary(): Record<ActionType, number> {
  return {
    forehand: 0,
    backhand: 0,
    serve: 0,
    volley: 0,
    overhead: 0,
    slice: 0,
    lob: 0,
    unknown: 0,
  };
}

function summarizeActions(actionSummary: Record<ActionType, number>) {
  return Object.entries(actionSummary)
    .filter(([actionType, count]) => actionType !== "unknown" && count > 0)
    .sort((left, right) => right[1] - left[1])
    .map(([actionType, count]) => ({
      actionType: actionType as ActionType,
      label: RECOGNIZED_ACTION_LABELS[actionType as ActionType] || actionType,
      count,
    }));
}

export default function Recorder() {
  const { user } = useAuth();
  const utils = trpc.useUtils();
  const runtimeQuery = trpc.analysis.runtimeGet.useQuery(undefined, {
    refetchInterval: 1000,
    refetchIntervalInBackground: true,
  });
  const finalizeTaskMutation = trpc.task.createMediaFinalize.useMutation({
    onSuccess: (data) => {
      setArchiveTaskId(data.taskId);
      toast.success("录制归档任务已提交");
    },
    onError: (error) => {
      toast.error(`录制归档任务提交失败: ${error.message}`);
    },
  });

  const liveVideoRef = useRef<HTMLVideoElement>(null);
  const playbackVideoRef = useRef<HTMLVideoElement>(null);
  const motionCanvasRef = useRef<HTMLCanvasElement>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const recorderRef = useRef<MediaRecorder | null>(null);
  const peerRef = useRef<RTCPeerConnection | null>(null);
  const actionPoseRef = useRef<any>(null);
  const currentSegmentStartedAtRef = useRef<number>(0);
  const recordingStartedAtRef = useRef<number>(0);
  const segmentSequenceRef = useRef(0);
  const motionFrameRef = useRef<Uint8ClampedArray | null>(null);
  const lastMotionMarkerAtRef = useRef(0);
  const actionTickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const actionFrameInFlightRef = useRef(false);
  const actionTrackingRef = useRef<TrackingState>({});
  const actionHistoryRef = useRef<ActionObservation[]>([]);
  const actionSummaryRef = useRef<Record<ActionType, number>>(createActionSummary());
  const lastRecognizedActionAtRef = useRef<number>(0);
  const lastActionMarkerAtRef = useRef<number>(0);
  const latestRecognizedActionRef = useRef<ActionType>("unknown");
  const validityOverrideRef = useRef<"valid" | "invalid" | null>(null);
  const invalidAutoMarkedRef = useRef(false);
  const pendingUploadsRef = useRef<PendingSegment[]>([]);
  const uploadInFlightRef = useRef(false);
  const currentSessionRef = useRef<MediaSession | null>(null);
  const segmentTickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const timerTickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const motionTickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const modeRef = useRef<RecorderMode>("idle");
  const reconnectAttemptsRef = useRef(0);
  const facingModeRef = useRef<"user" | "environment">("environment");
  const suppressTrackEndedRef = useRef(false);
  const zoomTargetRef = useRef(1);

  const [mode, setMode] = useState<RecorderMode>("idle");
  const [qualityPreset, setQualityPreset] = useState<keyof typeof QUALITY_PRESETS>("economy");
  const [facingMode, setFacingMode] = useState<"user" | "environment">("environment");
  const [cameraActive, setCameraActive] = useState(false);
  const [hasMultipleCameras, setHasMultipleCameras] = useState(false);
  const [durationMs, setDurationMs] = useState(0);
  const [sessionMode, setSessionMode] = useState<"practice" | "pk">("practice");
  const [isOnline, setIsOnline] = useState(() => navigator.onLine);
  const [reconnectAttempts, setReconnectAttempts] = useState(0);
  const [queuedSegments, setQueuedSegments] = useState(0);
  const [queuedBytes, setQueuedBytes] = useState(0);
  const [uploadedSegments, setUploadedSegments] = useState(0);
  const [uploadBytes, setUploadBytes] = useState(0);
  const [cameraError, setCameraError] = useState("");
  const [title, setTitle] = useState(() => `训练录制 ${formatDateTimeShanghai(new Date(), { year: undefined, second: undefined })}`);
  const [mediaSession, setMediaSession] = useState<MediaSession | null>(null);
  const [markers, setMarkers] = useState<MediaMarker[]>([]);
  const [connectionState, setConnectionState] = useState<RTCPeerConnectionState>("new");
  const [immersivePreview, setImmersivePreview] = useState(false);
  const [archiveTaskId, setArchiveTaskId] = useState<string | null>(null);
  const [zoomState, setZoomState] = useState(() => readTrackZoomState(null));
  const [actionSummary, setActionSummary] = useState<Record<ActionType, number>>(() => createActionSummary());
  const [currentDetectedAction, setCurrentDetectedAction] = useState<ActionType>("unknown");
  const [recordingValidity, setRecordingValidity] = useState<"pending" | "valid" | "invalid">("pending");
  const [recordingValidityReason, setRecordingValidityReason] = useState("");

  const mobile = useMemo(() => isMobileDevice(), []);
  const mimeType = useMemo(() => pickRecorderMimeType(), []);
  const runtimeRole = runtimeQuery.data?.role ?? "idle";
  const liveAnalysisRuntime = runtimeQuery.data?.runtimeSession;
  const liveAnalysisOccupied = runtimeRole === "viewer" && liveAnalysisRuntime?.status === "active";
  const currentPlaybackUrl = mediaSession?.playback.mp4Url || mediaSession?.playback.webmUrl || "";
  const archiveTaskQuery = useBackgroundTask(archiveTaskId);
  const archiveProgress = archiveTaskQuery.data?.progress ?? getArchiveProgress(mediaSession);
  const archivePhaseLabel = getArchivePhaseLabel(mode, mediaSession, archiveTaskQuery.data?.progress);
  const totalUploadBytes = uploadBytes + queuedBytes;
  const uploadStillDraining = mode === "finalizing" && (queuedBytes > 0 || queuedSegments > 0 || finalizeTaskMutation.isPending);
  const archiveRunning =
    archiveTaskQuery.data?.status === "queued" ||
    archiveTaskQuery.data?.status === "running" ||
    mediaSession?.archiveStatus === "queued" ||
    mediaSession?.archiveStatus === "processing";
  const canLeaveRecorderPage = !uploadStillDraining && (archiveRunning || mode === "archived");
  const recognizedActionItems = useMemo(() => summarizeActions(actionSummary), [actionSummary]);

  const syncSessionState = useCallback((session: MediaSession | null) => {
    currentSessionRef.current = session;
    setMediaSession(session);
    setMarkers(session?.markers ?? []);
    setUploadedSegments(session?.uploadedSegments ?? 0);
    setUploadBytes(session?.uploadedBytes ?? 0);
  }, []);

  const syncQueuedUploadState = useCallback(() => {
    setQueuedSegments(pendingUploadsRef.current.length);
    setQueuedBytes(pendingUploadsRef.current.reduce((total, item) => total + item.blob.size, 0));
  }, []);

  useEffect(() => {
    modeRef.current = mode;
  }, [mode]);

  useEffect(() => {
    reconnectAttemptsRef.current = reconnectAttempts;
  }, [reconnectAttempts]);

  useEffect(() => {
    facingModeRef.current = facingMode;
  }, [facingMode]);

  useEffect(() => {
    if (archiveTaskQuery.data?.status === "succeeded") {
      void (async () => {
        if (currentSessionRef.current?.id) {
          const response = await getMediaSession(currentSessionRef.current.id);
          syncSessionState(response.session);
        }
        setMode("archived");
        utils.video.list.invalidate();
        toast.success("回放文件已归档完成");
        setArchiveTaskId(null);
      })();
    } else if (archiveTaskQuery.data?.status === "failed") {
      toast.error(`录制归档失败: ${archiveTaskQuery.data.error || "未知错误"}`);
      setMode("idle");
      setArchiveTaskId(null);
    }
  }, [archiveTaskQuery.data, syncSessionState, utils.video.list]);

  const stopTickers = useCallback(() => {
    if (segmentTickerRef.current) clearInterval(segmentTickerRef.current);
    if (timerTickerRef.current) clearInterval(timerTickerRef.current);
    if (motionTickerRef.current) clearInterval(motionTickerRef.current);
    if (actionTickerRef.current) clearInterval(actionTickerRef.current);
    segmentTickerRef.current = null;
    timerTickerRef.current = null;
    motionTickerRef.current = null;
    actionTickerRef.current = null;
  }, []);

  const closePeer = useCallback(() => {
    if (peerRef.current) {
      peerRef.current.onconnectionstatechange = null;
      peerRef.current.close();
      peerRef.current = null;
    }
    setConnectionState("closed");
  }, []);

  const stopCamera = useCallback(() => {
    suppressTrackEndedRef.current = true;
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((track) => track.stop());
      streamRef.current = null;
    }
    if (liveVideoRef.current) {
      liveVideoRef.current.srcObject = null;
    }
    setZoomState(readTrackZoomState(null));
    setCameraActive(false);
  }, []);

  const syncZoomState = useCallback(async (preferredZoom?: number, providedTrack?: MediaStreamTrack | null) => {
    const track = providedTrack || streamRef.current?.getVideoTracks()[0] || null;
    if (!track) {
      zoomTargetRef.current = 1;
      setZoomState(readTrackZoomState(null));
      return;
    }

    let nextState = readTrackZoomState(track);
    if (nextState.supported && preferredZoom != null && Math.abs(preferredZoom - nextState.current) > nextState.step / 2) {
      try {
        nextState = await applyTrackZoom(track, preferredZoom);
      } catch {
        nextState = readTrackZoomState(track);
      }
    }

    zoomTargetRef.current = nextState.current;
    setZoomState(nextState);
  }, []);

  const updateZoom = useCallback(async (nextZoom: number) => {
    const track = streamRef.current?.getVideoTracks()[0] || null;
    if (!track) return;

    try {
      const nextState = await applyTrackZoom(track, nextZoom);
      zoomTargetRef.current = nextState.current;
      setZoomState(nextState);
    } catch (error: any) {
      toast.error(`镜头缩放调整失败: ${error?.message || "当前设备不支持"}`);
    }
  }, []);

  const stepZoom = useCallback((direction: -1 | 1) => {
    if (!zoomState.supported) return;
    const nextZoom = Math.max(
      zoomState.min,
      Math.min(zoomState.max, zoomState.current + zoomState.step * direction),
    );
    void updateZoom(nextZoom);
  }, [updateZoom, zoomState]);

  const startCamera = useCallback((
    async (
      nextFacingMode = facingMode,
      preferredZoom = zoomTargetRef.current,
      preset: keyof typeof QUALITY_PRESETS = qualityPreset,
    ) => {
    if (liveAnalysisOccupied) {
      const title = liveAnalysisRuntime?.title || "其他设备正在实时分析";
      toast.error(`${title}，当前设备不能再开启录制摄像头`);
      throw new Error("当前账号已有其他设备正在实时分析");
    }
    try {
      if (streamRef.current) {
        streamRef.current.getTracks().forEach((track) => track.stop());
        streamRef.current = null;
      }

      const stream = await navigator.mediaDevices.getUserMedia({
        video: getCameraVideoConstraints(nextFacingMode, mobile, preset),
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
        },
      });

      stream.getVideoTracks().forEach((track) => {
        track.onended = () => {
          if (!suppressTrackEndedRef.current && (modeRef.current === "recording" || modeRef.current === "reconnecting")) {
            void attemptReconnect();
          }
        };
      });

      suppressTrackEndedRef.current = false;
      streamRef.current = stream;
      if (liveVideoRef.current) {
        liveVideoRef.current.srcObject = stream;
        await liveVideoRef.current.play();
      }
      await syncZoomState(preferredZoom, stream.getVideoTracks()[0] || null);
      setCameraError("");
      setCameraActive(true);
      return stream;
    } catch (error: any) {
      const message = error?.message || "无法访问摄像头";
      setCameraError(message);
      toast.error(`摄像头启动失败: ${message}`);
      throw error;
    }
  }), [facingMode, liveAnalysisOccupied, liveAnalysisRuntime?.title, mobile, qualityPreset, syncZoomState]);

  const ensurePreviewStream = useCallback(async () => {
    if (streamRef.current) {
      if (liveVideoRef.current && liveVideoRef.current.srcObject !== streamRef.current) {
        liveVideoRef.current.srcObject = streamRef.current;
        await liveVideoRef.current.play().catch(() => {});
      }
      await syncZoomState(zoomTargetRef.current);
      setCameraActive(true);
      return streamRef.current;
    }

    return startCamera(facingModeRef.current, zoomTargetRef.current, qualityPreset);
  }, [startCamera, syncZoomState]);

  const refreshDevices = useCallback(async () => {
    try {
      const devices = await navigator.mediaDevices.enumerateDevices();
      setHasMultipleCameras(devices.filter((device) => device.kind === "videoinput").length > 1);
    } catch {
      setHasMultipleCameras(false);
    }
  }, []);

  const processUploadQueue = useCallback(async () => {
    if (uploadInFlightRef.current || pendingUploadsRef.current.length === 0 || !currentSessionRef.current?.id || !navigator.onLine) {
      syncQueuedUploadState();
      return;
    }

    uploadInFlightRef.current = true;
    try {
      while (pendingUploadsRef.current.length > 0 && currentSessionRef.current?.id && navigator.onLine) {
        const nextSegment = pendingUploadsRef.current[0];
        const response = await uploadMediaSegment(
          currentSessionRef.current.id,
          nextSegment.sequence,
          nextSegment.durationMs,
          nextSegment.blob
        );
        pendingUploadsRef.current.shift();
        syncQueuedUploadState();
        syncSessionState(response.session);
      }
    } catch (error: any) {
      toast.error(`分段上传失败: ${error?.message || "未知错误"}`);
    } finally {
      uploadInFlightRef.current = false;
    }
  }, [syncQueuedUploadState, syncSessionState]);

  const enqueueSegment = useCallback(async (blob: Blob, durationForSegmentMs: number) => {
    if (!blob.size) return;
    pendingUploadsRef.current.push({
      sequence: segmentSequenceRef.current++,
      durationMs: Math.max(1, durationForSegmentMs),
      blob,
    });
    syncQueuedUploadState();
    await processUploadQueue();
  }, [processUploadQueue, syncQueuedUploadState]);

  const flushPendingSegments = useCallback(async () => {
    while (pendingUploadsRef.current.length > 0 || uploadInFlightRef.current) {
      await processUploadQueue();
      await new Promise((resolve) => setTimeout(resolve, 200));
    }
  }, [processUploadQueue]);

  const stopRecorder = useCallback(async () => {
    stopTickers();
    const recorder = recorderRef.current;
    recorderRef.current = null;

    if (!recorder || recorder.state === "inactive") {
      return;
    }

    const stopped = new Promise<void>((resolve) => {
      recorder.addEventListener("stop", () => resolve(), { once: true });
    });

    try {
      recorder.requestData();
    } catch {
      // ignore
    }
    recorder.stop();
    await stopped;
  }, [stopTickers]);

  const createManualMarker = useCallback(async (
    type: "manual" | "motion" | "action_detected" | "invalid_auto" | "invalid_manual" | "valid_manual",
    label: string,
    confidence?: number,
  ) => {
    const sessionId = currentSessionRef.current?.id;
    if (!sessionId) return;

    const timestampMs = Math.max(0, Date.now() - recordingStartedAtRef.current);
    const optimisticMarker: MediaMarker = {
      id: `${type}-${timestampMs}`,
      type,
      label,
      timestampMs,
      confidence,
      createdAt: new Date().toISOString(),
    };

    setMarkers((current) => [...current, optimisticMarker].sort((a, b) => a.timestampMs - b.timestampMs));

    try {
      const response = await createMediaMarker(sessionId, {
        type,
        label,
        timestampMs,
        confidence,
      });
      syncSessionState(response.session);
    } catch (error: any) {
      toast.error(`标记失败: ${error?.message || "未知错误"}`);
    }
  }, [syncSessionState]);

  const setValidityState = useCallback((
    nextStatus: "pending" | "valid" | "invalid",
    reason: string,
    override: "valid" | "invalid" | null = validityOverrideRef.current,
  ) => {
    validityOverrideRef.current = override;
    setRecordingValidity(nextStatus);
    setRecordingValidityReason(reason);
  }, []);

  const startActionSampling = useCallback(async () => {
    if (typeof window === "undefined") return;
    const liveVideo = liveVideoRef.current;
    if (!liveVideo) return;

    if (!actionPoseRef.current) {
      const testFactory = (
        window as typeof window & {
          __TEST_MEDIAPIPE_FACTORY__?: () => Promise<{ Pose: any }>;
        }
      ).__TEST_MEDIAPIPE_FACTORY__;
      const { Pose } = testFactory ? await testFactory() : await import("@mediapipe/pose");
      const pose = new Pose({
        locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`,
      });
      pose.setOptions({
        modelComplexity: 0,
        smoothLandmarks: true,
        enableSegmentation: false,
        minDetectionConfidence: 0.5,
        minTrackingConfidence: 0.5,
      });
      pose.onResults((results: { poseLandmarks?: Array<{ x: number; y: number; visibility?: number }> }) => {
        if (!results.poseLandmarks) {
          return;
        }

        const analyzed = stabilizeActionFrame(
          recognizeActionFrame(results.poseLandmarks, actionTrackingRef.current, performance.now()),
          actionHistoryRef.current,
        );
        setCurrentDetectedAction(analyzed.action);

        if (analyzed.action === "unknown" || analyzed.confidence < 0.55) {
          return;
        }

        latestRecognizedActionRef.current = analyzed.action;
        lastRecognizedActionAtRef.current = Date.now();
        actionSummaryRef.current = {
          ...actionSummaryRef.current,
          [analyzed.action]: (actionSummaryRef.current[analyzed.action] || 0) + 1,
        };
        setActionSummary({ ...actionSummaryRef.current });

        if (validityOverrideRef.current !== "invalid") {
          setValidityState("valid", `已识别到 ${RECOGNIZED_ACTION_LABELS[analyzed.action]}`, validityOverrideRef.current);
        }

        if (Date.now() - lastActionMarkerAtRef.current >= 15_000) {
          lastActionMarkerAtRef.current = Date.now();
          void createManualMarker("action_detected", `识别到${RECOGNIZED_ACTION_LABELS[analyzed.action]}`, analyzed.confidence);
        }
      });
      actionPoseRef.current = pose;
    }

    const checkInvalidWindow = () => {
      const elapsedMs = Date.now() - recordingStartedAtRef.current;
      if (elapsedMs < INVALID_RECORDING_WINDOW_MS || invalidAutoMarkedRef.current) {
        return;
      }
      if (Date.now() - lastRecognizedActionAtRef.current < INVALID_RECORDING_WINDOW_MS) {
        return;
      }
      invalidAutoMarkedRef.current = true;
      setValidityState("invalid", "连续 60 秒未识别到有效动作，已自动标记为无效录制", validityOverrideRef.current);
      void createManualMarker("invalid_auto", "连续60秒未识别到有效动作，自动标记为无效录制");
    };

    actionTickerRef.current = setInterval(() => {
      const video = liveVideoRef.current;
      if (!video || video.readyState < 2 || !actionPoseRef.current || actionFrameInFlightRef.current) {
        checkInvalidWindow();
        return;
      }

      actionFrameInFlightRef.current = true;
      actionPoseRef.current.send({ image: video })
        .catch(() => undefined)
        .finally(() => {
          actionFrameInFlightRef.current = false;
          checkInvalidWindow();
        });
    }, ACTION_SAMPLE_MS);
  }, [createManualMarker, setValidityState]);

  const stopActionSampling = useCallback(async () => {
    if (actionTickerRef.current) {
      clearInterval(actionTickerRef.current);
      actionTickerRef.current = null;
    }
    if (actionPoseRef.current?.close) {
      try {
        await actionPoseRef.current.close();
      } catch {
        // ignore pose teardown failures during recorder stop/reset
      }
    }
    actionPoseRef.current = null;
    actionFrameInFlightRef.current = false;
  }, []);

  const updateRecordingValidity = useCallback(async (next: "valid" | "invalid") => {
    validityOverrideRef.current = next;
    if (next === "valid") {
      setValidityState("valid", "已手工恢复为有效录制", "valid");
      invalidAutoMarkedRef.current = false;
      await createManualMarker("valid_manual", "手工恢复为有效录制");
      return;
    }

    setValidityState("invalid", "已手工标记为无效录制", "invalid");
    await createManualMarker("invalid_manual", "手工标记为无效录制");
  }, [createManualMarker, setValidityState]);

  const sampleMotion = useCallback(() => {
    const video = liveVideoRef.current;
    const canvas = motionCanvasRef.current;
    if (!video || !canvas || video.readyState < 2) {
      return;
    }

    const ctx = canvas.getContext("2d", { willReadFrequently: true });
    if (!ctx) {
      return;
    }

    const width = 160;
    const height = 90;
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(video, 0, 0, width, height);
    const frame = ctx.getImageData(0, 0, width, height).data;
    const lastFrame = motionFrameRef.current;

    if (lastFrame) {
      let totalDifference = 0;
      let samples = 0;
      for (let index = 0; index < frame.length; index += 16) {
        totalDifference += Math.abs(frame[index] - lastFrame[index]);
        samples += 1;
      }
      const averageDifference = samples ? totalDifference / samples : 0;
      const now = Date.now();
      if (averageDifference >= MOTION_THRESHOLD && now - lastMotionMarkerAtRef.current >= MOTION_COOLDOWN_MS) {
        lastMotionMarkerAtRef.current = now;
        void createManualMarker("motion", "自动关键片段", Math.min(0.98, averageDifference / 100));
      }
    }

    motionFrameRef.current = new Uint8ClampedArray(frame);
  }, [createManualMarker]);

  const startRealtimePush = useCallback(async (stream: MediaStream, sessionId: string) => {
    closePeer();
    const peer = new RTCPeerConnection({
      iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
    });
    peerRef.current = peer;
    setConnectionState("connecting");

    stream.getTracks().forEach((track) => {
      peer.addTrack(track, stream);
    });

    peer.onconnectionstatechange = () => {
      setConnectionState(peer.connectionState);
      if (peer.connectionState === "disconnected" && (modeRef.current === "recording" || modeRef.current === "reconnecting")) {
        void attemptReconnect();
      }
    };

    const offer = await peer.createOffer();
    await peer.setLocalDescription(offer);
    await waitForIceGathering(peer);

    const answer = await signalMediaSession(sessionId, {
      sdp: peer.localDescription?.sdp || "",
      type: peer.localDescription?.type || "offer",
    });

    await peer.setRemoteDescription({
      type: answer.type as RTCSdpType,
      sdp: answer.sdp,
    });
  }, [closePeer]);

  const startRecorderLoop = useCallback((stream: MediaStream) => {
    const recorder = new MediaRecorder(stream, {
      mimeType,
      videoBitsPerSecond: pickBitrate(qualityPreset, mobile),
    });

    currentSegmentStartedAtRef.current = Date.now();

    recorder.ondataavailable = (event) => {
      if (!event.data.size) {
        return;
      }
      const now = Date.now();
      const segmentDuration = now - currentSegmentStartedAtRef.current;
      currentSegmentStartedAtRef.current = now;
      void enqueueSegment(event.data, segmentDuration);
    };

    recorder.onerror = () => {
      void attemptReconnect();
    };

    recorder.start();
    recorderRef.current = recorder;

    segmentTickerRef.current = setInterval(() => {
      if (recorder.state === "recording") {
        recorder.requestData();
      }
    }, SEGMENT_LENGTH_MS);

    timerTickerRef.current = setInterval(() => {
      setDurationMs(Date.now() - recordingStartedAtRef.current);
    }, 250);

    motionTickerRef.current = setInterval(() => {
      sampleMotion();
    }, MOTION_SAMPLE_MS);
  }, [enqueueSegment, mimeType, mobile, qualityPreset, sampleMotion]);

  const attemptReconnect = useCallback(async () => {
    if (!currentSessionRef.current || modeRef.current === "finalizing" || modeRef.current === "archived") {
      return;
    }

    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current);
    }

    setMode("reconnecting");
    setReconnectAttempts((count) => {
      const nextCount = count + 1;
      reconnectAttemptsRef.current = nextCount;
      return nextCount;
    });

    try {
      await stopRecorder();
    } catch {
      // ignore
    }
    closePeer();
    stopCamera();

    const nextDelay = Math.min(1_000 * Math.max(1, reconnectAttemptsRef.current), 8_000);
    reconnectTimeoutRef.current = setTimeout(async () => {
      try {
        const stream = await startCamera();
        await startRealtimePush(stream, currentSessionRef.current?.id || "");
        startRecorderLoop(stream);
        setMode("recording");
        toast.success("摄像头已重新连接，录制继续");
      } catch (error: any) {
        toast.error(`自动重连失败: ${error?.message || "未知错误"}`);
        if (modeRef.current === "reconnecting") {
          void attemptReconnect();
        }
      }
    }, nextDelay);
  }, [closePeer, startCamera, startRealtimePush, startRecorderLoop, stopCamera, stopRecorder]);

  const beginRecording = useCallback(async () => {
    if (!user) {
      toast.error("请先登录后再开始录制");
      return;
    }
    if (liveAnalysisOccupied) {
      const title = liveAnalysisRuntime?.title || "其他设备正在实时分析";
      toast.error(`${title}，当前设备不能同时开始录制`);
      return;
    }

    try {
      setMode("preparing");
      setMarkers([]);
      setDurationMs(0);
      setUploadedSegments(0);
      setUploadBytes(0);
      setQueuedSegments(0);
      setQueuedBytes(0);
      setReconnectAttempts(0);
      setArchiveTaskId(null);
      segmentSequenceRef.current = 0;
      motionFrameRef.current = null;
      pendingUploadsRef.current = [];
      actionTrackingRef.current = {};
      actionHistoryRef.current = [];
      actionSummaryRef.current = createActionSummary();
      setActionSummary(createActionSummary());
      setCurrentDetectedAction("unknown");
      setRecordingValidity("pending");
      setRecordingValidityReason("正在抽样动作帧，持续 60 秒未识别到有效动作将自动标记无效。");
      validityOverrideRef.current = null;
      invalidAutoMarkedRef.current = false;
      latestRecognizedActionRef.current = "unknown";
      lastActionMarkerAtRef.current = 0;

      const stream = await ensurePreviewStream();
      const sessionResponse = await createMediaSession({
        userId: String(user.id),
        title: title.trim() || `训练录制 ${formatDateTimeShanghai(new Date())}`,
        format: "webm",
        mimeType,
        qualityPreset,
        facingMode,
        deviceKind: mobile ? "mobile" : "desktop",
      });
      syncSessionState(sessionResponse.session);
      await startRealtimePush(stream, sessionResponse.session.id);

      recordingStartedAtRef.current = Date.now();
      lastRecognizedActionAtRef.current = recordingStartedAtRef.current;
      startRecorderLoop(stream);
      await startActionSampling();
      setMode("recording");
      toast.success("录制已开始，已同步启动实时推流");
    } catch (error: any) {
      setMode("idle");
      toast.error(`启动录制失败: ${error?.message || "未知错误"}`);
    }
  }, [
    ensurePreviewStream,
    facingMode,
    liveAnalysisOccupied,
    liveAnalysisRuntime?.title,
    mimeType,
    mobile,
    qualityPreset,
    startActionSampling,
    startRealtimePush,
    startRecorderLoop,
    syncSessionState,
    title,
    user,
  ]);

  const finishRecording = useCallback(async () => {
    const session = currentSessionRef.current;
    if (!session) {
      return;
    }

    try {
      setMode("finalizing");
      await stopActionSampling();
      await stopRecorder();
      await flushPendingSegments();
      closePeer();
      stopCamera();

      const response = await finalizeMediaSession(session.id, {
        title: title.trim() || session.title,
        durationMs: Date.now() - recordingStartedAtRef.current,
      });
      syncSessionState(response.session);
      await finalizeTaskMutation.mutateAsync({
        sessionId: session.id,
        title: title.trim() || session.title,
        exerciseType: "recording",
        sessionMode,
        durationMinutes: Math.max(1, Math.round((Date.now() - recordingStartedAtRef.current) / 60000)),
        actionCount: Object.entries(actionSummaryRef.current)
          .filter(([actionType]) => actionType !== "unknown")
          .reduce((sum, [, count]) => sum + count, 0),
        actionSummary: actionSummaryRef.current,
        dominantAction: latestRecognizedActionRef.current !== "unknown" ? latestRecognizedActionRef.current : undefined,
        validityStatus:
          recordingValidity === "invalid"
            ? validityOverrideRef.current === "invalid" ? "invalid_manual" : "invalid_auto"
            : recordingValidity === "valid"
              ? validityOverrideRef.current === "valid" ? "valid_manual" : "valid"
              : "pending",
        invalidReason: recordingValidity === "invalid" ? recordingValidityReason : undefined,
      });
      toast.success("录制已提交，后台正在整理回放文件");
    } catch (error: any) {
      toast.error(`结束录制失败: ${error?.message || "未知错误"}`);
      setMode("recording");
    }
  }, [closePeer, finalizeTaskMutation, flushPendingSegments, recordingValidity, recordingValidityReason, sessionMode, stopActionSampling, stopCamera, stopRecorder, syncSessionState, title]);

  const resetRecorder = useCallback(async () => {
    if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
    stopTickers();
    await stopRecorder().catch(() => {});
    closePeer();
    stopCamera();
    pendingUploadsRef.current = [];
    uploadInFlightRef.current = false;
    motionFrameRef.current = null;
    await stopActionSampling().catch(() => {});
    actionTrackingRef.current = {};
    actionHistoryRef.current = [];
    actionSummaryRef.current = createActionSummary();
    currentSessionRef.current = null;
    setArchiveTaskId(null);
    setMediaSession(null);
    setMarkers([]);
    setActionSummary(createActionSummary());
    setCurrentDetectedAction("unknown");
    setRecordingValidity("pending");
    setRecordingValidityReason("");
    setDurationMs(0);
    setQueuedSegments(0);
    setQueuedBytes(0);
    setUploadedSegments(0);
    setUploadBytes(0);
    setReconnectAttempts(0);
    setConnectionState("new");
    setCameraError("");
    setMode("idle");
  }, [closePeer, stopActionSampling, stopCamera, stopRecorder, stopTickers]);

  const flipCamera = useCallback(async () => {
    const nextFacingMode = facingMode === "user" ? "environment" : "user";
    setFacingMode(nextFacingMode);
    if (mode === "idle" && cameraActive) {
      stopCamera();
      await startCamera(nextFacingMode, zoomTargetRef.current, qualityPreset);
    }
  }, [cameraActive, facingMode, mode, qualityPreset, startCamera, stopCamera]);

  const handleQualityPresetChange = useCallback(async (nextPreset: keyof typeof QUALITY_PRESETS) => {
    setQualityPreset(nextPreset);
    if (cameraActive && mode === "idle") {
      await startCamera(facingModeRef.current, zoomTargetRef.current, nextPreset);
    }
  }, [cameraActive, mode, startCamera]);

  useEffect(() => {
    void refreshDevices();
    navigator.mediaDevices?.addEventListener?.("devicechange", refreshDevices);

    const handleOnline = () => {
      setIsOnline(true);
      toast.success("网络已恢复，继续上传缓存分段");
      void processUploadQueue();
    };
    const handleOffline = () => {
      setIsOnline(false);
      toast.warning("网络已断开，录制分段已留在本地队列");
    };

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      navigator.mediaDevices?.removeEventListener?.("devicechange", refreshDevices);
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, [processUploadQueue, refreshDevices]);

  useEffect(() => {
    if (!currentPlaybackUrl || !playbackVideoRef.current || mode !== "archived") {
      return;
    }
    playbackVideoRef.current.load();
  }, [currentPlaybackUrl, immersivePreview, mode]);

  useEffect(() => {
    if (mode === "archived") {
      return;
    }

    const liveVideo = liveVideoRef.current;
    if (!liveVideo || !streamRef.current) {
      return;
    }

    if (liveVideo.srcObject !== streamRef.current) {
      liveVideo.srcObject = streamRef.current;
    }
    liveVideo.play().catch(() => {});
  }, [cameraActive, immersivePreview, mode]);

  useEffect(() => {
    if (!mobile) {
      setImmersivePreview(false);
      return;
    }

    if (!immersivePreview) {
      document.body.style.overflow = "";
      return;
    }

    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = "";
    };
  }, [immersivePreview, mobile]);

  useEffect(() => {
    return () => {
      if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
      stopTickers();
      void stopActionSampling();
      if (recorderRef.current && recorderRef.current.state !== "inactive") {
        try {
          recorderRef.current.stop();
        } catch {
          // ignore
        }
      }
      closePeer();
      if (streamRef.current) {
        streamRef.current.getTracks().forEach((track) => track.stop());
      }
    };
  }, [closePeer, stopActionSampling, stopTickers]);

  const statusBadge = useMemo(() => {
    if (mode === "finalizing") {
      return { label: "归档处理中", tone: "default" as const, icon: CloudUpload };
    }
    if (mode === "reconnecting") {
      return { label: "摄像头重连中", tone: "secondary" as const, icon: ShieldAlert };
    }
    if (mode === "recording") {
      return { label: "录制中", tone: "destructive" as const, icon: Circle };
    }
    if (mode === "archived") {
      return { label: "回放已就绪", tone: "default" as const, icon: Video };
    }
    return { label: "待机", tone: "outline" as const, icon: Camera };
  }, [mode]);

  const previewTitle = mode === "archived" ? "归档回放" : "实时取景";
  const StatusIcon = statusBadge.icon;
  const previewVideoClassName = immersivePreview && mobile
    ? "h-full w-full object-contain"
    : "h-full w-full object-contain";
  const railButtonClassName =
    "h-16 w-16 flex-col gap-1 rounded-[20px] px-2 text-[11px] leading-tight shadow-lg shadow-black/20";

  const renderPrimaryActions = (layout: "toolbar" | "rail" = "toolbar") => {
    const rail = layout === "rail";
    const labelFor = (full: string, short: string) => (rail ? short : full);
    const buttonClass = (tone: "default" | "outline" | "destructive" | "record" = "default") => {
      if (!rail) {
        switch (tone) {
          case "outline":
            return "h-12 rounded-2xl px-4";
          case "destructive":
            return "h-12 rounded-2xl px-5";
          case "record":
            return "h-12 rounded-2xl bg-red-600 px-5 hover:bg-red-700";
          default:
            return "h-12 rounded-2xl px-5";
        }
      }

      switch (tone) {
        case "outline":
          return `${railButtonClassName} border-white/15 bg-white/8 text-white hover:bg-white/14`;
        case "destructive":
          return `${railButtonClassName} bg-red-600 text-white hover:bg-red-700`;
        case "record":
          return `${railButtonClassName} bg-red-600 text-white hover:bg-red-700`;
        default:
          return `${railButtonClassName} bg-white text-slate-950 hover:bg-white/90`;
      }
    };

    const iconClass = rail ? "h-5 w-5" : "mr-2 h-4 w-4";

    return (
      <>
        {mode === "idle" && (
          <>
            {!cameraActive ? (
              <Button
                data-testid="recorder-start-camera-button"
                onClick={() => void startCamera()}
                className={buttonClass()}
                disabled={liveAnalysisOccupied}
              >
                <Camera className={iconClass} />
                {labelFor(liveAnalysisOccupied ? "实时分析占用中" : "启动摄像头", liveAnalysisOccupied ? "占用" : "启动")}
              </Button>
            ) : (
              <>
                <Button
                  data-testid="recorder-start-recording-button"
                  onClick={() => void beginRecording()}
                  className={buttonClass("record")}
                  disabled={liveAnalysisOccupied}
                >
                  <Circle className={`${iconClass} ${rail ? "fill-current" : "fill-current"}`} />
                  {labelFor(liveAnalysisOccupied ? "实时分析占用中" : "开始录制", liveAnalysisOccupied ? "占用" : "录制")}
                </Button>
                <Button variant="outline" onClick={stopCamera} className={buttonClass("outline")}>
                  <VideoOff className={iconClass} />
                  {labelFor("关闭摄像头", "关闭")}
                </Button>
              </>
            )}
            {hasMultipleCameras && (
              <Button variant="outline" onClick={() => void flipCamera()} className={buttonClass("outline")}>
                <FlipHorizontal className={iconClass} />
                {labelFor("切换摄像头", "切换")}
              </Button>
            )}
          </>
        )}

        {(mode === "recording" || mode === "reconnecting") && (
          <>
            <Button
              data-testid="recorder-marker-button"
              variant="outline"
              onClick={() => void createManualMarker("manual", "手动剪辑点")}
              className={buttonClass("outline")}
            >
              <Scissors className={iconClass} />
              {labelFor("标记剪辑点", "标记")}
            </Button>
            <Button
              data-testid="recorder-finish-button"
              variant="destructive"
              onClick={() => void finishRecording()}
              className={buttonClass("destructive")}
            >
              <CloudUpload className={iconClass} />
              {labelFor("结束并归档", "完成")}
            </Button>
          </>
        )}

        {mode === "archived" && (
          <>
            {currentPlaybackUrl && (
              <Button asChild variant="outline" className={buttonClass("outline")}>
                <a href={currentPlaybackUrl} download target="_blank" rel="noreferrer">
                  <Download className={iconClass} />
                  {labelFor("下载回放", "下载")}
                </a>
              </Button>
            )}
            <Button
              data-testid="recorder-reset-button"
              onClick={() => void resetRecorder()}
              className={buttonClass()}
            >
              {rail ? null : null}
              {labelFor("重新录制", "重来")}
            </Button>
          </>
        )}

        {mode === "finalizing" && (
          <Button disabled className={rail ? `${railButtonClassName} bg-white/15 text-white` : "h-12 rounded-2xl px-5"}>
            <Loader2 className={rail ? "h-5 w-5 animate-spin" : "mr-2 h-4 w-4 animate-spin"} />
            {labelFor("正在生成回放", "处理中")}
          </Button>
        )}
      </>
    );
  };

  const renderPreviewMedia = () => (
    <>
      {mode === "archived" && currentPlaybackUrl ? (
        <video
          ref={playbackVideoRef}
          className={previewVideoClassName}
          src={currentPlaybackUrl}
          controls
          playsInline
        />
      ) : (
        <video
          ref={liveVideoRef}
          className={previewVideoClassName}
          playsInline
          muted
          autoPlay
        />
      )}
    </>
  );

  const renderZoomOverlay = () => (
    <div className="absolute right-3 bottom-3 z-20 flex items-center gap-2 rounded-2xl border border-white/10 bg-black/65 px-2 py-2 text-white shadow-lg">
      <Button
        type="button"
        size="icon"
        variant="secondary"
        onClick={() => stepZoom(-1)}
        disabled={!zoomState.supported}
        className="h-10 w-10 rounded-xl border border-white/10 bg-white/10 text-white hover:bg-white/20 disabled:opacity-40"
      >
        <Minus className="h-4 w-4" />
      </Button>
      <div className="min-w-[78px] text-center">
        <div className="text-[10px] uppercase tracking-[0.16em] text-white/50">焦距</div>
        <div className="mt-1 text-sm font-semibold">{zoomState.supported ? `${zoomState.current.toFixed(1)}x` : "自动"}</div>
      </div>
      <Button
        type="button"
        size="icon"
        variant="secondary"
        onClick={() => stepZoom(1)}
        disabled={!zoomState.supported}
        className="h-10 w-10 rounded-xl border border-white/10 bg-white/10 text-white hover:bg-white/20 disabled:opacity-40"
      >
        <Plus className="h-4 w-4" />
      </Button>
    </div>
  );

  return (
    <div className="no-overscroll mobile-safe-bottom mobile-safe-inline mobile-bottom-spacing space-y-4">
      <canvas ref={motionCanvasRef} className="hidden" />

      <section className="rounded-[28px] border border-border/60 bg-[radial-gradient(circle_at_top_left,_rgba(34,197,94,0.18),_transparent_35%),linear-gradient(135deg,rgba(12,18,24,0.98),rgba(18,38,30,0.95))] p-5 text-white shadow-xl shadow-black/10 md:p-7">
        <div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
          <div className="space-y-3">
            <div className="flex flex-wrap items-center gap-2">
              <Badge variant={statusBadge.tone} className="gap-1.5 border-white/10 bg-white/10 text-white hover:bg-white/10">
                <StatusIcon className={`h-3.5 w-3.5 ${mode === "recording" ? "fill-current" : ""}`} />
                {statusBadge.label}
              </Badge>
              <Badge variant="outline" className="gap-1.5 border-white/15 bg-white/5 text-white/80">
                {isOnline ? <Wifi className="h-3.5 w-3.5" /> : <WifiOff className="h-3.5 w-3.5" />}
                {isOnline ? "网络在线" : "离线缓存中"}
              </Badge>
              <Badge variant="outline" className="gap-1.5 border-white/15 bg-white/5 text-white/80">
                <MonitorUp className="h-3.5 w-3.5" />
                WebRTC 推流
              </Badge>
              <Badge variant="outline" className="gap-1.5 border-white/15 bg-white/5 text-white/80">
                <Sparkles className="h-3.5 w-3.5" />
                {sessionMode === "practice" ? "练习会话" : "训练 PK"}
              </Badge>
            </div>

            <div>
              <h1 className="text-3xl font-semibold tracking-tight" data-testid="recorder-title">在线录制控制台</h1>
              <p className="mt-2 max-w-2xl text-sm leading-6 text-white/70">
                录制采用 Chrome 优先的 WebM 压缩流，60 秒自动分段上传，实时推流与本地压缩并行运行。弱网和摄像头中断时会保留已录片段并尝试自动恢复。
              </p>
            </div>
          </div>

          <div className="grid grid-cols-3 gap-2 self-stretch rounded-2xl border border-white/10 bg-white/5 p-2 text-center text-xs text-white/70 sm:w-[360px]">
            <div className="rounded-xl bg-black/15 px-3 py-3">
              <div className="text-[11px] uppercase tracking-[0.18em] text-white/45">录制时长</div>
              <div className="mt-2 text-lg font-semibold text-white">{formatRecordingTime(durationMs)}</div>
            </div>
            <div className="rounded-xl bg-black/15 px-3 py-3">
              <div className="text-[11px] uppercase tracking-[0.18em] text-white/45">已传片段</div>
              <div className="mt-2 text-lg font-semibold text-white">{uploadedSegments}</div>
            </div>
            <div className="rounded-xl bg-black/15 px-3 py-3">
              <div className="text-[11px] uppercase tracking-[0.18em] text-white/45">已传体积</div>
              <div className="mt-2 text-lg font-semibold text-white">{formatFileSize(uploadBytes)}</div>
            </div>
          </div>
        </div>
      </section>

      {(finalizeTaskMutation.isPending || archiveTaskQuery.data?.status === "queued" || archiveTaskQuery.data?.status === "running") ? (
        <Alert>
          <ListTodo className="h-4 w-4" />
          <AlertTitle>后台归档处理中</AlertTitle>
          <AlertDescription>
            {archiveTaskQuery.data?.message || "录制文件正在后台整理、转码并登记到视频库。"}
            当前已上传 {formatFileSize(uploadBytes)}
            {queuedBytes > 0 ? `，待上传 ${formatFileSize(queuedBytes)}` : ""}。
            你可以离开当前页面，完成后任务中心会提示结果。
          </AlertDescription>
        </Alert>
      ) : null}

      {uploadStillDraining ? (
        <Alert variant="destructive">
          <ShieldAlert className="h-4 w-4" />
          <AlertTitle>仍需停留在当前页面</AlertTitle>
          <AlertDescription>
            结束已提交，但还有 {queuedSegments} 个分段、约 {formatFileSize(queuedBytes)} 待上传。
            请先保持当前页面和网络连接，等“待上传缓存”变为 0 后再离开，否则本次录制尾段可能缺失。
          </AlertDescription>
        </Alert>
      ) : null}

      {canLeaveRecorderPage ? (
        <Alert>
          <Video className="h-4 w-4" />
          <AlertTitle>上传已完成，无需继续停留在此页面</AlertTitle>
          <AlertDescription>
            当前录制分段已经全部上传。
            {mode === "archived"
              ? " 回放文件已生成，你现在可以返回其他页面，稍后也能在视频库继续查看。"
              : " 后台会继续合并分段、生成回放并写入视频库，你可以直接离开，任务中心会反馈完成结果。"}
          </AlertDescription>
        </Alert>
      ) : null}

      {liveAnalysisOccupied ? (
        <Alert className="border-amber-300/70 bg-amber-50 text-amber-950">
          <ShieldAlert className="h-4 w-4" />
          <AlertTitle>当前账号已有其他设备正在实时分析</AlertTitle>
          <AlertDescription>
            {liveAnalysisRuntime?.title || "其他设备正在实时分析"}，本页已禁止再次启动摄像头和录制，避免同账号多端同时占用镜头。
            你可以前往
            {" "}
            <a href="/live-camera" className="font-medium underline underline-offset-4">
              实时分析页
            </a>
            {" "}
            查看同步画面与动作识别结果。
          </AlertDescription>
        </Alert>
      ) : null}

      <div className="grid gap-4 xl:grid-cols-[minmax(0,1.7fr)_minmax(340px,0.9fr)]">
        <section className="space-y-4">
          <Card className="overflow-hidden border-0 shadow-lg">
            <CardContent className="p-0">
              <div className="relative aspect-[16/10] overflow-hidden bg-black sm:aspect-video">
                {!immersivePreview && renderPreviewMedia()}

                {!cameraActive && mode === "idle" && (
                  <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(circle_at_center,_rgba(16,185,129,0.12),_rgba(0,0,0,0.75))] px-6 text-center text-white/80">
                    <VideoOff className="h-14 w-14" />
                    <div className="space-y-1">
                      <h2 className="text-xl font-medium">准备开始训练录制</h2>
                      <p className="text-sm text-white/60">启动摄像头后会同时准备本地压缩录制和实时推流链路。</p>
                    </div>
                  </div>
                )}

                <div className="pointer-events-none absolute left-3 top-3 flex flex-wrap gap-2">
                  <Badge className="gap-1.5 bg-black/55 text-white shadow-sm">
                    <Sparkles className="h-3.5 w-3.5" />
                    {previewTitle}
                  </Badge>
                  <Badge className="gap-1.5 bg-black/55 text-white shadow-sm">
                    <Zap className="h-3.5 w-3.5" />
                    {QUALITY_PRESETS[qualityPreset].subtitle}
                  </Badge>
                  {mobile && (
                    <Badge className="gap-1.5 bg-black/55 text-white shadow-sm">
                      <Smartphone className="h-3.5 w-3.5" />
                      移动端优化
                    </Badge>
                  )}
                </div>

                {(mode === "recording" || mode === "reconnecting") && (
                  <div className="absolute bottom-3 left-3 flex items-center gap-2 rounded-full bg-black/65 px-3 py-2 text-sm text-white shadow-lg">
                    <span className="relative flex h-3 w-3">
                      <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
                      <span className="relative inline-flex h-3 w-3 rounded-full bg-red-500" />
                    </span>
                    {formatRecordingTime(durationMs)}
                  </div>
                )}

                {mobile && (
                  <Button
                    type="button"
                    size="icon"
                    variant="secondary"
                    data-testid="recorder-mobile-focus-button"
                    onClick={() => setImmersivePreview(true)}
                    className="absolute right-3 top-3 z-20 h-11 w-11 rounded-full border border-white/10 bg-black/60 text-white shadow-lg hover:bg-black/75"
                  >
                    <Maximize2 className="h-4 w-4" />
                  </Button>
                )}

                {cameraActive && zoomState.supported ? renderZoomOverlay() : null}
              </div>

              <div className="border-t border-border/60 bg-card/80 p-4">
                <div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_180px_auto]">
                  <Input
                    value={title}
                    onChange={(event) => setTitle(event.target.value)}
                    placeholder="本次训练录制标题"
                    className="h-12 rounded-2xl border-border/60"
                  />
                  <Select value={sessionMode} onValueChange={(value) => setSessionMode(value as "practice" | "pk")} disabled={mode !== "idle" && mode !== "archived"}>
                    <SelectTrigger className="h-12 rounded-2xl border-border/60">
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      <SelectItem value="practice">练习会话</SelectItem>
                      <SelectItem value="pk">训练 PK</SelectItem>
                    </SelectContent>
                  </Select>
                  <div className="flex flex-wrap gap-2">
                    {renderPrimaryActions()}
                  </div>
                </div>
              </div>
            </CardContent>
          </Card>

          <Card className="border-0 shadow-sm">
            <CardHeader className="pb-3">
              <CardTitle className="text-base">质量与设备设置</CardTitle>
            </CardHeader>
            <CardContent className="space-y-3">
              <div className="grid gap-3 lg:grid-cols-3">
                    {Object.entries(QUALITY_PRESETS).map(([key, preset]) => {
                      const active = qualityPreset === key;
                      const disabled = mode !== "idle" && mode !== "archived";
                      return (
                        <button
                          key={key}
                          type="button"
                          onClick={() => void handleQualityPresetChange(key as keyof typeof QUALITY_PRESETS)}
                          disabled={disabled}
                          className={`rounded-2xl border px-4 py-4 text-left transition ${
                            active
                              ? "border-primary/60 bg-primary/5 shadow-sm"
                              : "border-border/60 hover:border-primary/30 hover:bg-muted/50"
                          } ${disabled ? "cursor-not-allowed opacity-60" : ""}`}
                        >
                      <div className="text-sm font-semibold">{preset.label}</div>
                      <div className="mt-1 text-xs text-muted-foreground">{preset.subtitle}</div>
                      <p className="mt-3 text-sm leading-6 text-muted-foreground">{preset.description}</p>
                    </button>
                  );
                })}
              </div>

              <div className="grid gap-3 md:grid-cols-2">
                <div className="rounded-2xl border border-border/60 bg-muted/30 p-4">
                  <div className="text-sm font-medium">设备方向</div>
                  <div className="mt-2 text-sm text-muted-foreground">
                    当前优先使用 {facingMode === "environment" ? "后置摄像头" : "前置摄像头"}，横屏时自动保留更大预览区域。
                  </div>
                </div>
                <div className="rounded-2xl border border-border/60 bg-muted/30 p-4">
                  <div className="text-sm font-medium">录制容器</div>
                  <div className="mt-2 text-sm text-muted-foreground">
                    {mimeType} · 每 {SEGMENT_LENGTH_MS / 1000} 秒自动分段上传，服务端归档后提供 WebM/MP4 回放。
                  </div>
                </div>
              </div>

              <div className="rounded-2xl border border-border/60 bg-muted/25 p-4">
                <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
                  <div>
                    <div className="text-sm font-medium">镜头焦距 / 放大缩小</div>
                    <div className="mt-2 text-sm text-muted-foreground">
                      {zoomState.supported
                        ? `当前 ${zoomState.current.toFixed(1)}x，可在录制过程中直接调整；设备焦点模式为 ${zoomState.focusMode}。`
                        : "当前设备或浏览器未开放镜头缩放能力，仍会保持自动对焦。Chrome 安卓和部分后置摄像头通常支持此能力。"}
                    </div>
                  </div>
                  <Badge variant="outline" className="w-fit">
                    默认 {QUALITY_PRESETS.economy.label}
                  </Badge>
                </div>

                {zoomState.supported ? (
                  <div className="mt-4 space-y-3">
                    <Slider
                      value={[zoomState.current]}
                      min={zoomState.min}
                      max={zoomState.max}
                      step={zoomState.step}
                      onValueChange={(value) => {
                        if (typeof value[0] === "number") {
                          void updateZoom(value[0]);
                        }
                      }}
                    />
                    <div className="flex items-center justify-between text-xs text-muted-foreground">
                      <span>{zoomState.min.toFixed(1)}x</span>
                      <span>默认建议 1.0x-1.5x，用于半场动作取景</span>
                      <span>{zoomState.max.toFixed(1)}x</span>
                    </div>
                  </div>
                ) : null}
              </div>
            </CardContent>
          </Card>
        </section>

        <aside className="space-y-4">
          <Card className="border-0 shadow-sm">
            <CardHeader className="pb-3">
              <CardTitle className="text-base">会话状态</CardTitle>
            </CardHeader>
            <CardContent className="space-y-4">
              <div className="grid grid-cols-2 gap-3">
                <div className="rounded-2xl bg-muted/35 p-4">
                  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">推流连接</div>
                  <div className="mt-2 flex items-center gap-2 text-sm font-medium">
                    <Activity className="h-4 w-4 text-primary" />
                    {connectionState}
                  </div>
                </div>
                <div className="rounded-2xl bg-muted/35 p-4">
                  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">重连次数</div>
                  <div className="mt-2 text-sm font-medium">{reconnectAttempts}</div>
                </div>
              </div>

              <div className="grid grid-cols-2 gap-3">
                <div className="rounded-2xl bg-muted/35 p-4">
                  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">动作有效性</div>
                  <div className="mt-2 text-sm font-medium">
                    {recordingValidity === "valid" ? "有效录制" : recordingValidity === "invalid" ? "无效录制" : "待判定"}
                  </div>
                  <div className="mt-2 text-xs leading-5 text-muted-foreground">
                    {recordingValidityReason || "录制中会自动抽样动作帧并进行判定。"}
                  </div>
                </div>
                <div className="rounded-2xl bg-muted/35 p-4">
                  <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">当前识别动作</div>
                  <div className="mt-2 text-sm font-medium">
                    {RECOGNIZED_ACTION_LABELS[currentDetectedAction] || "未知动作"}
                  </div>
                  <div className="mt-2 text-xs leading-5 text-muted-foreground">
                    每 {Math.round(ACTION_SAMPLE_MS / 1000)} 秒抽样动作帧；连续 {Math.round(INVALID_RECORDING_WINDOW_MS / 1000)} 秒无有效动作会自动标记无效。
                  </div>
                </div>
              </div>

              <div className="flex flex-wrap gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => void updateRecordingValidity("valid")}
                  disabled={!currentSessionRef.current}
                >
                  手工恢复有效
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => void updateRecordingValidity("invalid")}
                  disabled={!currentSessionRef.current}
                >
                  手工标记无效
                </Button>
                {mediaSession?.playback.previewUrl ? (
                  <Button asChild variant="outline" size="sm">
                    <a href={mediaSession.playback.previewUrl} target="_blank" rel="noreferrer">
                      查看滚动预归档
                    </a>
                  </Button>
                ) : null}
              </div>

              <div className="space-y-2">
                <div className="flex items-center justify-between text-sm">
                  <span>已上传文件</span>
                  <span className="font-medium">{formatFileSize(uploadBytes)}</span>
                </div>
                <div className="flex items-center justify-between text-sm">
                  <span>待上传缓存</span>
                  <span className="font-medium">{queuedSegments} 段 · {formatFileSize(queuedBytes)}</span>
                </div>
                <div className="flex items-center justify-between text-sm">
                  <span>录制源文件累计</span>
                  <span className="font-medium">{formatFileSize(totalUploadBytes)}</span>
                </div>
                <div className="flex items-center justify-between text-sm">
                  <span>总片段数</span>
                  <span className="font-medium">{uploadedSegments + queuedSegments}</span>
                </div>
                <div className="flex items-center justify-between text-sm">
                  <span>服务端状态</span>
                  <span className="font-medium">{mediaSession?.status || "idle"}</span>
                </div>
                <div className="flex items-center justify-between text-sm">
                  <span>滚动预归档</span>
                  <span className="font-medium">
                    {mediaSession?.previewStatus || "idle"}
                    {typeof mediaSession?.previewSegments === "number" ? ` · ${mediaSession.previewSegments} 段` : ""}
                  </span>
                </div>
              </div>

              {(mode === "finalizing" || mode === "archived" || mediaSession?.archiveStatus === "failed") && (
                <div className="space-y-2 rounded-2xl border border-border/60 bg-muted/25 p-4">
                  <div className="flex items-center justify-between text-sm">
                    <span>{archivePhaseLabel}</span>
                    <span className="font-medium">{archiveProgress}%</span>
                  </div>
                  <Progress value={archiveProgress} className="h-2" />
                  <div className="grid gap-2 rounded-2xl bg-background/70 p-3 text-xs text-muted-foreground sm:grid-cols-3">
                    <div>
                      <div className="text-[10px] uppercase tracking-[0.14em]">已上传</div>
                      <div className="mt-1 text-sm font-medium text-foreground">{formatFileSize(uploadBytes)}</div>
                    </div>
                    <div>
                      <div className="text-[10px] uppercase tracking-[0.14em]">待上传</div>
                      <div className="mt-1 text-sm font-medium text-foreground">{formatFileSize(queuedBytes)}</div>
                    </div>
                    <div>
                      <div className="text-[10px] uppercase tracking-[0.14em]">片段总数</div>
                      <div className="mt-1 text-sm font-medium text-foreground">{uploadedSegments + queuedSegments} 段</div>
                    </div>
                  </div>
                  <p className="text-xs leading-5 text-muted-foreground">
                    {archiveTaskQuery.data?.message
                      ? `${archiveTaskQuery.data.message}，当前已上传 ${formatFileSize(uploadBytes)}。`
                      : mediaSession?.archiveStatus === "completed"
                      ? "归档完成，已生成可回放文件并同步到视频库。"
                      : mediaSession?.archiveStatus === "failed"
                        ? mediaSession.lastError || "归档失败，请检查媒体服务日志。"
                        : `Worker 正在合并分段并生成归档文件，当前已上传 ${formatFileSize(uploadBytes)}。`}
                  </p>
                  <div
                    className={`rounded-2xl px-3 py-2 text-xs leading-5 ${
                      uploadStillDraining
                        ? "border border-destructive/25 bg-destructive/5 text-destructive"
                        : canLeaveRecorderPage
                          ? "border border-emerald-200 bg-emerald-50 text-emerald-700"
                          : "border border-border/60 bg-background/70 text-muted-foreground"
                    }`}
                  >
                    {uploadStillDraining
                      ? `仍需停留：还有 ${queuedSegments} 个分段、约 ${formatFileSize(queuedBytes)} 待上传，待上传清零后再离开当前页面。`
                      : canLeaveRecorderPage
                        ? "上传已完成：无需继续停留在此页面，后台会继续生成回放并把结果写入视频库。"
                        : "结束录制后会先清空本地待上传缓存，再转入后台归档。"}
                  </div>
                </div>
              )}

              {recognizedActionItems.length > 0 ? (
                <div className="rounded-2xl border border-border/60 bg-muted/25 p-4">
                  <div className="text-sm font-medium">识别到的动作数据</div>
                  <div className="mt-3 flex flex-wrap gap-2">
                    {recognizedActionItems.map((item) => (
                      <Badge key={item.actionType} variant="secondary">
                        {item.label} {item.count} 次
                      </Badge>
                    ))}
                  </div>
                </div>
              ) : null}

              {cameraError && (
                <div className="rounded-2xl border border-destructive/20 bg-destructive/5 p-4 text-sm text-destructive">
                  {cameraError}
                </div>
              )}
            </CardContent>
          </Card>

          <Card className="border-0 shadow-sm">
            <CardHeader className="pb-3">
              <CardTitle className="text-base">关键片段标记</CardTitle>
            </CardHeader>
            <CardContent className="space-y-3">
              <p className="text-sm leading-6 text-muted-foreground">
                运动检测每隔约 {MOTION_SAMPLE_MS / 1000} 秒抽样一次，触发明显动作变化时自动创建关键片段标记。你也可以在录制过程中手动插入剪辑点。
              </p>

              <div className="space-y-2">
                {markers.length === 0 ? (
                  <div className="rounded-2xl border border-dashed border-border/70 px-4 py-6 text-center text-sm text-muted-foreground">
                    尚无标记点。录制开始后，系统会自动识别动作变化并写入关键片段。
                  </div>
                ) : (
                  markers.map((marker) => (
                    <div
                      key={marker.id}
                      className={`rounded-2xl border px-4 py-3 ${
                        marker.type === "motion"
                          ? "border-primary/25 bg-primary/5"
                          : "border-border/60 bg-muted/35"
                      }`}
                    >
                      <div className="flex items-center justify-between gap-3">
                        <div>
                          <div className="text-sm font-medium">{marker.label}</div>
                          <div className="mt-1 text-xs text-muted-foreground">
                            {marker.type === "motion" ? "自动标记" : "手动标记"} · {formatRecordingTime(marker.timestampMs)}
                          </div>
                        </div>
                        {typeof marker.confidence === "number" && (
                          <Badge variant="outline" className="rounded-full">
                            {Math.round(marker.confidence * 100)}%
                          </Badge>
                        )}
                      </div>
                    </div>
                  ))
                )}
              </div>
            </CardContent>
          </Card>
        </aside>
      </div>

      {mobile && immersivePreview && (
        <div
          className="mobile-safe-top mobile-safe-bottom mobile-safe-inline fixed inset-0 z-[80] bg-black/95 px-3 py-4"
          data-testid="recorder-mobile-focus-shell"
        >
          <div className="grid h-full grid-cols-[minmax(0,1fr)_72px] gap-3">
            <div className="relative flex min-h-0 items-center justify-center">
              <div className="relative h-full w-full overflow-hidden rounded-[32px] border border-white/10 bg-black shadow-2xl shadow-black/40">
                {renderPreviewMedia()}

                {!cameraActive && mode === "idle" && (
                  <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(circle_at_center,_rgba(16,185,129,0.12),_rgba(0,0,0,0.8))] px-6 text-center text-white/80">
                    <VideoOff className="h-14 w-14" />
                    <div className="space-y-1">
                      <h2 className="text-xl font-medium">竖屏专注录制</h2>
                      <p className="text-sm text-white/60">右侧按钮可直接启动摄像头、录制、标记和结束归档。</p>
                    </div>
                  </div>
                )}

                <div className="pointer-events-none absolute left-3 top-3 flex flex-wrap gap-2">
                  <Badge className="gap-1.5 bg-black/55 text-white shadow-sm">
                    <Sparkles className="h-3.5 w-3.5" />
                    {previewTitle}
                  </Badge>
                  <Badge className="gap-1.5 bg-black/55 text-white shadow-sm">
                    <Zap className="h-3.5 w-3.5" />
                    {QUALITY_PRESETS[qualityPreset].subtitle}
                  </Badge>
                </div>

                {(mode === "recording" || mode === "reconnecting") && (
                  <div className="absolute bottom-3 left-3 flex items-center gap-2 rounded-full bg-black/65 px-3 py-2 text-sm text-white shadow-lg">
                    <span className="relative flex h-3 w-3">
                      <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
                      <span className="relative inline-flex h-3 w-3 rounded-full bg-red-500" />
                    </span>
                    {formatRecordingTime(durationMs)}
                  </div>
                )}

                <Button
                  type="button"
                  size="icon"
                  variant="secondary"
                  onClick={() => setImmersivePreview(false)}
                  className="absolute right-3 top-3 z-20 h-11 w-11 rounded-full border border-white/10 bg-black/60 text-white shadow-lg hover:bg-black/75"
                >
                  <Minimize2 className="h-4 w-4" />
                </Button>

                {cameraActive && zoomState.supported ? renderZoomOverlay() : null}
              </div>
            </div>

            <div className="flex flex-col items-center justify-center gap-3">
              {renderPrimaryActions("rail")}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
