import { useEffect, useMemo, useState } from "react";
import { useAuth } from "@/_core/hooks/useAuth";
import { trpc } from "@/lib/trpc";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useBackgroundTask } from "@/hooks/useBackgroundTask";
import { formatDateTimeShanghai } from "@/lib/time";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import {
  Activity,
  ArrowRight,
  ChevronLeft,
  ChevronRight,
  CheckCircle2,
  Clock,
  Dumbbell,
  ExternalLink,
  Flame,
  Footprints,
  Hand,
  ListTodo,
  Loader2,
  RefreshCw,
  Ruler,
  Scale,
  Sparkles,
  Target,
  TestTube2,
  Trophy,
} from "lucide-react";

const SCORE_OPTIONS = [
  { value: "1", label: "1 分", description: "明显偏弱" },
  { value: "2", label: "2 分", description: "偏弱" },
  { value: "3", label: "3 分", description: "基础达标" },
  { value: "4", label: "4 分", description: "较强" },
  { value: "5", label: "5 分", description: "优势项" },
];

const NTRP_OPTIONS = ["1.0", "1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5", "5.0"];

const REQUIRED_PROFILE_KEYS = [
  "heightCm",
  "weightKg",
  "sprintSpeedScore",
  "explosivePowerScore",
  "agilityScore",
  "enduranceScore",
  "flexibilityScore",
  "coreStabilityScore",
  "shoulderMobilityScore",
  "hipMobilityScore",
] as const;

type AssessmentScoreGuide = {
  score: "1" | "2" | "3" | "4" | "5";
  range: string;
  note: string;
};

type AssessmentFieldConfig = {
  key: keyof Pick<
    TrainingProfileDraft,
    | "sprintSpeedScore"
    | "explosivePowerScore"
    | "agilityScore"
    | "enduranceScore"
    | "flexibilityScore"
    | "coreStabilityScore"
    | "shoulderMobilityScore"
    | "hipMobilityScore"
  >;
  label: string;
  method: string;
  hint: string;
  benchmarkLabel: string;
  benchmarkNote: string;
  scoreGuide: AssessmentScoreGuide[];
};

const ASSESSMENT_FIELDS = [
  {
    key: "sprintSpeedScore",
    label: "速度",
    method: "20m 或 30m 冲刺，记录自己的最佳一次。",
    hint: "根据实际表现按 1-5 分录入；1=启动慢，5=启动速度明显有优势。",
    benchmarkLabel: "20 米冲刺参考",
    benchmarkNote: "适用于成人业余训练的自测换算，秒数越低越好。",
    scoreGuide: [
      { score: "5", range: "≤ 3.35 秒", note: "启动和加速明显占优" },
      { score: "4", range: "3.36 - 3.60 秒", note: "速度较好，能较快抢位" },
      { score: "3", range: "3.61 - 3.95 秒", note: "基础达标，比赛可用" },
      { score: "2", range: "3.96 - 4.25 秒", note: "偏弱，第一步需要加强" },
      { score: "1", range: "> 4.25 秒", note: "启动明显慢，建议重点补强" },
    ],
  },
  {
    key: "explosivePowerScore",
    label: "爆发力",
    method: "可参考立定跳远、纵跳或第一步启动爆发感受。",
    hint: "重点看蹬地发力和短时间输出能力。",
    benchmarkLabel: "立定跳远参考",
    benchmarkNote: "建议穿平底鞋测试两次取最好成绩，距离越长越好。",
    scoreGuide: [
      { score: "5", range: "≥ 220 cm", note: "爆发输出强，启动质量高" },
      { score: "4", range: "200 - 219 cm", note: "爆发力较好" },
      { score: "3", range: "180 - 199 cm", note: "基础达标" },
      { score: "2", range: "160 - 179 cm", note: "偏弱，需要加强下肢爆发" },
      { score: "1", range: "< 160 cm", note: "爆发力明显不足" },
    ],
  },
  {
    key: "agilityScore",
    label: "敏捷性",
    method: "做折返跑、T-test 或小碎步变向练习后评分。",
    hint: "重点看变向反应和急停再启动能力。",
    benchmarkLabel: "T-Test 参考",
    benchmarkNote: "越快越好，测试时注意侧滑步和转身控制。",
    scoreGuide: [
      { score: "5", range: "≤ 10.2 秒", note: "变向和回位非常积极" },
      { score: "4", range: "10.21 - 11.0 秒", note: "敏捷性较好" },
      { score: "3", range: "11.01 - 12.0 秒", note: "基础达标" },
      { score: "2", range: "12.01 - 13.0 秒", note: "变向偏慢" },
      { score: "1", range: "> 13.0 秒", note: "急停与再启动能力较弱" },
    ],
  },
  {
    key: "enduranceScore",
    label: "耐力",
    method: "优先做 12 分钟跑：热身后连续跑 12 分钟，记录总距离。没有跑道或计距条件时，再用 20 分钟连续多球或折返跑后半段状态做替代判断。",
    hint: "填写时尽量写清测试类型和结果，例如“12 分钟跑 2350 米”或“20 分钟多球后最后 5 分钟明显掉速”。不要只按主观感觉随意打分。",
    benchmarkLabel: "优先参考：12 分钟跑",
    benchmarkNote: "分数优先按 12 分钟跑总距离判断。只有无法测距离时，才参考连续多球/折返跑在中后段是否明显掉速，手动选择最接近的档位。",
    scoreGuide: [
      { score: "5", range: "≥ 2900 米", note: "末段仍能稳住配速，长时间训练后还有余量" },
      { score: "4", range: "2600 - 2899 米", note: "整体耐力较好，后程有轻微下降但节奏稳定" },
      { score: "3", range: "2200 - 2599 米", note: "基础达标，30-45 分钟训练后还能维持常规节奏" },
      { score: "2", range: "1800 - 2199 米", note: "中后段掉速明显，连续多球或折返跑容易顶不住" },
      { score: "1", range: "< 1800 米", note: "耐力偏弱，短时间内就出现明显喘和失速，需优先提升" },
    ],
  },
  {
    key: "flexibilityScore",
    label: "柔韧性",
    method: "可参考坐位体前屈、腿后侧拉伸或全身活动范围。",
    hint: "重点看下肢与躯干活动度是否限制击球动作。",
    benchmarkLabel: "坐位体前屈参考",
    benchmarkNote: "单位为厘米，数值越高越好。",
    scoreGuide: [
      { score: "5", range: "≥ 18 cm", note: "柔韧性优秀，动作舒展" },
      { score: "4", range: "12 - 17 cm", note: "柔韧性较好" },
      { score: "3", range: "6 - 11 cm", note: "基础达标" },
      { score: "2", range: "0 - 5 cm", note: "活动范围偏紧" },
      { score: "1", range: "< 0 cm", note: "柔韧性明显不足" },
    ],
  },
  {
    key: "coreStabilityScore",
    label: "核心稳定性",
    method: "可参考平板支撑、单腿稳定或挥拍时躯干稳定感。",
    hint: "核心越稳，击球连贯性和受力传导通常越好。",
    benchmarkLabel: "平板支撑参考",
    benchmarkNote: "动作保持标准前提下计时，时间越长越好。",
    scoreGuide: [
      { score: "5", range: "≥ 180 秒", note: "核心稳定性非常好" },
      { score: "4", range: "120 - 179 秒", note: "核心较稳" },
      { score: "3", range: "75 - 119 秒", note: "基础达标" },
      { score: "2", range: "45 - 74 秒", note: "稳定性偏弱" },
      { score: "1", range: "< 45 秒", note: "核心支撑不足" },
    ],
  },
  {
    key: "shoulderMobilityScore",
    label: "肩部灵活性",
    method: "持拍绕肩、肩关节活动测试或发球动作舒展程度。",
    hint: "发球和高压动作受肩部活动度影响很大。",
    benchmarkLabel: "背后摸手测试参考",
    benchmarkNote: "记录双手中指间距，越接近或重叠越好；如伴随疼痛需下调 1 档。",
    scoreGuide: [
      { score: "5", range: "重叠 ≥ 5 cm", note: "肩部活动范围非常好" },
      { score: "4", range: "接触或间距 ≤ 3 cm", note: "活动度较好" },
      { score: "3", range: "间距 4 - 10 cm", note: "基础达标" },
      { score: "2", range: "间距 11 - 20 cm", note: "发球舒展度可能受限" },
      { score: "1", range: "间距 > 20 cm", note: "肩部灵活性明显不足" },
    ],
  },
  {
    key: "hipMobilityScore",
    label: "髋部灵活性",
    method: "看开髋、弓步、转髋和侧向移动的流畅度。",
    hint: "髋部活动不足会直接影响移动、转体和蹬转发力。",
    benchmarkLabel: "主动直腿抬高参考",
    benchmarkNote: "仰卧抬腿，记录大腿与地面夹角；越高越好，可用手机量角辅助。",
    scoreGuide: [
      { score: "5", range: "≥ 85°", note: "髋部与后侧链活动优秀" },
      { score: "4", range: "75° - 84°", note: "活动度较好" },
      { score: "3", range: "65° - 74°", note: "基础达标" },
      { score: "2", range: "55° - 64°", note: "开髋和转髋受限" },
      { score: "1", range: "< 55°", note: "髋部活动明显不足" },
    ],
  },
] as const satisfies readonly AssessmentFieldConfig[];

const categoryIcons: Record<string, React.ReactNode> = {
  "影子挥拍": <Hand className="h-4 w-4" />,
  "脚步移动": <Footprints className="h-4 w-4" />,
  "体能训练": <Dumbbell className="h-4 w-4" />,
  "墙壁练习": <Target className="h-4 w-4" />,
};

const categoryColors: Record<string, string> = {
  "影子挥拍": "bg-blue-50 text-blue-700 border-blue-200",
  "脚步移动": "bg-green-50 text-green-700 border-green-200",
  "体能训练": "bg-orange-50 text-orange-700 border-orange-200",
  "墙壁练习": "bg-purple-50 text-purple-700 border-purple-200",
};

type Exercise = {
  day: number;
  name: string;
  category: string;
  duration: number;
  description: string;
  tips: string;
  sets: number;
  reps: number;
};

type TutorialVisualRecord = {
  id: number;
  title: string;
  category?: string | null;
  description?: string | null;
  thumbnailUrl?: string | null;
  externalUrl?: string | null;
};

type TrainingProfileDraft = {
  skillLevel: "beginner" | "intermediate" | "advanced";
  manualNtrpRating: string;
  heightCm: string;
  weightKg: string;
  sprintSpeedScore: string;
  explosivePowerScore: string;
  agilityScore: string;
  enduranceScore: string;
  flexibilityScore: string;
  coreStabilityScore: string;
  shoulderMobilityScore: string;
  hipMobilityScore: string;
  assessmentNotes: string;
};

function buildDraftFromUser(user: Record<string, any> | null | undefined): TrainingProfileDraft {
  return {
    skillLevel: (user?.skillLevel as TrainingProfileDraft["skillLevel"]) || "beginner",
    manualNtrpRating: user?.manualNtrpRating != null ? String(user.manualNtrpRating) : "",
    heightCm: user?.heightCm != null ? String(user.heightCm) : "",
    weightKg: user?.weightKg != null ? String(user.weightKg) : "",
    sprintSpeedScore: user?.sprintSpeedScore != null ? String(user.sprintSpeedScore) : "",
    explosivePowerScore: user?.explosivePowerScore != null ? String(user.explosivePowerScore) : "",
    agilityScore: user?.agilityScore != null ? String(user.agilityScore) : "",
    enduranceScore: user?.enduranceScore != null ? String(user.enduranceScore) : "",
    flexibilityScore: user?.flexibilityScore != null ? String(user.flexibilityScore) : "",
    coreStabilityScore: user?.coreStabilityScore != null ? String(user.coreStabilityScore) : "",
    shoulderMobilityScore: user?.shoulderMobilityScore != null ? String(user.shoulderMobilityScore) : "",
    hipMobilityScore: user?.hipMobilityScore != null ? String(user.hipMobilityScore) : "",
    assessmentNotes: user?.assessmentNotes ?? "",
  };
}

function parseOptionalNumber(value: string) {
  if (!value.trim()) return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function parseOptionalInteger(value: string) {
  if (!value.trim()) return null;
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : null;
}

function formatDateTime(value?: string | Date | null) {
  if (!value) return null;
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return null;
  return formatDateTimeShanghai(date);
}

function formatScoreLabel(value: unknown) {
  return typeof value === "number" ? `${value}/5` : "未填写";
}

function scrollToSection(sectionId: string) {
  if (typeof document === "undefined") return;
  document.getElementById(sectionId)?.scrollIntoView({ behavior: "smooth", block: "start" });
}

function normalizeExerciseText(...values: Array<string | null | undefined>) {
  return values.join(" ").replace(/\s+/g, " ").trim().toLowerCase();
}

function inferExerciseTutorialCategories(exercise: Exercise) {
  const categoryHints: string[] = [];
  const text = normalizeExerciseText(exercise.name, exercise.description, exercise.tips, exercise.category);

  if (exercise.category === "影子挥拍") categoryHints.push("shadow");
  if (exercise.category === "脚步移动") categoryHints.push("footwork");
  if (exercise.category === "墙壁练习") categoryHints.push("wall");
  if (exercise.category === "体能训练") categoryHints.push("fitness");

  if (/正手/.test(text)) categoryHints.push("forehand");
  if (/反手/.test(text)) categoryHints.push("backhand");
  if (/发球|抛球/.test(text)) categoryHints.push("serve");
  if (/截击|网前/.test(text)) categoryHints.push("volley");
  if (/影子|挥拍|空练/.test(text)) categoryHints.push("shadow");
  if (/脚步|步伐|移动|回位|交叉步|碎步|标志盘|z字|侧滑|折返|冲刺/.test(text)) categoryHints.push("footwork");
  if (/墙|对墙/.test(text)) categoryHints.push("wall");
  if (/热身|拉伸|放松|弹力带|肩袖|核心|平板|猫牛|婴儿式|体能|爆发|耐力|敏捷|灵活/.test(text)) categoryHints.push("fitness");
  if (/战术|比赛|策略/.test(text)) categoryHints.push("strategy");

  return Array.from(new Set(categoryHints));
}

function scoreTutorialRelevance(exercise: Exercise, tutorial: TutorialVisualRecord) {
  const exerciseText = normalizeExerciseText(exercise.name, exercise.description, exercise.tips, exercise.category);
  const tutorialText = normalizeExerciseText(tutorial.title, tutorial.description, tutorial.category);
  let score = 0;

  const sharedTokens = [
    "正手",
    "反手",
    "发球",
    "截击",
    "脚步",
    "影子",
    "挥拍",
    "墙",
    "体能",
    "战术",
    "比赛",
    "上旋",
    "基础",
    "热身",
    "拉伸",
    "核心",
    "肩袖",
    "敏捷",
  ];

  for (const token of sharedTokens) {
    if (exerciseText.includes(token.toLowerCase()) && tutorialText.includes(token.toLowerCase())) {
      score += 2;
    }
  }

  if ((/空练|影子|挥拍/.test(exerciseText)) && tutorial.category === "shadow") score += 5;
  if ((/热身|拉伸|放松|肩袖|核心|敏捷|弹力带/.test(exerciseText)) && tutorial.category === "fitness") score += 5;
  if ((/标志盘|z字|脚步|侧滑|交叉步|回位|折返/.test(exerciseText)) && tutorial.category === "footwork") score += 5;
  if ((/正手/.test(exerciseText)) && tutorial.category === "forehand") score += 4;
  if ((/反手/.test(exerciseText)) && tutorial.category === "backhand") score += 4;
  if ((/发球/.test(exerciseText)) && tutorial.category === "serve") score += 4;
  if ((/墙/.test(exerciseText)) && tutorial.category === "wall") score += 4;

  return score;
}

function findExerciseVisual(
  exercise: Exercise,
  tutorialsByCategory: Map<string, TutorialVisualRecord[]>,
) {
  const categories = inferExerciseTutorialCategories(exercise);

  for (const category of categories) {
    const candidates = tutorialsByCategory.get(category) ?? [];
    if (candidates.length === 0) continue;

    const bestMatch = [...candidates].sort(
      (left, right) => scoreTutorialRelevance(exercise, right) - scoreTutorialRelevance(exercise, left),
    )[0];

    if (bestMatch?.thumbnailUrl) {
      return {
        imageUrl: bestMatch.thumbnailUrl,
        linkUrl: bestMatch.externalUrl || bestMatch.thumbnailUrl,
        title: bestMatch.title,
      };
    }
  }

  return null;
}

export default function Training() {
  const { user } = useAuth();
  const [durationDays, setDurationDays] = useState(7);
  const [selectedDay, setSelectedDay] = useState(1);
  const [generatorPanelOpen, setGeneratorPanelOpen] = useState(() => {
    if (typeof window === "undefined") return false;
    return !window.matchMedia("(min-width: 1280px)").matches;
  });
  const [generateTaskId, setGenerateTaskId] = useState<string | null>(null);
  const [adjustTaskId, setAdjustTaskId] = useState<string | null>(null);
  const [profileDraft, setProfileDraft] = useState<TrainingProfileDraft>(() => buildDraftFromUser(user));

  const utils = trpc.useUtils();
  const { data: activePlan, isLoading: planLoading } = trpc.plan.active.useQuery();
  const { data: stats } = trpc.profile.stats.useQuery();
  const { data: tutorialCatalog } = trpc.tutorial.list.useQuery({ topicArea: "tennis_skill" });
  const generateTaskQuery = useBackgroundTask(generateTaskId);
  const adjustTaskQuery = useBackgroundTask(adjustTaskId);

  useEffect(() => {
    setProfileDraft(buildDraftFromUser(user));
  }, [user]);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const mediaQuery = window.matchMedia("(min-width: 1280px)");
    const syncPanelState = () => {
      if (!mediaQuery.matches) {
        setGeneratorPanelOpen(true);
      }
    };

    syncPanelState();
    mediaQuery.addEventListener?.("change", syncPanelState);
    return () => mediaQuery.removeEventListener?.("change", syncPanelState);
  }, []);

  const profileMutation = trpc.profile.update.useMutation({
    onSuccess: async () => {
      await Promise.all([
        utils.auth.me.invalidate(),
        utils.profile.stats.invalidate(),
      ]);
      toast.success("训练档案已保存");
    },
    onError: (error) => toast.error(`保存失败: ${error.message}`),
  });

  const generateMutation = trpc.plan.generate.useMutation({
    onSuccess: (data) => {
      setGenerateTaskId(data.taskId);
      toast.success("训练计划任务已提交");
    },
    onError: (err) => toast.error("生成失败: " + err.message),
  });

  const adjustMutation = trpc.plan.adjust.useMutation({
    onSuccess: (data) => {
      setAdjustTaskId(data.taskId);
      toast.success("训练计划调整任务已提交");
    },
    onError: (err) => toast.error("调整失败: " + err.message),
  });

  const recordMutation = trpc.record.create.useMutation({
    onSuccess: () => toast.success("训练记录已创建"),
  });

  const savedDraft = useMemo(() => buildDraftFromUser(user), [user]);
  const hasSystemNtrp = stats?.latestNtrpSnapshot?.rating != null;
  const currentDisplayedNtrp = hasSystemNtrp
    ? stats?.latestNtrpSnapshot?.rating
    : user?.manualNtrpRating ?? null;
  const currentNtrpLabel = hasSystemNtrp
    ? `系统评分 ${Number(currentDisplayedNtrp || 0).toFixed(1)}`
    : currentDisplayedNtrp != null
      ? `人工基线 ${Number(currentDisplayedNtrp).toFixed(1)}`
      : "待填写人工基线";

  const missingProfileFields = useMemo(() => {
    const missing: string[] = [];
    if (!profileDraft.heightCm.trim()) missing.push("身高");
    if (!profileDraft.weightKg.trim()) missing.push("体重");
    for (const field of ASSESSMENT_FIELDS) {
      if (!profileDraft[field.key].trim()) {
        missing.push(field.label);
      }
    }
    if (!hasSystemNtrp && !profileDraft.manualNtrpRating.trim()) {
      missing.push("人工 NTRP 基线");
    }
    return missing;
  }, [hasSystemNtrp, profileDraft]);

  const hasUnsavedChanges = JSON.stringify(profileDraft) !== JSON.stringify(savedDraft);
  const canGenerate = missingProfileFields.length === 0 && !hasUnsavedChanges && !generateMutation.isPending;

  const exercises = useMemo(() => {
    if (!activePlan?.exercises) return [];
    return (activePlan.exercises as Exercise[]).filter((exercise) => exercise.day === selectedDay);
  }, [activePlan, selectedDay]);

  const tutorialsByCategory = useMemo(() => {
    const grouped = new Map<string, TutorialVisualRecord[]>();
    for (const tutorial of (tutorialCatalog ?? []) as TutorialVisualRecord[]) {
      if (!tutorial.category) continue;
      const existing = grouped.get(tutorial.category) ?? [];
      existing.push(tutorial);
      grouped.set(tutorial.category, existing);
    }
    return grouped;
  }, [tutorialCatalog]);

  const exerciseVisualMap = useMemo(() => {
    const visualMap = new Map<string, ReturnType<typeof findExerciseVisual>>();
    for (const exercise of exercises) {
      visualMap.set(`${exercise.day}:${exercise.name}:${exercise.category}`, findExerciseVisual(exercise, tutorialsByCategory));
    }
    return visualMap;
  }, [exercises, tutorialsByCategory]);

  const totalDays = activePlan?.durationDays || durationDays;
  const generating = generateMutation.isPending || generateTaskQuery.data?.status === "queued" || generateTaskQuery.data?.status === "running";
  const adjusting = adjustMutation.isPending || adjustTaskQuery.data?.status === "queued" || adjustTaskQuery.data?.status === "running";

  useEffect(() => {
    if (activePlan?.durationDays && selectedDay > activePlan.durationDays) {
      setSelectedDay(1);
    }
  }, [activePlan?.durationDays, selectedDay]);

  useEffect(() => {
    if (generateTaskQuery.data?.status === "succeeded") {
      toast.success("训练计划已生成");
      utils.plan.active.invalidate();
      utils.plan.list.invalidate();
      setGenerateTaskId(null);
    } else if (generateTaskQuery.data?.status === "failed") {
      toast.error(`训练计划生成失败: ${generateTaskQuery.data.error || "未知错误"}`);
      setGenerateTaskId(null);
    }
  }, [generateTaskQuery.data, utils.plan.active, utils.plan.list]);

  useEffect(() => {
    if (adjustTaskQuery.data?.status === "succeeded") {
      toast.success("训练计划已调整");
      utils.plan.active.invalidate();
      utils.plan.list.invalidate();
      const adjustmentNotes = (adjustTaskQuery.data.result as { adjustmentNotes?: string } | null)?.adjustmentNotes;
      if (adjustmentNotes) {
        toast.info(`调整说明: ${adjustmentNotes}`);
      }
      setAdjustTaskId(null);
    } else if (adjustTaskQuery.data?.status === "failed") {
      toast.error(`训练计划调整失败: ${adjustTaskQuery.data.error || "未知错误"}`);
      setAdjustTaskId(null);
    }
  }, [adjustTaskQuery.data, utils.plan.active, utils.plan.list]);

  const saveProfile = () => {
    profileMutation.mutate({
      skillLevel: profileDraft.skillLevel,
      manualNtrpRating: hasSystemNtrp ? undefined : parseOptionalNumber(profileDraft.manualNtrpRating),
      heightCm: parseOptionalNumber(profileDraft.heightCm),
      weightKg: parseOptionalNumber(profileDraft.weightKg),
      sprintSpeedScore: parseOptionalInteger(profileDraft.sprintSpeedScore),
      explosivePowerScore: parseOptionalInteger(profileDraft.explosivePowerScore),
      agilityScore: parseOptionalInteger(profileDraft.agilityScore),
      enduranceScore: parseOptionalInteger(profileDraft.enduranceScore),
      flexibilityScore: parseOptionalInteger(profileDraft.flexibilityScore),
      coreStabilityScore: parseOptionalInteger(profileDraft.coreStabilityScore),
      shoulderMobilityScore: parseOptionalInteger(profileDraft.shoulderMobilityScore),
      hipMobilityScore: parseOptionalInteger(profileDraft.hipMobilityScore),
      assessmentNotes: profileDraft.assessmentNotes.trim() ? profileDraft.assessmentNotes.trim() : null,
    });
  };

  const triggerGenerate = () => {
    generateMutation.mutate({
      skillLevel: profileDraft.skillLevel,
      durationDays,
    });
  };

  const openGeneratorPanel = () => {
    setGeneratorPanelOpen(true);
    window.setTimeout(() => {
      scrollToSection("training-generator-panel");
    }, 50);
  };

  const renderGeneratorPanel = (options?: { desktop?: boolean }) => {
    const desktop = options?.desktop ?? false;

    return (
      <Card
        id="training-generator-panel"
        className={cn(
          "border-0 shadow-sm",
          desktop && "w-[360px]",
        )}
      >
        <CardHeader className="pb-3">
          <div className="flex items-start justify-between gap-3">
            <div>
              <CardTitle className="flex items-center gap-2">
                <Sparkles className="h-5 w-5 text-primary" />
                {activePlan ? "重新生成计划" : "生成训练计划"}
              </CardTitle>
              <CardDescription className="mt-2">
                训练计划会综合技能水平、体测档案、人工或系统 NTRP、以及最近的分析结果。
              </CardDescription>
            </div>
            {desktop ? (
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="shrink-0"
                onClick={() => setGeneratorPanelOpen(false)}
                aria-label="收起计划生成面板"
              >
                <ChevronRight className="h-4 w-4" />
              </Button>
            ) : null}
          </div>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="space-y-2">
            <label className="text-sm font-medium">训练周期</label>
            <Select value={String(durationDays)} onValueChange={(value) => setDurationDays(Number(value))}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="3">3天计划</SelectItem>
                <SelectItem value="7">7天计划</SelectItem>
                <SelectItem value="14">14天计划</SelectItem>
                <SelectItem value="30">30天计划</SelectItem>
              </SelectContent>
            </Select>
          </div>

          <div className="rounded-2xl border border-border/60 bg-muted/20 p-4 text-sm">
            <div className="flex items-center gap-2 font-medium">
              <Trophy className="h-4 w-4 text-primary" />
              当前用于生成的评分来源
            </div>
            <div className="mt-2 text-muted-foreground">
              {hasSystemNtrp
                ? `系统自动判定 NTRP ${Number(stats?.latestNtrpSnapshot?.rating || 0).toFixed(1)}`
                : profileDraft.manualNtrpRating
                  ? `人工基线 NTRP ${profileDraft.manualNtrpRating}`
                  : "还没有人工 NTRP 基线，暂时不能生成计划"}
            </div>
          </div>

          {missingProfileFields.length > 0 ? (
            <Alert>
              <Activity className="h-4 w-4" />
              <AlertTitle>先补齐训练档案</AlertTitle>
              <AlertDescription>
                当前还缺少：{missingProfileFields.join("、")}
              </AlertDescription>
            </Alert>
          ) : null}

          {hasUnsavedChanges ? (
            <Alert>
              <ArrowRight className="h-4 w-4" />
              <AlertTitle>先保存，再生成</AlertTitle>
              <AlertDescription>
                你刚修改了训练档案。为了保证后台任务使用最新数据，请先保存训练档案。
              </AlertDescription>
            </Alert>
          ) : null}

          <Button
            data-testid="training-generate-button"
            onClick={triggerGenerate}
            disabled={!canGenerate || generating}
            className="w-full gap-2"
          >
            {generating ? (
              <>
                <Loader2 className="h-4 w-4 animate-spin" />
                后台生成中...
              </>
            ) : (
              <>
                <Sparkles className="h-4 w-4" />
                {activePlan ? "重新生成训练计划" : "生成训练计划"}
              </>
            )}
          </Button>

          <div className="text-xs leading-6 text-muted-foreground">
            当前计划生成前会先检查身高、体重、速度、爆发力、敏捷性、耐力、柔韧性、核心稳定性、肩部灵活性、髋部灵活性，以及人工或系统 NTRP。
          </div>
        </CardContent>
      </Card>
    );
  };

  if (planLoading) {
    return (
      <div className="space-y-4">
        <Skeleton className="h-40 w-full" />
        <Skeleton className="h-60 w-full" />
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold tracking-tight" data-testid="training-title">训练计划</h1>
          <p className="mt-1 text-sm text-muted-foreground">先完善训练档案，再根据当前水平与体测信息生成更可靠的训练安排。</p>
        </div>
      </div>

      {generating || adjusting ? (
        <Alert>
          <ListTodo className="h-4 w-4" />
          <AlertTitle>后台任务执行中</AlertTitle>
          <AlertDescription>
            {generating ? "训练计划正在后台生成。" : "训练计划正在根据最近分析结果调整。"}
            你可以切换到其他页面，完成后会在任务中心显示结果。
          </AlertDescription>
        </Alert>
      ) : null}

      {activePlan ? (
        <Card className="border-0 shadow-sm">
          <CardHeader className="pb-3">
            <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
              <div className="space-y-3">
                <div className="flex flex-wrap items-center gap-2">
                  <Badge className="bg-emerald-500/10 text-emerald-700">已生成当前训练计划</Badge>
                  <Badge variant="secondary" className="text-xs">
                    {activePlan.skillLevel === "beginner" ? "初级" : activePlan.skillLevel === "intermediate" ? "中级" : "高级"}
                  </Badge>
                  <Badge variant="outline" className="text-xs">{activePlan.durationDays}天周期</Badge>
                  {activePlan.version > 1 ? (
                    <Badge variant="outline" className="text-xs">v{activePlan.version} 已调整</Badge>
                  ) : null}
                </div>

                <div>
                  <CardTitle className="text-xl">{activePlan.title}</CardTitle>
                  <CardDescription className="mt-2">
                    该计划已作为当前主计划展示在页面顶部。你可以直接查看每日安排，或返回下方档案区更新评测后重新生成。
                  </CardDescription>
                </div>
              </div>

              <div className="flex flex-wrap gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => scrollToSection("training-profile-section")}
                >
                  查看训练档案
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => adjustMutation.mutate({ planId: activePlan.id })}
                  disabled={adjusting}
                  className="gap-1"
                >
                  {adjusting ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
                  重新调整
                </Button>
              </div>
            </div>
            {activePlan.adjustmentNotes ? (
              <div className="rounded-lg bg-primary/5 p-3 text-sm text-primary">
                <strong>调整说明：</strong>{activePlan.adjustmentNotes}
              </div>
            ) : null}
          </CardHeader>

          <CardContent>
            <div className="mb-4 flex gap-2 overflow-x-auto pb-2">
              {Array.from({ length: totalDays }, (_, index) => index + 1).map((day) => (
                <button
                  key={day}
                  onClick={() => setSelectedDay(day)}
                  className={cn(
                    "h-10 w-10 shrink-0 rounded-xl text-sm font-medium transition-all",
                    selectedDay === day
                      ? "bg-primary text-primary-foreground shadow-md"
                      : "bg-muted hover:bg-accent",
                  )}
                >
                  {day}
                </button>
              ))}
            </div>

            <h3 className="mb-3 font-semibold">第 {selectedDay} 天训练</h3>

            {exercises.length > 0 ? (
              <div className="space-y-3">
                {exercises.map((exercise, index) => (
                  <div key={index} className="rounded-xl border p-4 transition-shadow hover:shadow-sm">
                    <div className="flex items-start justify-between gap-3">
                      <div className="flex items-start gap-3">
                        <div className={cn(
                          "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl",
                          categoryColors[exercise.category] || "bg-gray-50 text-gray-700",
                        )}>
                          {categoryIcons[exercise.category] || <Target className="h-4 w-4" />}
                        </div>
                        <div>
                          <h4 className="text-sm font-medium">{exercise.name}</h4>
                          <p className="mt-1 text-xs text-muted-foreground">{exercise.description}</p>
                          <div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
                            <span className="flex items-center gap-1">
                              <Clock className="h-3 w-3" />
                              {exercise.duration}分钟
                            </span>
                            <span>{exercise.sets}组 × {exercise.reps}次</span>
                          </div>
                          {exercise.tips ? (
                            <p className="mt-2 rounded-md bg-primary/5 px-2 py-1 text-xs text-primary">
                              💡 {exercise.tips}
                            </p>
                          ) : null}
                          {(() => {
                            const visual = exerciseVisualMap.get(`${exercise.day}:${exercise.name}:${exercise.category}`);
                            if (!visual?.imageUrl) return null;

                            return (
                              <a
                                href={visual.linkUrl || visual.imageUrl}
                                target="_blank"
                                rel="noreferrer"
                                className="group mt-3 block max-w-sm overflow-hidden rounded-xl border border-border/60 bg-muted/10 transition-colors hover:border-primary/40"
                              >
                                <div className="relative aspect-[16/9] overflow-hidden bg-muted/30">
                                  <img
                                    src={visual.imageUrl}
                                    alt={`${exercise.name} 示意图`}
                                    loading="lazy"
                                    className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
                                  />
                                  <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 via-black/25 to-transparent px-3 py-2 text-[11px] font-medium text-white">
                                    点击查看示意图
                                  </div>
                                </div>
                                <div className="flex items-center justify-between gap-3 px-3 py-2">
                                  <div>
                                    <div className="text-xs font-medium text-foreground">匹配教程：{visual.title}</div>
                                    <div className="mt-1 text-[11px] text-muted-foreground">可点击查看标准配图或对应参考页</div>
                                  </div>
                                  <ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
                                </div>
                              </a>
                            );
                          })()}
                        </div>
                      </div>

                      <Button
                        variant="ghost"
                        size="sm"
                        className="shrink-0"
                        onClick={() => {
                          recordMutation.mutate({
                            planId: activePlan.id,
                            exerciseName: exercise.name,
                            durationMinutes: exercise.duration,
                          });
                        }}
                      >
                        <CheckCircle2 className="h-4 w-4" />
                      </Button>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <div className="py-8 text-center text-sm text-muted-foreground">
                该天暂无训练安排
              </div>
            )}
          </CardContent>
        </Card>
      ) : (
        <Card className="border-0 shadow-sm">
          <CardHeader className="pb-3">
            <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
              <div className="space-y-3">
                <Badge variant="outline" className="w-fit border-amber-300 bg-amber-50 text-amber-700">
                  尚未生成当前训练计划
                </Badge>
                <div>
                  <CardTitle className="text-xl">先完善训练档案，再生成专属训练安排</CardTitle>
                  <CardDescription className="mt-2">
                    页面顶部会优先展示当前主训练计划。你还没有可用计划时，可以先补齐评测分数并保存，然后到右侧生成区提交 3 天、7 天、14 天或 30 天计划。
                  </CardDescription>
                </div>
              </div>

              <Button
                size="sm"
                onClick={openGeneratorPanel}
                className="gap-2"
              >
                <Sparkles className="h-4 w-4" />
                前往生成训练计划
              </Button>
            </div>
          </CardHeader>
          <CardContent className="grid gap-3 md:grid-cols-3">
            <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
              <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">当前状态</div>
              <div className="mt-2 text-base font-semibold">
                {missingProfileFields.length === 0 ? "档案已可生成" : `仍缺 ${missingProfileFields.length} 项`}
              </div>
              <div className="mt-2 text-xs leading-5 text-muted-foreground">
                {missingProfileFields.length === 0 ? "保存后即可直接生成训练计划。" : `待补项目：${missingProfileFields.join("、")}`}
              </div>
            </div>
            <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
              <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">建议流程</div>
              <div className="mt-2 text-base font-semibold">档案录入 → 保存 → 生成</div>
              <div className="mt-2 text-xs leading-5 text-muted-foreground">
                建议先完成体测分数与 NTRP 基线，再提交后台任务，避免计划使用旧数据。
              </div>
            </div>
            <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
              <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">当前周期</div>
              <div className="mt-2 text-base font-semibold">{durationDays} 天</div>
              <div className="mt-2 text-xs leading-5 text-muted-foreground">
                你可以在下方生成区切换周期长度，系统会基于当前档案重新排布内容。
              </div>
            </div>
          </CardContent>
        </Card>
      )}

      <section id="training-profile-section" className="grid gap-4 xl:grid-cols-[minmax(0,1.15fr)_auto]">
        <Card className="border-0 shadow-sm">
          <CardHeader className="pb-3">
            <CardTitle className="flex items-center gap-2">
              <TestTube2 className="h-5 w-5 text-primary" />
              训练档案与评测前置信息
            </CardTitle>
            <CardDescription>
              这些数据会作为训练计划生成前的固定输入。每个专项分值统一按 1-5 分记录，旁边给出评测方法和记录建议。
            </CardDescription>
          </CardHeader>
          <CardContent className="space-y-5">
            <div className="grid gap-3 sm:grid-cols-3">
              <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
                <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">当前 NTRP</div>
                <div className="mt-2 text-lg font-semibold">{currentNtrpLabel}</div>
                <div className="mt-2 text-xs text-muted-foreground">
                  {hasSystemNtrp ? "后续以系统自动判定为准" : "当前需先填写人工基线"}
                </div>
              </div>
              <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
                <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">档案完整度</div>
                <div className="mt-2 text-lg font-semibold">{missingProfileFields.length === 0 ? "已完整" : `缺 ${missingProfileFields.length} 项`}</div>
                <div className="mt-2 text-xs text-muted-foreground">
                  {missingProfileFields.length === 0 ? "可以直接生成训练计划" : `待补：${missingProfileFields.slice(0, 3).join("、")}${missingProfileFields.length > 3 ? "…" : ""}`}
                </div>
              </div>
              <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4">
                <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">最近系统判定</div>
                <div className="mt-2 text-lg font-semibold">
                  {stats?.latestNtrpSnapshot?.rating != null ? Number(stats.latestNtrpSnapshot.rating).toFixed(1) : "暂无"}
                </div>
                <div className="mt-2 text-xs text-muted-foreground">
                  {formatDateTime(stats?.latestNtrpSnapshot?.createdAt) || "还没有自动评分记录"}
                </div>
              </div>
            </div>

            <div className="grid gap-4 md:grid-cols-2">
              <div className="space-y-2">
                <label className="text-sm font-medium">技能水平</label>
                <Select
                  value={profileDraft.skillLevel}
                  onValueChange={(value: TrainingProfileDraft["skillLevel"]) =>
                    setProfileDraft((current) => ({ ...current, skillLevel: value }))
                  }
                >
                  <SelectTrigger>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="beginner">初级 - 刚开始学习网球</SelectItem>
                    <SelectItem value="intermediate">中级 - 有一定基础</SelectItem>
                    <SelectItem value="advanced">高级 - 有较稳定实战经验</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              <div className="space-y-2">
                <label className="text-sm font-medium">人工 NTRP 基线</label>
                <Select
                  value={profileDraft.manualNtrpRating || undefined}
                  onValueChange={(value) => setProfileDraft((current) => ({ ...current, manualNtrpRating: value }))}
                  disabled={hasSystemNtrp}
                >
                  <SelectTrigger>
                    <SelectValue placeholder={hasSystemNtrp ? "系统评分已接管" : "请选择人工基线"} />
                  </SelectTrigger>
                  <SelectContent>
                    {NTRP_OPTIONS.map((option) => (
                      <SelectItem key={option} value={option}>{option}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <p className="text-xs text-muted-foreground">
                  {hasSystemNtrp
                    ? "已有系统 NTRP 后，这里只保留为历史基线，不再作为当前值编辑。"
                    : "前期请人工填写一个当前水平基线，后续系统会自动接管。"}
                </p>
              </div>

              <div className="space-y-2">
                <label className="flex items-center gap-2 text-sm font-medium">
                  <Ruler className="h-4 w-4 text-muted-foreground" />
                  身高（cm）
                </label>
                <Input
                  inputMode="decimal"
                  value={profileDraft.heightCm}
                  onChange={(event) => setProfileDraft((current) => ({ ...current, heightCm: event.target.value }))}
                  placeholder="例如 178"
                />
                <p className="text-xs text-muted-foreground">记录当前实际身高即可，用于估算力量与负荷建议。</p>
              </div>

              <div className="space-y-2">
                <label className="flex items-center gap-2 text-sm font-medium">
                  <Scale className="h-4 w-4 text-muted-foreground" />
                  体重（kg）
                </label>
                <Input
                  inputMode="decimal"
                  value={profileDraft.weightKg}
                  onChange={(event) => setProfileDraft((current) => ({ ...current, weightKg: event.target.value }))}
                  placeholder="例如 68"
                />
                <p className="text-xs text-muted-foreground">记录当前稳定体重即可，用于调节训练量与体能负荷。</p>
              </div>
            </div>

            <div className="space-y-4">
              <div className="rounded-2xl border border-border/60 bg-muted/20 px-4 py-4 text-sm text-muted-foreground">
                以下分数换算为成人业余网球训练的自测参考值。建议先做一次原始测试，再按参考区间录入 1-5 分；如果有年龄、伤病或恢复期因素，可结合体感上下调整 1 档。
              </div>

              {ASSESSMENT_FIELDS.map((field) => (
                <div key={field.key} className="grid gap-3 rounded-2xl border border-border/60 bg-background/90 p-4 xl:grid-cols-[220px_minmax(0,1fr)_minmax(280px,340px)]">
                  <div className="space-y-2">
                    <div className="text-sm font-medium">{field.label}</div>
                    <Select
                      value={profileDraft[field.key] || undefined}
                      onValueChange={(value) =>
                        setProfileDraft((current) => ({ ...current, [field.key]: value }))
                      }
                    >
                      <SelectTrigger>
                        <SelectValue placeholder="请选择 1-5 分" />
                      </SelectTrigger>
                      <SelectContent>
                        {SCORE_OPTIONS.map((option) => (
                          <SelectItem key={option.value} value={option.value}>
                            {option.label} · {option.description}
                          </SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </div>

                  <div className="grid gap-3 sm:grid-cols-2">
                    <div className="rounded-xl bg-muted/30 px-4 py-3">
                      <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">评测方法</div>
                      <div className="mt-2 text-sm leading-6">{field.method}</div>
                    </div>
                    <div className="rounded-xl bg-muted/30 px-4 py-3">
                      <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">记录方式</div>
                      <div className="mt-2 text-sm leading-6">{field.hint}</div>
                    </div>
                  </div>

                  <div className="rounded-xl border border-primary/10 bg-primary/5 px-4 py-3">
                    <div className="text-xs uppercase tracking-[0.24em] text-muted-foreground">分数测试数据</div>
                    <div className="mt-2 text-sm font-medium">{field.benchmarkLabel}</div>
                    <div className="mt-1 text-xs leading-5 text-muted-foreground">{field.benchmarkNote}</div>

                    <div className="mt-3 space-y-2">
                      {field.scoreGuide.map((guide) => {
                        const isSelected = profileDraft[field.key] === guide.score;
                        return (
                          <button
                            key={guide.score}
                            type="button"
                            onClick={() =>
                              setProfileDraft((current) => ({ ...current, [field.key]: guide.score }))
                            }
                            className={cn(
                              "flex w-full items-start justify-between gap-3 rounded-xl border px-3 py-2 text-left transition-colors",
                              isSelected
                                ? "border-primary bg-background shadow-sm"
                                : "border-border/60 bg-background/80 hover:border-primary/40",
                            )}
                          >
                            <div>
                              <div className="text-sm font-medium">{guide.score} 分</div>
                              <div className="mt-1 text-xs leading-5 text-muted-foreground">{guide.note}</div>
                            </div>
                            <div className="shrink-0 text-sm font-semibold text-primary">{guide.range}</div>
                          </button>
                        );
                      })}
                    </div>
                  </div>
                </div>
              ))}
            </div>

            <div className="space-y-2">
              <label className="text-sm font-medium">补充说明</label>
              <Textarea
                rows={4}
                value={profileDraft.assessmentNotes}
                onChange={(event) => setProfileDraft((current) => ({ ...current, assessmentNotes: event.target.value }))}
                placeholder="例如：右肩偏紧、最近恢复期、脚踝旧伤、目前每周可训练 3 次。"
              />
            </div>

            <div className="flex flex-wrap items-center gap-3">
              <Button onClick={saveProfile} disabled={profileMutation.isPending}>
                {profileMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <CheckCircle2 className="h-4 w-4" />}
                保存训练档案
              </Button>
              {hasUnsavedChanges ? (
                <Badge variant="outline">有未保存的档案变更</Badge>
              ) : (
                <Badge className="bg-emerald-500/10 text-emerald-700">档案已同步</Badge>
              )}
            </div>
          </CardContent>
        </Card>

        <div className="xl:sticky xl:top-24 xl:self-start">
          <div className="xl:hidden">
            {renderGeneratorPanel()}
          </div>

          <div className="hidden xl:block">
            <Collapsible open={generatorPanelOpen} onOpenChange={setGeneratorPanelOpen}>
              <div className="flex items-start justify-end gap-3">
                {!generatorPanelOpen ? (
                  <CollapsibleTrigger asChild>
                    <Card className="w-[76px] border-0 shadow-sm">
                      <button
                        type="button"
                        className="flex h-[320px] w-full flex-col items-center justify-between rounded-[24px] px-3 py-5 text-center"
                      >
                        <div className="rounded-2xl bg-primary/10 p-3 text-primary">
                          <Sparkles className="h-5 w-5" />
                        </div>
                        <div className="[writing-mode:vertical-rl] rotate-180 text-sm font-semibold tracking-[0.18em] text-foreground">
                          {activePlan ? "重新生成计划" : "生成训练计划"}
                        </div>
                        <div className="flex flex-col items-center gap-2 text-[11px] text-muted-foreground">
                          <span>{durationDays}天</span>
                          <span>{currentNtrpLabel}</span>
                          <ChevronLeft className="h-4 w-4 text-foreground" />
                        </div>
                      </button>
                    </Card>
                  </CollapsibleTrigger>
                ) : null}

                <CollapsibleContent forceMount className={cn(!generatorPanelOpen && "hidden")}>
                  {renderGeneratorPanel({ desktop: true })}
                </CollapsibleContent>
              </div>
            </Collapsible>
          </div>
        </div>
      </section>

      <div className="grid gap-4 md:grid-cols-3">
        <Card className="border-0 shadow-sm">
          <CardContent className="pt-5">
            <div className="flex items-center gap-3">
              <Ruler className="h-5 w-5 text-sky-600" />
              <div>
                <div className="text-sm text-muted-foreground">身高 / 体重</div>
                <div className="mt-1 text-xl font-semibold">
                  {profileDraft.heightCm || "--"} / {profileDraft.weightKg || "--"}
                </div>
              </div>
            </div>
          </CardContent>
        </Card>

        <Card className="border-0 shadow-sm">
          <CardContent className="pt-5">
            <div className="flex items-center gap-3">
              <Flame className="h-5 w-5 text-amber-600" />
              <div>
                <div className="text-sm text-muted-foreground">速度 / 爆发力</div>
                <div className="mt-1 text-xl font-semibold">
                  {formatScoreLabel(parseOptionalInteger(profileDraft.sprintSpeedScore))} · {formatScoreLabel(parseOptionalInteger(profileDraft.explosivePowerScore))}
                </div>
              </div>
            </div>
          </CardContent>
        </Card>

        <Card className="border-0 shadow-sm">
          <CardContent className="pt-5">
            <div className="flex items-center gap-3">
              <Target className="h-5 w-5 text-emerald-600" />
              <div>
                <div className="text-sm text-muted-foreground">敏捷 / 核心 / 灵活性</div>
                <div className="mt-1 text-xl font-semibold">
                  {formatScoreLabel(parseOptionalInteger(profileDraft.agilityScore))}
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}
