import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
  Activity,
  AlertTriangle,
  Apple,
  ArrowRight,
  Bell,
  Bluetooth,
  BookOpen,
  Bot,
  CalendarCheck,
  ClipboardCheck,
  Check,
  CheckCircle2,
  ChevronLeft,
  ChevronRight,
  CircleGauge,
  CloudSun,
  Database,
  Droplet,
  Droplets,
  Egg,
  FileChartColumn,
  FileText,
  Footprints,
  Gauge,
  Headphones,
  HeartPulse,
  History,
  Info,
  Languages,
  Link2,
  LoaderCircle,
  LocateFixed,
  LockKeyhole,
  LogOut,
  MapPin,
  MessageCircleMore,
  Minus,
  Moon,
  MoreHorizontal,
  Pause,
  Play,
  Plus,
  RefreshCw,
  Route,
  Search,
  Settings2,
  ShieldAlert,
  ShieldCheck,
  Smartphone,
  Sparkles,
  Sun,
  Timer,
  Trash2,
  TrendingUp,
  Trophy,
  UploadCloud,
  Utensils,
  Watch,
  Waves,
  Wheat,
  Zap,
} from "lucide-react";
import {
  Area,
  AreaChart,
  Bar,
  CartesianGrid,
  ComposedChart,
  LabelList,
  Line,
  LineChart,
  ReferenceArea,
  ReferenceLine,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";
import {
  AiSummary,
  Button,
  Card,
  Composer,
  DateStrip,
  demoItems,
  DemoDock,
  EmptyState,
  Metric,
  PageHeader,
  PhoneFrame,
  SafetyBanner,
  Score20Slider,
  SectionTitle,
  StatusBadge,
  TimelineRail,
} from "./components";
import {
  authCapabilityCopy,
  isAuthCapability,
  type AuthCapability,
} from "./auth";
import { serviceMode, services } from "./services";
import { buildAgentContext, useApp } from "./store";
import { formatPace, resolveTrainingProgress } from "./training-engine";
import { deriveTrainingWeekStatus } from "./training-status";
import type {
  AgentCycleComparisonSummary,
  AgentDailyRecipePlan,
  AgentMealType,
  ActivityRecord,
  CycleComparisonRange,
  CycleComparisonSummaryRequest,
  CycleComparisonZoneType,
  FoodEntry,
  MealEntry,
  NutritionIntake,
  NutritionProduct,
  PlanItem,
  RouteId,
  TrainingGoal,
  TrainingAnalysisData,
} from "./types";

type SessionZone = { label: string; value: number; color: string };
type SessionSplit = {
  segment: string;
  pace: string;
  heartRate: number;
  cadence: number;
  duration: string;
};

type SessionPerformance = {
  fastestPace: string;
  trainingLoad: number;
  recommendedLoad: [number, number];
  cadence: number;
  strideLength: number;
  vdot: number;
  elevationGain: number;
  recoveryHours: number;
  paceTrend: Array<{ point: string; paceSeconds: number }>;
  heartRateTrend: Array<{ point: string; heartRate: number }>;
  elevationTrend: Array<{ point: string; elevation: number }>;
  paceZones: SessionZone[];
  heartRateZones: SessionZone[];
  splits: SessionSplit[];
};

const parsePaceSeconds = (pace: string) => {
  const matched = pace.match(/(\d+)[′'’:]\s*(\d+)/);
  return matched ? Number(matched[1]) * 60 + Number(matched[2]) : 330;
};

const paceFromSeconds = (seconds: number) => {
  const safe = Math.max(1, Math.round(seconds));
  return `${Math.floor(safe / 60)}′${String(safe % 60).padStart(2, "0")}″`;
};

const durationFromSeconds = (seconds: number) => {
  const safe = Math.max(0, Math.round(seconds));
  const hours = Math.floor(safe / 3600);
  const minutes = Math.floor((safe % 3600) / 60);
  const rest = safe % 60;
  return hours
    ? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`
    : `${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
};

function buildSessionPerformance(
  activity: ActivityRecord,
  plan?: PlanItem | null,
): SessionPerformance {
  const seed = [...activity.id].reduce((sum, char) => sum + char.charCodeAt(0), 0);
  const averagePaceSeconds = parsePaceSeconds(activity.pace);
  const cadence = 176 + (seed % 9);
  const strideLength = Number(((1000 / cadence) / (averagePaceSeconds / 60)).toFixed(2));
  const trainingLoad = Math.round(
    Math.max(28, (plan?.load ?? activity.distance * 8.1) + (activity.heartRate - 135) * 0.7),
  );
  const recommendedLoad: [number, number] = [
    Math.max(20, Math.round((plan?.load ?? trainingLoad) * 0.82)),
    Math.round((plan?.load ?? trainingLoad) * 1.18),
  ];
  const pointCount = 12;
  const paceTrend = Array.from({ length: pointCount }, (_, index) => ({
    point: `${Math.round(((index + 1) / pointCount) * activity.distance * 10) / 10} km`,
    paceSeconds: Math.round(
      averagePaceSeconds + Math.sin(index * 1.35) * 12 + (index > 8 ? 7 : 0) - (index === 6 ? 18 : 0),
    ),
  }));
  const heartRateTrend = Array.from({ length: pointCount }, (_, index) => ({
    point: paceTrend[index].point,
    heartRate: Math.round(
      activity.heartRate - 9 + Math.min(index, 7) * 1.7 + Math.sin(index * 1.1) * 4,
    ),
  }));
  const baseElevation = 42 + (seed % 18);
  const elevationTrend = Array.from({ length: pointCount }, (_, index) => ({
    point: paceTrend[index].point,
    elevation: Math.round(baseElevation + Math.sin(index * 0.85) * 13 + index * 0.8),
  }));
  const segmentCount = Math.max(1, Math.ceil(activity.distance));
  const splits = Array.from({ length: segmentCount }, (_, index) => {
    const remaining = Math.max(0, activity.distance - index);
    const segmentDistance = Math.min(1, remaining || 1);
    const paceSeconds = Math.round(
      averagePaceSeconds + Math.sin(index * 1.42) * 11 + (index === segmentCount - 1 ? 4 : 0),
    );
    return {
      segment: segmentDistance < 0.995 ? `${segmentDistance.toFixed(2)} km` : `${index + 1} km`,
      pace: paceFromSeconds(paceSeconds),
      heartRate: Math.round(activity.heartRate - 7 + Math.min(index, 6) * 1.4 + Math.sin(index) * 3),
      cadence: cadence + ((index % 3) - 1) * 2,
      duration: durationFromSeconds(paceSeconds * segmentDistance),
    };
  });
  return {
    fastestPace: paceFromSeconds(Math.min(...paceTrend.map((point) => point.paceSeconds))),
    trainingLoad,
    recommendedLoad,
    cadence,
    strideLength,
    vdot: Number((40.8 + Math.max(0, (360 - averagePaceSeconds) / 18) + activity.distance / 14).toFixed(1)),
    elevationGain: Math.round(activity.distance * (7 + (seed % 5))),
    recoveryHours: Math.max(8, Math.round(trainingLoad / 5.3)),
    paceTrend,
    heartRateTrend,
    elevationTrend,
    paceZones: [
      { label: "有氧基础", value: 18, color: "#5a9ec4" },
      { label: "有氧耐力", value: 52, color: "#6ca447" },
      { label: "乳酸阈值", value: 19, color: "#d5a82d" },
      { label: "速度耐力", value: 8, color: "#de6d37" },
      { label: "冲刺", value: 3, color: "#c44b4b" },
    ],
    heartRateZones: [
      { label: "1 区", value: 7, color: "#5a9ec4" },
      { label: "2 区", value: 61, color: "#6ca447" },
      { label: "3 区", value: 21, color: "#d5a82d" },
      { label: "4 区", value: 9, color: "#de6d37" },
      { label: "5 区", value: 2, color: "#c44b4b" },
    ],
    splits,
  };
}

const emptyNutritionIntake = (): NutritionIntake => ({
  caloriesKcal: 0,
  hydrationMl: 0,
  carbohydrateG: 0,
  proteinG: 0,
  fatG: 0,
});

const addNutritionIntake = (
  total: NutritionIntake,
  item: NutritionIntake,
): NutritionIntake => ({
  caloriesKcal: total.caloriesKcal + item.caloriesKcal,
  hydrationMl: total.hydrationMl + item.hydrationMl,
  carbohydrateG: total.carbohydrateG + item.carbohydrateG,
  proteinG: total.proteinG + item.proteinG,
  fatG: total.fatG + item.fatG,
});

const sumMealNutrition = (meals: MealEntry[]) =>
  meals.flatMap((meal) => meal.foods).reduce(addNutritionIntake, emptyNutritionIntake());

const mealImageByName: Record<string, string> = {
  早餐: "/meal-breakfast.png",
  午餐: "/meal-lunch.png",
  晚餐: "/meal-dinner.png",
  加餐: "/meal-snack.png",
};

const mealImageKeyByName: Record<string, string> = {
  早餐: "breakfast",
  午餐: "lunch",
  晚餐: "dinner",
  加餐: "snack",
};

const mealImageForName = (mealName: string) =>
  mealImageByName[mealName] ?? "/meal-snack.png";

const nutritionForProduct = (product: NutritionProduct): NutritionIntake =>
  product.nutrients ?? {
    caloriesKcal:
      product.category === "gel" ? 120 : product.category === "recovery" ? 360 : 0,
    hydrationMl:
      product.category === "water" || product.category === "electrolyte"
        ? product.servingAmount
        : 0,
    carbohydrateG:
      product.category === "gel" ? 30 : product.category === "recovery" ? 60 : 0,
    proteinG: product.category === "recovery" ? 25 : 0,
    fatG: 0,
  };

const topRoutes: RouteId[] = ["HOME-01", "TRAIN-HUB", "NUT-01", "PROFILE-01"];
const noBottomRoutes: RouteId[] = [
  "AUTH-01",
  "ONB-01",
  "ONB-02",
  "ONB-03",
  "DEV-01",
  "DEV-02",
  "PLAN-02",
  "PLAN-03",
  "TRAIN-LIVE",
  "TRAIN-FEEDBACK",
  "NUT-02",
  "NUT-08",
  "NUT-09",
  "NUT-10",
  "ACCOUNT-SECURITY",
  "PHONE-BIND",
  "LANGUAGE-SETTINGS",
  "VERSION-INFO",
  "CONTACT-DEV",
  "ABOUT-CPT",
  "LEGAL-DOCUMENT",
  "REPORT-01",
  "STATE-LAB",
];

const trainingTypeIcon = (item: Pick<PlanItem, "title">) => {
  if (item.title.includes("间歇"))
    return {
      src: "/training-interval-v29.png",
      alt: "间歇跑——跨越标志桶的动画跑者",
    };
  if (item.title.includes("恢复"))
    return {
      src: "/training-recovery-v29.png",
      alt: "恢复跑——佩戴毛巾的放松慢跑者",
    };
  if (
    item.title.includes("节奏") ||
    item.title.includes("阈值") ||
    item.title.includes("稳态")
  )
    return {
      src: "/training-tempo-v29.png",
      alt: "节奏跑——查看运动手表的稳定跑者",
    };
  if (item.title.includes("长距离") || item.title.includes("长跑"))
    return {
      src: "/training-long-v29.png",
      alt: "长距离跑——穿补水背心的耐力跑者",
    };
  return {
    src: "/training-easy-v30.png",
    alt: "轻松跑——姿态舒展的低强度动画跑者",
  };
};

const reminderFromNode = (
  node: NonNullable<ReturnType<typeof createReminderNode>>,
) => ({
  id: `fuel-${node.id}`,
  nodeId: node.id,
  enabled: true,
  triggerSecond:
    serviceMode === "mock"
      ? node.reminder!.demoOffsetSecond ?? node.reminder!.offsetSecond
      : node.reminder!.offsetSecond,
  triggerLabel: node.reminder!.displayTime,
  type: node.product.type,
  amount: node.product.dose,
  productName: node.product.name,
  status: "scheduled" as const,
  vibrationCount: 0,
});

function createReminderNode(
  state: ReturnType<typeof useApp>["state"],
  afterNodeId?: string,
) {
  const nodes = [...(state.nutritionPlan?.nodes ?? [])]
    .filter((node) => node.phase === "训练中" && node.reminder?.enabled)
    .sort(
      (first, second) =>
        first.reminder!.offsetSecond - second.reminder!.offsetSecond,
    );
  const startIndex = afterNodeId
    ? nodes.findIndex((node) => node.id === afterNodeId) + 1
    : 0;
  return (
    nodes
      .slice(Math.max(0, startIndex))
      .find(
        (node) =>
          !state.completedNutritionNodes.includes(node.id),
      ) ?? null
  );
}

function CoachAvatar() {
  return (
    <div
      className="message-avatar message-avatar--coach"
      aria-label="CPT AI 教练"
    >
      <img src="/cpt-ai-coach-avatar-v4.png" alt="" aria-hidden="true" />
    </div>
  );
}

function PlanGenerationGate({
  title,
  back = false,
}: {
  title: string;
  back?: boolean;
}) {
  const { go } = useApp();
  return (
    <div className="page incomplete-profile-page">
      <PageHeader title={title} back={back} />
      <Card className="incomplete-profile-card">
        <div className="incomplete-profile-icon">
          <CircleGauge />
        </div>
        <span>你的基础档案已经准备好了</span>
        <h1>下一段训练，等你一起定下来</h1>
        <p>
          回到首页告诉 AI 教练“帮我生成训练计划”，我会结合你的目标、恢复和已授权数据，
          陪你把方案定下来。也可以先去“我的”补充档案。
        </p>
        <Button onClick={() => go("HOME-01", "home")}>
          去首页一起规划
        </Button>
        <Button variant="secondary" onClick={() => go("PROFILE-01", "profile")}>
          先完善我的档案
        </Button>
      </Card>
    </div>
  );
}

function SessionModeBar({
  authenticated,
  onLogin,
}: {
  authenticated: boolean;
  onLogin: () => void;
}) {
  return (
    <div
      className={`session-mode-bar ${authenticated ? "is-authenticated" : "is-guest"}`}
      role="status"
      aria-live="polite"
    >
      <span aria-hidden="true">{authenticated ? <ShieldCheck /> : <Info />}</span>
      <div>
        <b>{authenticated ? "个人模式" : "游客预览"}</b>
        <small>{authenticated ? "个人数据与云端服务已启用" : "当前展示演示数据，不会写入云端"}</small>
      </div>
      {!authenticated && (
        <button type="button" onClick={onLogin}>
          登录解锁
        </button>
      )}
    </div>
  );
}

export function App() {
  const { state, go, connectDevice } = useApp();
  const [authRequest, setAuthRequest] = useState<AuthCapability | null>(null);
  const [deviceAuthRequest, setDeviceAuthRequest] = useState<string | null>(null);
  const pendingAuthActionRef = useRef<HTMLElement | null>(null);
  const authTriggerRef = useRef<HTMLElement | null>(null);
  const requestedRouteHandledRef = useRef(false);
  const urlParameters = new URLSearchParams(window.location.search);
  const requestedRoute = [
    ...topRoutes,
    "ONB-03" as RouteId,
    "PLAN-04" as RouteId,
    "RECORD-03" as RouteId,
    "DEV-01" as RouteId,
  ].find(
    (route) => route === urlParameters.get("route"),
  );
  const requestedFocus = urlParameters.get("focus");
  const authGateEnabled = !navigator.webdriver || urlParameters.has("authGate");
  const activeDemo = demoItems[state.demo - 1];
  const presentationMode =
    navigator.webdriver ||
    urlParameters.has("presentation");
  const showBottomNav =
    state.profileComplete && !noBottomRoutes.includes(state.route);
  useEffect(() => {
    if (requestedRouteHandledRef.current || !requestedRoute) return;
    requestedRouteHandledRef.current = true;
    if (state.route !== requestedRoute) {
      go(requestedRoute);
    }
  }, [go, requestedRoute, state.route]);
  useEffect(() => {
    if (state.route !== requestedRoute || requestedFocus !== "cycle-mileage") {
      return;
    }
    const timer = window.setTimeout(() => {
      document
        .getElementById("cycle-mileage")
        ?.scrollIntoView({ block: "start", behavior: "instant" });
    }, 80);
    return () => window.clearTimeout(timer);
  }, [requestedFocus, requestedRoute, state.route]);
  const openLogin = (capability: AuthCapability = "account") => {
    pendingAuthActionRef.current = null;
    authTriggerRef.current = document.activeElement as HTMLElement | null;
    setAuthRequest(capability);
  };
  const authOverlay = authRequest ? (
    <div className="auth-gate" role="presentation">
      <AuthPage
        embedded
        capability={authRequest}
        onAuthenticated={() => {
          const pendingAction = pendingAuthActionRef.current;
          pendingAuthActionRef.current = null;
          authTriggerRef.current = null;
          setAuthRequest(null);
          if (pendingAction) window.setTimeout(() => pendingAction.click(), 0);
        }}
        onCancel={() => {
          const trigger = authTriggerRef.current;
          pendingAuthActionRef.current = null;
          authTriggerRef.current = null;
          setAuthRequest(null);
          window.setTimeout(() => trigger?.focus(), 0);
        }}
      />
    </div>
  ) : null;
  const deviceAuthOverlay = deviceAuthRequest ? (
    <ProviderAuthorizationDialog
      key={deviceAuthRequest}
      provider={deviceAuthRequest}
      onAuthorize={() => connectDevice(deviceAuthRequest)}
      onClose={() => setDeviceAuthRequest(null)}
    />
  ) : null;
  return (
    <div
      className={`prototype-stage ${presentationMode ? "is-presentation" : "is-app-preview"}`}
    >
      <DemoDock />
      <div
        className={`prototype-phone-host auth-mode-${state.authMode}`}
        onClickCapture={(event) => {
          if (
            !authGateEnabled ||
            state.authMode === "authenticated" ||
            authRequest ||
            state.route === "AUTH-01"
          )
            return;
          const target = event.target as HTMLElement;
          const protectedAction = target.closest<HTMLElement>("[data-auth-required]");
          const capability = protectedAction?.dataset.authRequired;
          if (!protectedAction || !isAuthCapability(capability)) return;
          event.preventDefault();
          event.stopPropagation();
          pendingAuthActionRef.current = protectedAction;
          authTriggerRef.current = protectedAction;
          setAuthRequest(capability);
        }}
      >
        <PhoneFrame
          showBottomNav={showBottomNav}
          toast={state.toast}
          sessionBanner={
            state.route !== "AUTH-01" ? (
            <SessionModeBar
              authenticated={state.authMode === "authenticated"}
              onLogin={() => openLogin("account")}
            />
              ) : null
          }
          overlay={authOverlay ?? deviceAuthOverlay}
        >
          <RouteView
            route={state.route}
            onRequestDeviceAuthorization={setDeviceAuthRequest}
          />
        </PhoneFrame>
      </div>
      <aside className="stage-note">
        <span>{activeDemo ? `D${state.demo} · ${activeDemo.title}` : "当前路由"}</span>
        <b>{state.route}</b>
        {activeDemo && "steps" in activeDemo ? (
          <ol className="stage-note__steps">
            {activeDemo.steps.map((step, index) => (
              <li key={step}>
                <span>{index + 1}</span>
                {step}
              </li>
            ))}
          </ol>
        ) : (
          <p>三个一级入口共享统一状态；首页作为 AI 训练与营养交互中枢。</p>
        )}
      </aside>
    </div>
  );
}

function RouteView({
  route,
  onRequestDeviceAuthorization,
}: {
  route: RouteId;
  onRequestDeviceAuthorization: (provider: string) => void;
}) {
  switch (route) {
    case "AUTH-01":
      return <AuthPage />;
    case "ONB-01":
      return <GoalPage />;
    case "ONB-02":
      return <AbilityPage />;
    case "ONB-03":
      return <NutritionProfilePage />;
    case "DEV-01":
      return <DevicePage onRequestAuthorization={onRequestDeviceAuthorization} />;
    case "DEV-02":
      return <PermissionPage />;
    case "HOME-01":
      return <HomePage />;
    case "RECOVERY-01":
      return <RecoveryPage />;
    case "REMIND-01":
      return <ReminderPage />;
    case "TRAIN-HUB":
      return <TrainingHub />;
    case "PLAN-02":
      return <PlanWizard />;
    case "PLAN-03":
      return <PlanPreview />;
    case "PLAN-04":
      return <PlanDetail />;
    case "PLAN-05":
      return <PlanAgentRedirect />;
    case "PLAN-06":
      return <PlanConflict />;
    case "TRAIN-READY":
      return <TrainingDeviceHandoff />;
    case "TRAIN-LIVE":
      return <TrainingDeviceHandoff />;
    case "TRAIN-SUMMARY":
      return <TrainingSummary />;
    case "TRAIN-FEEDBACK":
      return <TrainingFeedback />;
    case "TRAIN-ANALYSIS":
      return <TrainingAnalysis />;
    case "NUT-01":
      return <TodayFuelingPage />;
    case "NUT-02":
      return <NutritionPlan />;
    case "NUT-03":
      return <NutritionDuring />;
    case "NUT-04":
      return <NutritionAgentRedirect />;
    case "NUT-05":
      return <NutritionAgentRedirect />;
    case "NUT-06":
      return <NutritionAgentRedirect />;
    case "NUT-07":
      return <NutritionAgentRedirect />;
    case "NUT-08":
      return <NutritionAlternative />;
    case "NUT-09":
      return <NutritionReminderSettings />;
    case "NUT-10":
      return <GutTrainingGuide />;
    case "RECORD-02":
      return <RecordDay />;
    case "RECORD-03":
      return <RecordDetail />;
    case "RECORD-04":
      return <TrendPage />;
    case "PROFILE-01":
      return <ProfilePage />;
    case "ACCOUNT-SECURITY":
      return <AccountSecurityPage />;
    case "PHONE-BIND":
      return <PhoneBindPage />;
    case "LANGUAGE-SETTINGS":
      return <LanguageSettingsPage />;
    case "VERSION-INFO":
      return <VersionInfoPage />;
    case "CONTACT-DEV":
      return <ContactDeveloperPage />;
    case "ABOUT-CPT":
      return <AboutCptPage />;
    case "LEGAL-DOCUMENT":
      return <LegalDocumentPage />;
    case "REPORT-01":
      return <ReportPage />;
    case "AI-02":
      return <AiDetail />;
    case "AI-03":
      return <HistoryPage />;
    case "AI-04":
      return <AssistantInfo />;
    case "STATE-LAB":
      return <StateLab />;
    default:
      return <HomePage />;
  }
}

function AuthPage({
  embedded = false,
  capability = "account",
  onAuthenticated,
  onCancel,
}: {
  embedded?: boolean;
  capability?: AuthCapability;
  onAuthenticated?: () => void;
  onCancel?: () => void;
} = {}) {
  const { state, go, patch, startDemo } = useApp();
  const [agreed, setAgreed] = useState(false);
  const [showConsentError, setShowConsentError] = useState(false);
  const [loginMode, setLoginMode] = useState<"wechat" | "phone" | null>(null);
  const [phone, setPhone] = useState("");
  const [code, setCode] = useState("");
  const [loggingIn, setLoggingIn] = useState(false);
  const consentRef = useRef<HTMLInputElement>(null);
  const loginTriggerRef = useRef<HTMLButtonElement | null>(null);
  const dialogRef = useRef<HTMLDivElement>(null);
  const sheetRef = useRef<HTMLDivElement>(null);
  const consentDescription = showConsentError
    ? "auth-consent-hint auth-consent-error"
    : "auth-consent-hint";

  const openLogin = (mode: "wechat" | "phone", trigger: HTMLButtonElement) => {
    if (!agreed) {
      setShowConsentError(true);
      consentRef.current?.focus();
      return;
    }
    loginTriggerRef.current = trigger;
    setLoginMode(mode);
  };
  const closeLogin = () => {
    setLoginMode(null);
    window.setTimeout(() => loginTriggerRef.current?.focus(), 0);
  };
  const submitLogin = async () => {
    if (!loginMode || loggingIn) return;
    setLoggingIn(true);
    try {
      const result = await services.account.login(
        loginMode,
        loginMode === "phone"
          ? { phone: phone.replace(/\D/g, ""), code }
          : undefined,
      );
      localStorage.setItem("running-ai-access-token", result.accessToken);
      patch({
        authMode: "authenticated",
        toast: "欢迎回来，个人训练数据已经接续",
      });
      if (onAuthenticated) onAuthenticated();
      else go(state.profileComplete ? "HOME-01" : "ONB-01", state.profileComplete ? "home" : undefined);
    } catch {
      patch({ toast: "暂时无法登录。请核对账号信息或稍后再试。" });
    } finally {
      setLoggingIn(false);
    }
  };
  useEffect(() => {
    if (!loginMode) return;
    const dialog = dialogRef.current;
    const controls = dialog?.querySelectorAll<HTMLElement>("button,input");
    controls?.[0]?.focus();
    const onKey = (event: KeyboardEvent) => {
      if (event.key === "Escape") closeLogin();
      if (event.key === "Tab" && controls?.length) {
        const first = controls[0];
        const last = controls[controls.length - 1];
        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      }
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [loginMode]);
  useEffect(() => {
    if (!embedded || loginMode) return;
    const controls = sheetRef.current?.querySelectorAll<HTMLElement>(
      "button:not(:disabled), input:not(:disabled), [tabindex]:not([tabindex='-1'])",
    );
    const firstAction = sheetRef.current?.querySelector<HTMLElement>(
      ".auth-actions button",
    );
    window.requestAnimationFrame(() => firstAction?.focus());
    const closeSheet = (event: KeyboardEvent) => {
      if (event.key === "Escape") onCancel?.();
      if (event.key === "Tab" && controls?.length) {
        const first = controls[0];
        const last = controls[controls.length - 1];
        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      }
    };
    document.addEventListener("keydown", closeSheet);
    return () => document.removeEventListener("keydown", closeSheet);
  }, [embedded, loginMode, onCancel]);

  return (
    <div
      ref={sheetRef}
      className={`auth-page page ${embedded ? "auth-page--embedded" : ""}`}
      role={embedded ? "dialog" : undefined}
      aria-modal={embedded ? "true" : undefined}
      aria-labelledby="auth-page-title"
    >
      {embedded && (
        <button className="auth-gate__close" aria-label="暂不登录" onClick={onCancel}>
          ×
        </button>
      )}
      <div className="auth-symbol">
        <Route size={38} />
      </div>
      <p className="eyebrow">AI RUNNING COMPANION</p>
      <h1 id="auth-page-title">
        {embedded ? authCapabilityCopy[capability].title : "把训练、恢复和补给连成一条线"}
      </h1>
      <p className="lead">
        {embedded
          ? authCapabilityCopy[capability].description
          : "无需登录即可完整预览全部界面；登录后再连接你的档案、设备和云端服务。"}
      </p>
      {embedded && <p className="auth-benefit">{authCapabilityCopy[capability].benefit}</p>}
      <div className="auth-actions">
        <Button
          onClick={(event) => openLogin("wechat", event.currentTarget)}
          aria-describedby="auth-consent-hint"
        >
          <MessageCircleMore size={18} /> 微信一键登录
        </Button>
        <Button
          variant="secondary"
          onClick={(event) => openLogin("phone", event.currentTarget)}
          aria-describedby="auth-consent-hint"
        >
          手机号登录
        </Button>
        <Button
          variant="ghost"
          onClick={() => {
            if (onCancel) onCancel();
            else startDemo(2);
          }}
        >
          暂不登录，继续预览
        </Button>
      </div>
      <div className="auth-consent">
        <label className="check-row">
          <input
            ref={consentRef}
            type="checkbox"
            checked={agreed}
            aria-invalid={showConsentError}
            aria-describedby={consentDescription}
            onChange={(event) => {
              setAgreed(event.target.checked);
              if (event.target.checked) setShowConsentError(false);
            }}
          />
          <span>已阅读并同意用户协议与隐私政策</span>
        </label>
        <p id="auth-consent-hint" className="consent-hint">
          登录前需明确勾选同意，取消勾选不会进入下一步。
        </p>
        {showConsentError && (
          <p id="auth-consent-error" className="consent-error" role="alert">
            请先阅读并同意用户协议与隐私政策
          </p>
        )}
      </div>
      {loginMode && (
        <div
          className="dialog-backdrop auth-login-backdrop"
          onMouseDown={(event) =>
            event.target === event.currentTarget && closeLogin()
          }
        >
          <div
            ref={dialogRef}
            className="confirm-dialog auth-login-dialog"
            role="dialog"
            aria-modal="true"
            aria-labelledby="auth-login-title"
          >
            <button
              className="dialog-close"
              aria-label="关闭登录窗口"
              onClick={closeLogin}
            >
              ×
            </button>
            <span className="dialog-kicker">账号登录</span>
            <h2 id="auth-login-title">
              {loginMode === "wechat" ? "授权微信账号登录" : "手机号验证码登录"}
            </h2>
            {loginMode === "wechat" ? (
              <Card className="account-preview">
                <MessageCircleMore />
                <div>
                  <b>微信账号</b>
                  <p>将同步头像、昵称和跨端个人数据</p>
                </div>
              </Card>
            ) : (
              <>
                <label className="field">
                  <span>手机号</span>
                  <input
                    type="tel"
                    value={phone}
                    onChange={(event) => setPhone(event.target.value)}
                    placeholder="请输入手机号"
                  />
                </label>
                <label className="field">
                  <span>验证码</span>
                  <input
                    inputMode="numeric"
                    value={code}
                    onChange={(event) =>
                      setCode(event.target.value.replace(/\D/g, ""))
                    }
                    placeholder="请输入验证码"
                  />
                </label>
              </>
            )}
            <Button
              className="page-primary"
              disabled={
                loggingIn ||
                (loginMode === "phone" &&
                  (!/^1\d{10}$/.test(phone.replace(/\D/g, "")) ||
                    code.length < 4))
              }
              onClick={submitLogin}
            >
              {loggingIn
                ? "登录中…"
                : loginMode === "wechat"
                  ? "授权并登录"
                  : "验证并登录"}
            </Button>
            <button className="text-action" onClick={closeLogin}>
              取消
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function ProgressDots({ step }: { step: number }) {
  return (
    <div className="progress-dots">
      <span className={step >= 1 ? "on" : ""} />
      <span className={step >= 2 ? "on" : ""} />
      <span className={step >= 3 ? "on" : ""} />
    </div>
  );
}

const weekDayLabels = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const trainingGoalOptions: readonly TrainingGoal[] = [
  "全马完赛",
  "半马完赛",
  "规律跑发展体能",
  "10公里",
];

function GoalPage() {
  const { state, profile, go } = useApp();
  const balancedDayOrder = [0, 2, 5, 1, 3, 4, 6];
  const updateFrequency = (nextValue: number) => {
    const frequency = Math.max(1, Math.min(7, nextValue));
    const currentDays = [...(state.profile.trainingDays ?? [])];
    const trainingDays = currentDays.slice(0, frequency);
    balancedDayOrder.forEach((day) => {
      if (trainingDays.length < frequency && !trainingDays.includes(day))
        trainingDays.push(day);
    });
    profile({ frequency, trainingDays: trainingDays.sort((a, b) => a - b) });
  };
  const toggleTrainingDay = (day: number) => {
    const currentDays = state.profile.trainingDays ?? [];
    const trainingDays = currentDays.includes(day)
      ? currentDays.filter((item) => item !== day)
      : [...currentDays, day];
    if (!trainingDays.length) return;
    profile({
      frequency: trainingDays.length,
      trainingDays: trainingDays.sort((a, b) => a - b),
    });
  };
  return (
    <div className="page onboarding">
      <PageHeader title="训练目标" eyebrow="建立跑步档案" />
      <ProgressDots step={1} />
      <h1>
        你想通过跑步
        <br />
        完成什么？
      </h1>
      <p className="lead">告诉我你想抵达哪里，接下来的训练节奏和补给安排，我们一起拆解。</p>
      <fieldset className="field profile-radio-field goal-radio-field">
        <legend>训练目标</legend>
        <div className="profile-radio-grid goal-option-grid">
          {trainingGoalOptions.map((goal) => (
            <label
              className={state.profile.goal === goal ? "selected" : ""}
              key={goal}
            >
              <input
                type="radio"
                name="training-goal"
                value={goal}
                checked={state.profile.goal === goal}
                onChange={() => profile({ goal })}
              />
              <span>{goal}</span>
            </label>
          ))}
        </div>
        <small>选择最符合当前阶段的目标，后续可在“我的”页面重新调整。</small>
      </fieldset>
      <div className="field">
        <span>每周训练次数</span>
        <div className="number-stepper">
          <button
            aria-label="减少每周训练次数"
            onClick={() => updateFrequency(state.profile.frequency - 1)}
          >
            <Minus />
          </button>
          <b>
            {state.profile.frequency}
            <small>次 / 周</small>
          </b>
          <button
            aria-label="增加每周训练次数"
            onClick={() => updateFrequency(state.profile.frequency + 1)}
          >
            <Plus />
          </button>
        </div>
      </div>
      <fieldset className="field training-day-field">
        <legend>具体可训练日期</legend>
        <div className="training-day-grid" role="group" aria-label="选择每周可训练日期">
          {weekDayLabels.map((label, day) => {
            const selected = state.profile.trainingDays.includes(day);
            return (
              <button
                type="button"
                key={label}
                aria-label={label}
                aria-pressed={selected}
                className={selected ? "selected" : ""}
                onClick={() => toggleTrainingDay(day)}
              >
                <span>{label.slice(0, 1)}</span>
                <b>{label.slice(1)}</b>
              </button>
            );
          })}
        </div>
        <small>已为你留出 {state.profile.trainingDays.length} 个训练日，后续计划会按这些时间安排。</small>
      </fieldset>
      <Button className="page-primary" onClick={() => go("ONB-02")}>
        继续 <ArrowRight size={17} />
      </Button>
    </div>
  );
}

function AbilityPage() {
  const { state, profile, go } = useApp();
  const experiences = ["刚开始跑", "1–2 年", "3–5 年", "5 年以上"];
  const consistencyOptions = ["不足 2 周", "2–4 周", "1–3 个月", "3 个月以上"];
  const durationOptions = ["30 分钟内", "30–60 分钟", "60–90 分钟", "90 分钟以上"];
  const injuryOptions = ["否", "是，已经恢复", "是，目前仍影响"];
  const painOptions = ["无", "偶尔", "持续存在"];
  const painAreaOptions = ["膝", "踝 / 足", "胫骨", "小腿 / 跟腱", "髋 / 臀", "腰背", "其他"];
  const genders = [
    { value: "male" as const, label: "男" },
    { value: "female" as const, label: "女" },
    { value: "undisclosed" as const, label: "暂不透露" },
  ];
  const bodyValid =
    state.profile.heightCm >= 120 &&
    state.profile.heightCm <= 230 &&
    state.profile.weightKg >= 30 &&
    state.profile.weightKg <= 250;
  const longestRunValid =
    state.profile.longestRunKm >= 0 && state.profile.longestRunKm <= 100;
  const painValid =
    state.profile.currentPain === "无" || state.profile.currentPainAreas.length > 0;
  const trainingProfileValid =
    bodyValid &&
    longestRunValid &&
    Boolean(state.profile.weeklyKmConsistency) &&
    Boolean(state.profile.sessionDuration) &&
    Boolean(state.profile.injuryInterruption) &&
    Boolean(state.profile.currentPain) &&
    painValid;
  const togglePainArea = (item: string) => {
    const selected = state.profile.currentPainAreas.includes(item);
    profile({
      currentPainAreas: selected
        ? state.profile.currentPainAreas.filter((value) => value !== item)
        : [...state.profile.currentPainAreas, item],
    });
  };
  return (
    <div className="page onboarding training-profile-form">
      <PageHeader title="训练档案" eyebrow="建立跑步档案" />
      <ProgressDots step={2} />
      <h1>
        先了解你的
        <br />
        训练基础
      </h1>
      <p className="lead">回想最近一段时间的真实情况就好，没有标准答案。</p>
      <section className="profile-question-section training-profile-section" aria-labelledby="basic-profile-title">
        <div className="profile-question-section__head">
          <small>基本情况</small>
          <h2 id="basic-profile-title">先认识现在的你</h2>
          <p>用于估算训练负荷和恢复需求，之后可以随时修改。</p>
        </div>
      <fieldset className="field profile-radio-field question-block">
        <legend>性别</legend>
        <div className="profile-radio-grid">
          {genders.map((item) => (
            <label
              className={state.profile.gender === item.value ? "selected" : ""}
              key={item.value}
            >
              <input
                type="radio"
                name="gender"
                value={item.value}
                checked={state.profile.gender === item.value}
                onChange={() => profile({ gender: item.value })}
              />
              <span>{item.label}</span>
            </label>
          ))}
        </div>
        <small>这些信息只用于把训练负荷和补给算得更贴合你，之后可随时修改。</small>
      </fieldset>
      <div className="field question-block">
        <span>跑步经验</span>
        <div className="choice-grid">
          {experiences.map((item) => (
            <button
              className={state.profile.experience === item ? "selected" : ""}
              onClick={() => profile({ experience: item })}
              key={item}
            >
              {item}
            </button>
          ))}
        </div>
      </div>
      <div className="anthropometric-grid question-block question-block--body">
        <label className="field">
          <span>身高</span>
          <div className="unit-input">
            <input
              aria-label="身高"
              type="number"
              inputMode="decimal"
              min="120"
              max="230"
              value={state.profile.heightCm}
              onChange={(e) => profile({ heightCm: Number(e.target.value) })}
            />
            <b>cm</b>
          </div>
        </label>
        <label className="field">
          <span>体重</span>
          <div className="unit-input">
            <input
              aria-label="体重"
              type="number"
              inputMode="decimal"
              min="30"
              max="250"
              step="0.1"
              value={state.profile.weightKg}
              onChange={(e) => profile({ weightKg: Number(e.target.value) })}
            />
            <b>kg</b>
          </div>
        </label>
      </div>
      {!bodyValid && (
        <p className="field-error" role="alert">
          请输入有效身高（120–230 cm）和体重（30–250 kg）
        </p>
      )}
      </section>
      <section className="profile-question-section training-profile-section" aria-labelledby="training-load-title">
        <div className="profile-question-section__head">
          <small>近期训练</small>
          <h2 id="training-load-title">回想最近 4 周的训练情况</h2>
          <p>我会据此安排更合适的距离、频率和单次训练时长。</p>
        </div>
      <label className="field question-block">
        <span>近期每周跑量</span>
        <div className="unit-input">
          <input
            type="number"
            min="0"
            max="300"
            value={state.profile.weeklyKm}
            onChange={(e) => profile({ weeklyKm: Number(e.target.value) })}
          />
          <b>公里</b>
        </div>
      </label>
        <label className="field question-block">
          <span>过去 4 周，单次最长跑了多少公里？</span>
          <div className="unit-input">
            <input
              aria-label="过去4周单次最长距离"
              type="number"
              inputMode="decimal"
              min="0"
              max="100"
              step="0.1"
              value={state.profile.longestRunKm}
              onChange={(event) => profile({ longestRunKm: Number(event.target.value) })}
            />
            <b>公里</b>
          </div>
          <small>如最近没有完成长距离，可填写 0。</small>
        </label>
        {!longestRunValid && (
          <p className="field-error" role="alert">请输入 0–100 公里之间的有效距离</p>
        )}
        <div className="field question-block">
          <span>目前的周跑量已连续保持多久？</span>
          <div className="choice-grid">
            {consistencyOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.weeklyKmConsistency === item}
                className={state.profile.weeklyKmConsistency === item ? "selected" : ""}
                onClick={() => profile({ weeklyKmConsistency: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>一次训练通常能安排多长时间？</span>
          <div className="choice-grid">
            {durationOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.sessionDuration === item}
                className={state.profile.sessionDuration === item ? "selected" : ""}
                onClick={() => profile({ sessionDuration: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
      </section>
      <section className="profile-question-section profile-question-section--safety" aria-labelledby="injury-title">
        <div className="profile-question-section__head">
          <small>身体感受</small>
          <h2 id="injury-title">最近跑步时，身体感觉怎么样？</h2>
          <p>这些信息会帮助我避开不适合你的训练强度。</p>
        </div>
        <div className="field question-block">
          <span>最近 6 个月是否因疼痛或伤病中断跑步？</span>
          <div className="choice-grid choice-grid--single">
            {injuryOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.injuryInterruption === item}
                className={state.profile.injuryInterruption === item ? "selected" : ""}
                onClick={() => profile({ injuryInterruption: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>当前跑步时是否有持续疼痛或不适？</span>
          <div className="choice-grid choice-grid--three">
            {painOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.currentPain === item}
                className={state.profile.currentPain === item ? "selected" : ""}
                onClick={() => profile({
                  currentPain: item,
                  ...(item === "无" ? { currentPainAreas: [] } : {}),
                })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        {state.profile.currentPain !== "无" && (
          <div className="conditional-field" role="group" aria-label="疼痛或不适部位">
            <b>主要出现在哪些部位？</b>
            <div className="choice-grid profile-multi-choice">
              {painAreaOptions.map((item) => (
                <button
                  type="button"
                  aria-pressed={state.profile.currentPainAreas.includes(item)}
                  className={state.profile.currentPainAreas.includes(item) ? "selected" : ""}
                  onClick={() => togglePainArea(item)}
                  key={item}
                >{item}</button>
              ))}
            </div>
          </div>
        )}
        {!painValid && <p className="field-error" role="alert">请选择疼痛或不适部位</p>}
        {(state.profile.injuryInterruption === "是，目前仍影响" || state.profile.currentPain === "持续存在") && (
          <SafetyBanner>如果疼痛持续、越来越明显，或已经影响日常活动，建议先咨询医生或康复专业人士，再继续训练。</SafetyBanner>
        )}
      </section>
      <Card className="hint-card">
        <CircleGauge size={20} />
        <p>
          这些信息只用于让训练和补给建议更贴合你，之后可以在“我的”里随时更新。
        </p>
      </Card>
      <Button
        disabled={!trainingProfileValid}
        className="page-primary"
        onClick={() => go("ONB-03")}
      >
        继续 <ArrowRight size={17} />
      </Button>
    </div>
  );
}

function NutritionProfilePage() {
  const { state, patch, profile, generatePlan, go } = useApp();
  const editingExistingProfile = state.stack.includes("PROFILE-01");
  const allergyOptions = [
    "乳制品",
    "麸质",
    "花生 / 坚果",
    "海鲜",
    "蛋类",
    "大豆",
    "牛肉 / 猪肉",
    "素食",
    "其他明确忌口",
    "不确定",
  ];
  const giSymptomOptions = ["腹胀", "反酸", "恶心", "腹痛", "腹泻", "其他"];
  const fuelProductOptions = ["不补给", "水", "运动饮料", "能量胶", "能量棒 / 固体食物", "盐丸 / 电解质"];
  const fuelingIssueProductOptions = ["能量胶", "运动饮料", "能量棒 / 固体食物", "盐丸 / 电解质", "不确定"];
  const fuelingIssueStageOptions = ["训练前", "训练前半程", "训练后半程", "训练后", "不确定"];
  const barrierOptions = ["通常能按计划执行", "容易忘记", "没有饥饿或口渴感", "担心胃肠不适", "携带不方便", "不清楚补多少", "不喜欢现有产品"];
  const levels = [
    { n: 1, label: "少汗" },
    { n: 2, label: "轻微" },
    { n: 3, label: "明显" },
    { n: 4, label: "大量" },
    { n: 5, label: "湿透" },
  ];
  const toggleAllergy = (item: string) => {
    const selected = state.profile.foodAllergies.includes(item);
    profile({
      foodAllergies: selected
        ? state.profile.foodAllergies.filter((value) => value !== item)
        : [...state.profile.foodAllergies, item],
    });
  };
  const toggleGiSymptom = (item: string) => {
    const selected = state.profile.runningGiSymptoms.includes(item);
    profile({
      runningGiSymptoms: selected
        ? state.profile.runningGiSymptoms.filter((value) => value !== item)
        : [...state.profile.runningGiSymptoms, item],
    });
  };
  const toggleFuelProduct = (item: string) => {
    if (item === "不补给") {
      profile({ fuelProducts: ["不补给"] });
      return;
    }
    const current = state.profile.fuelProducts.filter((value) => value !== "不补给");
    profile({
      fuelProducts: current.includes(item)
        ? current.filter((value) => value !== item)
        : [...current, item],
    });
  };
  const toggleBarrier = (item: string) => {
    if (item === "通常能按计划执行") {
      profile({ fuelingBarriers: [item] });
      return;
    }
    const current = state.profile.fuelingBarriers.filter((value) => value !== "通常能按计划执行");
    profile({
      fuelingBarriers: current.includes(item)
        ? current.filter((value) => value !== item)
        : [...current, item],
    });
  };
  const parseFuelingGiSelections = (detail: string) => {
    const selections = new Set(
      detail
        .split("｜")
        .map((item) => item.trim())
        .filter((item) => /^(补给|阶段|症状)：/.test(item)),
    );
    if (!selections.size && detail.trim()) {
      fuelingIssueProductOptions.slice(0, -1).forEach((item) => {
        if (detail.includes(item.split(" / ")[0])) selections.add(`补给：${item}`);
      });
      if (/跑后段|训练后半|后半程/.test(detail)) selections.add("阶段：训练后半程");
      else if (/跑前段|训练前半|前半程/.test(detail)) selections.add("阶段：训练前半程");
      else if (/训练后|跑后/.test(detail)) selections.add("阶段：训练后");
      else if (/训练前|跑前/.test(detail)) selections.add("阶段：训练前");
      giSymptomOptions.slice(0, -1).forEach((item) => {
        if (detail.includes(item)) selections.add(`症状：${item}`);
      });
    }
    return [...selections];
  };
  const fuelingGiSelections = parseFuelingGiSelections(state.profile.fuelingGiDetail);
  const toggleFuelingGiSelection = (group: "补给" | "阶段" | "症状", item: string) => {
    const token = `${group}：${item}`;
    const groupPrefix = `${group}：`;
    const current = fuelingGiSelections.filter((value) => {
      if (group === "阶段" && value.startsWith(groupPrefix)) return false;
      if (item === "不确定" && value.startsWith(groupPrefix)) return false;
      if (item !== "不确定" && value === `${group}：不确定`) return false;
      return true;
    });
    const next = fuelingGiSelections.includes(token)
      ? fuelingGiSelections.filter((value) => value !== token)
      : [...current, token];
    profile({ fuelingGiDetail: next.join("｜") });
  };
  const foodSafetyValid =
    state.profile.foodSafetyStatus === "无" ||
    state.profile.foodAllergies.length > 0;
  const giValid =
    state.profile.runningGiFrequency === "从不" ||
    state.profile.runningGiSymptoms.length > 0;
  const fuelingGiValid =
    state.profile.fuelingGiHistory === "从未" ||
    fuelingGiSelections.length > 0;
  const nutritionProfileValid =
    Boolean(state.profile.foodSafetyStatus) &&
    foodSafetyValid &&
    Boolean(state.profile.runningGiFrequency) &&
    giValid &&
    Boolean(state.profile.fuelStartDuration) &&
    state.profile.fuelProducts.length > 0 &&
    Boolean(state.profile.carbsPerHour) &&
    Boolean(state.profile.fluidPerHour) &&
    Boolean(state.profile.electrolyteHabit) &&
    Boolean(state.profile.fuelingGiHistory) &&
    fuelingGiValid &&
    state.profile.fuelingBarriers.length > 0;
  return (
    <div className="page onboarding training-profile-form nutrition-profile-form">
      <PageHeader title="营养档案" eyebrow="建立跑步档案" />
      <ProgressDots step={3} />
      <h1>
        了解你的
        <br />
        补给习惯
      </h1>
      <p className="lead">
        回想平时训练中的真实情况就好，我会据此安排更合适的补水与补能。
      </p>
      <section className="profile-question-section" aria-labelledby="sweat-title">
        <div className="profile-question-section__head">
          <small>出汗情况</small>
          <h2 id="sweat-title">一次常见训练中，你通常出多少汗？</h2>
          <p>选择最接近平时训练后的状态。</p>
        </div>
        <div className="question-block sweat-question-block">
          <div className="sweat-rating" role="radiogroup" aria-label="训练出汗程度">
            {levels.map((level) => (
              <button
                type="button"
                role="radio"
                aria-checked={state.profile.sweat === level.n}
                key={level.n}
                aria-label={`${level.label}，出汗等级 ${level.n}`}
                className={state.profile.sweat === level.n ? "selected" : ""}
                onClick={() => profile({ sweat: level.n })}
              >
                <div className="sweat-person">
                  <img
                    src={`/sweat-level-${level.n}.png`}
                    alt={`${level.label}出汗示意`}
                  />
                </div>
                <span>{level.label}</span>
                {state.profile.sweat === level.n && (
                  <i className="sweat-selected">
                    <Check size={13} aria-hidden="true" />
                  </i>
                )}
              </button>
            ))}
          </div>
        </div>
      </section>
      <section className="profile-question-section profile-question-section--safety" aria-labelledby="nutrition-safety-title">
        <div className="profile-question-section__head">
          <small>饮食与胃肠</small>
          <h2 id="nutrition-safety-title">哪些食物或情况会让你不舒服？</h2>
          <p>用于避开可能引起不适的食物和补给方式。</p>
        </div>
        <div className="field question-block">
          <span>是否存在食物过敏、不耐受或明确忌口？</span>
          <div className="choice-grid">
            {["无", "有"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.foodSafetyStatus === item}
                className={state.profile.foodSafetyStatus === item ? "selected" : ""}
                onClick={() => profile({
                  foodSafetyStatus: item,
                  ...(item === "无" ? { foodAllergies: [], foodSafetyDetail: "" } : {}),
                })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        {state.profile.foodSafetyStatus === "有" && (
          <div className="conditional-field" role="group" aria-label="食物过敏不耐受或忌口详情">
            <b>请选择涉及的食物</b>
            <div className="choice-grid profile-multi-choice">
              {allergyOptions.map((item) => (
                <button
                  type="button"
                  aria-pressed={state.profile.foodAllergies.includes(item)}
                  className={state.profile.foodAllergies.includes(item) ? "selected" : ""}
                  onClick={() => toggleAllergy(item)}
                  key={item}
                >{item}</button>
              ))}
            </div>
          </div>
        )}
        {!foodSafetyValid && <p className="field-error" role="alert">请选择至少一项涉及的食物或饮食限制</p>}
        <div className="field question-block">
          <span>跑步中是否容易出现胃肠不适？</span>
          <div className="choice-grid choice-grid--three">
            {["从不", "偶尔", "经常"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.runningGiFrequency === item}
                className={state.profile.runningGiFrequency === item ? "selected" : ""}
                onClick={() => profile({
                  runningGiFrequency: item,
                  ...(item === "从不" ? { runningGiSymptoms: [] } : {}),
                })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        {state.profile.runningGiFrequency !== "从不" && (
          <div className="conditional-field" role="group" aria-label="跑步中胃肠症状">
            <b>通常出现哪些症状？</b>
            <div className="choice-grid profile-multi-choice">
              {giSymptomOptions.map((item) => (
                <button
                  type="button"
                  aria-pressed={state.profile.runningGiSymptoms.includes(item)}
                  className={state.profile.runningGiSymptoms.includes(item) ? "selected" : ""}
                  onClick={() => toggleGiSymptom(item)}
                  key={item}
                >{item}</button>
              ))}
            </div>
          </div>
        )}
        {!giValid && <p className="field-error" role="alert">请选择至少一种常见症状</p>}
      </section>
      <section className="profile-question-section" aria-labelledby="fueling-habit-title">
        <div className="profile-question-section__head">
          <small>跑中补给</small>
          <h2 id="fueling-habit-title">你平时是怎么补给的？</h2>
          <p>了解现在的习惯后，我会尽量给出更容易执行的安排。</p>
        </div>
        <div className="field question-block">
          <span>训练多长时间后，你通常开始补给？</span>
          <div className="choice-grid choice-grid--single">
            {["60 分钟内通常不补", "60–90 分钟开始", "90–120 分钟开始", "120 分钟以上开始", "没有固定规则"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.fuelStartDuration === item}
                className={state.profile.fuelStartDuration === item ? "selected" : ""}
                onClick={() => profile({ fuelStartDuration: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>跑步中通常会使用哪些补给？（可多选）</span>
          <div className="choice-grid profile-multi-choice">
            {fuelProductOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.fuelProducts.includes(item)}
                className={state.profile.fuelProducts.includes(item) ? "selected" : ""}
                onClick={() => toggleFuelProduct(item)}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>目前每小时大约补充多少碳水？</span>
          <div className="choice-grid">
            {["不知道", "少于 30 g/h", "30–60 g/h", "60–90 g/h", "高于 90 g/h"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.carbsPerHour === item}
                className={state.profile.carbsPerHour === item ? "selected" : ""}
                onClick={() => profile({ carbsPerHour: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>目前每小时大约补充多少液体？</span>
          <div className="choice-grid">
            {["不知道", "少于 300 ml/h", "300–500 ml/h", "500–750 ml/h", "高于 750 ml/h"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.fluidPerHour === item}
                className={state.profile.fluidPerHour === item ? "selected" : ""}
                onClick={() => profile({ fluidPerHour: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        <div className="field question-block">
          <span>你通常如何补充电解质或钠？</span>
          <div className="choice-grid choice-grid--single">
            {["基本不补", "主要靠运动饮料", "天气热或长距离时补", "每次按计划补充"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.electrolyteHabit === item}
                className={state.profile.electrolyteHabit === item ? "selected" : ""}
                onClick={() => profile({ electrolyteHabit: item })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
      </section>
      <section className="profile-question-section profile-question-section--feedback" aria-labelledby="fueling-feedback-title">
        <div className="profile-question-section__head">
          <small>补给体验</small>
          <h2 id="fueling-feedback-title">哪些情况会影响你按计划补给？</h2>
          <p>用于调整补给时间、剂量和携带方式。</p>
        </div>
        <div className="field question-block">
          <span>是否出现过与补给相关的胃肠不适？</span>
          <div className="choice-grid">
            {["从未", "有过"].map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.fuelingGiHistory === item}
                className={state.profile.fuelingGiHistory === item ? "selected" : ""}
                onClick={() => profile({
                  fuelingGiHistory: item,
                  ...(item === "从未" ? { fuelingGiDetail: "" } : {}),
                })}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
        {state.profile.fuelingGiHistory === "有过" && (
          <div className="conditional-field fueling-issue-picker" role="group" aria-label="补给相关胃肠不适详情">
            <div>
              <b>当时使用了什么补给？</b>
              <div className="choice-grid profile-multi-choice">
                {fuelingIssueProductOptions.map((item) => {
                  const token = `补给：${item}`;
                  return (
                    <button
                      type="button"
                      aria-pressed={fuelingGiSelections.includes(token)}
                      className={fuelingGiSelections.includes(token) ? "selected" : ""}
                      onClick={() => toggleFuelingGiSelection("补给", item)}
                      key={item}
                    >{item}</button>
                  );
                })}
              </div>
            </div>
            <div>
              <b>大约发生在什么时候？</b>
              <div className="choice-grid profile-multi-choice">
                {fuelingIssueStageOptions.map((item) => {
                  const token = `阶段：${item}`;
                  return (
                    <button
                      type="button"
                      aria-pressed={fuelingGiSelections.includes(token)}
                      className={fuelingGiSelections.includes(token) ? "selected" : ""}
                      onClick={() => toggleFuelingGiSelection("阶段", item)}
                      key={item}
                    >{item}</button>
                  );
                })}
              </div>
            </div>
            <div>
              <b>主要是什么感受？</b>
              <div className="choice-grid profile-multi-choice">
                {giSymptomOptions.map((item) => {
                  const token = `症状：${item}`;
                  return (
                    <button
                      type="button"
                      aria-pressed={fuelingGiSelections.includes(token)}
                      className={fuelingGiSelections.includes(token) ? "selected" : ""}
                      onClick={() => toggleFuelingGiSelection("症状", item)}
                      key={item}
                    >{item}</button>
                  );
                })}
              </div>
            </div>
          </div>
        )}
        {!fuelingGiValid && <p className="field-error" role="alert">请选择至少一项相关补给、发生阶段或身体感受</p>}
        <div className="field question-block">
          <span>哪些原因最容易让你没有按补给计划执行？（可多选）</span>
          <div className="choice-grid profile-multi-choice">
            {barrierOptions.map((item) => (
              <button
                type="button"
                aria-pressed={state.profile.fuelingBarriers.includes(item)}
                className={state.profile.fuelingBarriers.includes(item) ? "selected" : ""}
                onClick={() => toggleBarrier(item)}
                key={item}
              >{item}</button>
            ))}
          </div>
        </div>
      </section>
      <SafetyBanner>我会避开已确认的过敏或不耐受食物；如果胃肠症状频繁或明显，建议先咨询医生或营养专业人士。</SafetyBanner>
      <Button
        disabled={state.planGenerationStatus === "loading" || !nutritionProfileValid}
        className="page-primary"
        onClick={async () => {
          if (editingExistingProfile) {
            if (await generatePlan()) go("PROFILE-01");
          } else {
            patch({
              profileComplete: true,
              planGenerationStatus: "idle",
              toast: "档案准备好了。接下来可以连接设备，也可以先去首页和 AI 教练聊聊。",
            });
            go("DEV-01");
          }
        }}
      >
        {state.planGenerationStatus === "loading" ? (
          <>
            <LoaderCircle className="spin" /> 正在把新档案融入计划
          </>
        ) : editingExistingProfile ? (
          "保存并更新方案"
        ) : (
          "保存档案，连接设备"
        )}
      </Button>
    </div>
  );
}

const healthPlatforms = [
  { name: "华为健康", icon: HeartPulse, note: "运动、心率、睡眠与恢复" },
  { name: "Apple Health", icon: Apple, note: "统一读取 iPhone 与手表健康数据" },
  { name: "小米运动健康", icon: Smartphone, note: "运动记录、心率与睡眠" },
];

const directDeviceProviders = [
  { name: "高驰", icon: Watch, note: "过渡接入 · 课表同步与训练回传" },
];

const providerAuthorizationCopy: Record<
  string,
  {
    accountName: string;
    nativeAction: string;
    description: string;
    scopes: string[];
  }
> = {
  华为健康: {
    accountName: "华为账号",
    nativeAction: "使用华为账号一键授权",
    description: "将跳转至华为运动健康完成身份确认，并由你选择允许读取的数据。",
    scopes: ["运动记录", "心率", "睡眠与恢复"],
  },
  "Apple Health": {
    accountName: "Apple ID",
    nativeAction: "使用本机健康权限一键授权",
    description: "将调用 iPhone 健康权限页；数据授权范围由系统控制，可随时在设置中关闭。",
    scopes: ["体能训练", "心率", "睡眠与恢复"],
  },
  小米运动健康: {
    accountName: "小米账号",
    nativeAction: "使用小米账号一键授权",
    description: "将跳转至小米运动健康确认账号，并授权读取已同步的训练与身体状态。",
    scopes: ["运动记录", "心率", "睡眠"],
  },
  高驰: {
    accountName: "COROS 账号",
    nativeAction: "使用 COROS 账号一键授权",
    description: "将前往 COROS 授权页，用于课表下发与训练完成数据回传。",
    scopes: ["训练课表", "活动记录", "心率与跑姿数据"],
  },
};

function DevicePage({
  onRequestAuthorization,
}: {
  onRequestAuthorization: (provider: string) => void;
}) {
  const { state, patch, syncDevice, go, back } = useApp();
  const origin = state.stack[state.stack.length - 1];
  const onboarding = origin === "ONB-03" || !state.profileComplete;
  const hasConnectedProvider = state.device.connectedProviders.length > 0;
  const continueWithoutSync = () => {
    patch({
      toast: onboarding
        ? "已跳过平台同步。你可以先制定计划，之后随时在「我的」中连接数据。"
        : "已保留当前设置。之后可随时回来连接训练数据。",
    });
    if (onboarding) go("PLAN-02");
    else back();
  };
  return (
    <div className="page device-page">
      <PageHeader title="连接训练数据" />
      <div className="page-intro">
        <h1>让训练数据陪你一起进步</h1>
        <p>连接后可以减少手动记录并获得更贴合的建议；暂不同步也能继续使用，之后随时可以补充连接。</p>
      </div>
      <div className="device-optional-note" role="note">
        <Info aria-hidden="true" />
        <span>
          <b>平台同步为可选项</b>
          <small>不会影响你继续制定训练计划</small>
        </span>
      </div>
      <div className="data-source-section">
        <div className="data-source-section__head">
          <div>
            <b>推荐方式</b>
            <span>健康平台</span>
          </div>
          <StatusBadge tone="green">优先接入</StatusBadge>
        </div>
        <div className="provider-list provider-list--recommended">
          {healthPlatforms.map(({ name, icon: Icon, note }) => {
          const connected = state.device.connectedProviders.includes(name);
          const authorizing = state.device.authorizingProvider === name;
          return (
            <Card key={name} className="provider-row">
              <div className="provider-icon">
                <Icon size={23} />
              </div>
              <div>
                <b>{name}</b>
                <span>{note}</span>
              </div>
              {connected ? (
                <StatusBadge tone="green">
                  {state.device.status === "syncing" ? "同步中" : "已连接"}
                </StatusBadge>
              ) : (
                <Button
                  variant="secondary"
                  data-auth-required="device"
                  data-provider={name}
                  onClick={() => onRequestAuthorization(name)}
                  disabled={Boolean(state.device.authorizingProvider)}
                >
                  {authorizing ? (
                    <>
                      <LoaderCircle className="spin" /> 授权中
                    </>
                  ) : (
                    "连接"
                  )}
                </Button>
              )}
            </Card>
          );
          })}
        </div>
      </div>
      <details className="direct-device-disclosure">
        <summary>
          <span><Watch /> 暂时无法使用健康平台？</span>
          <small>查看设备直连（过渡方案）</small>
        </summary>
        <div className="provider-list provider-list--direct">
          {directDeviceProviders.map(({ name, icon: Icon, note }) => {
            const connected = state.device.connectedProviders.includes(name);
            const authorizing = state.device.authorizingProvider === name;
            return (
              <Card key={name} className="provider-row">
                <div className="provider-icon"><Icon size={23} /></div>
                <div><b>{name}</b><span>{note}</span></div>
                {connected ? (
                  <StatusBadge tone="green">
                    {state.device.status === "syncing" ? "同步中" : "已连接"}
                  </StatusBadge>
                ) : (
                  <Button
                    variant="secondary"
                    data-auth-required="device"
                    data-provider={name}
                    onClick={() => onRequestAuthorization(name)}
                    disabled={Boolean(state.device.authorizingProvider)}
                  >
                    {authorizing ? <><LoaderCircle className="spin" /> 授权中</> : "连接"}
                  </Button>
                )}
              </Card>
            );
          })}
        </div>
        <p>设备直连只用于当前原型验证；正式方案优先通过标准健康平台汇总数据。</p>
      </details>
      {hasConnectedProvider && (
        <Card className="sync-card">
          <div>
            <b>数据已经接上</b>
            <span>{state.device.lastSyncAt}</span>
            <small>
              数据来源：
              {state.device.syncSources.length
                ? state.device.syncSources.join("、")
                : state.device.connectedProviders.join("、")}
            </small>
          </div>
          <Button data-auth-required="cloudSync" onClick={syncDevice}>
            <RefreshCw size={15} /> 立即同步
          </Button>
        </Card>
      )}
      <div className={`device-page-actions ${hasConnectedProvider ? "is-connected" : "is-optional"}`}>
        {hasConnectedProvider ? (
          <Button
            className="page-primary"
            onClick={() => (onboarding ? go("PLAN-02") : back())}
          >
            {onboarding ? "继续，一起制定计划" : "保存并返回"}
            <ArrowRight aria-hidden="true" />
          </Button>
        ) : (
          <Button
            variant="secondary"
            className="device-continue-without-sync"
            onClick={continueWithoutSync}
          >
            {onboarding ? "暂不同步，继续制定计划" : "暂不同步，返回上一页"}
            <ArrowRight aria-hidden="true" />
          </Button>
        )}
        {hasConnectedProvider && (
          <Button
            variant="secondary"
            className="device-continue-without-sync"
            onClick={continueWithoutSync}
          >
            {onboarding ? "暂不更新数据，继续制定计划" : "暂不更新数据，完成设置"}
            <ArrowRight aria-hidden="true" />
          </Button>
        )}
        <small>
          {hasConnectedProvider
            ? "已连接的数据会用于后续训练分析"
            : "不会影响后续使用，可在「我的」中随时连接"}
        </small>
      </div>
    </div>
  );
}

function ProviderAuthorizationDialog({
  provider,
  onAuthorize,
  onClose,
}: {
  provider: string;
  onAuthorize: () => Promise<boolean>;
  onClose: () => void;
}) {
  const profile = providerAuthorizationCopy[provider] ?? {
    accountName: `${provider} 账号`,
    nativeAction: `使用 ${provider} 一键授权`,
    description: `将跳转至 ${provider} 完成身份确认和数据授权。`,
    scopes: ["运动记录", "心率", "恢复数据"],
  };
  const platform = [...healthPlatforms, ...directDeviceProviders].find(
    (item) => item.name === provider,
  );
  const PlatformIcon = platform?.icon ?? Link2;
  const dialogRef = useRef<HTMLElement>(null);
  const closeTimerRef = useRef<number | null>(null);
  const [mode, setMode] = useState<"choice" | "account">("choice");
  const [account, setAccount] = useState("");
  const [password, setPassword] = useState("");
  const [status, setStatus] = useState<"idle" | "pending" | "success" | "error">("idle");
  const [error, setError] = useState("");

  useEffect(() => {
    const previousFocus = document.activeElement as HTMLElement | null;
    const focusableSelector = [
      "button:not(:disabled)",
      "input:not(:disabled)",
      "[href]",
      '[tabindex]:not([tabindex="-1"])',
    ].join(",");
    const focusFrame = window.requestAnimationFrame(() => {
      const preferred = dialogRef.current?.querySelector<HTMLElement>("[data-autofocus]");
      const first = dialogRef.current?.querySelector<HTMLElement>(focusableSelector);
      (preferred ?? first ?? dialogRef.current)?.focus();
    });
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        event.stopPropagation();
        onClose();
        return;
      }
      if (event.key !== "Tab" || !dialogRef.current) return;
      const focusable = Array.from(
        dialogRef.current.querySelectorAll<HTMLElement>(focusableSelector),
      );
      if (!focusable.length) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      window.cancelAnimationFrame(focusFrame);
      document.removeEventListener("keydown", handleKeyDown);
      if (closeTimerRef.current) window.clearTimeout(closeTimerRef.current);
      window.setTimeout(() => previousFocus?.focus(), 0);
    };
  }, []);

  const authorize = async (method: "native" | "account") => {
    if (status === "pending") return;
    if (method === "account" && (!account.trim() || !password)) {
      setError(`请输入完整的${profile.accountName}和密码。`);
      return;
    }
    setError("");
    setStatus("pending");
    const connected = await onAuthorize();
    if (!connected) {
      setStatus("error");
      setError(`${provider} 暂时没有完成授权，请检查网络后重试。`);
      return;
    }
    setStatus("success");
    closeTimerRef.current = window.setTimeout(onClose, 850);
  };

  return (
    <div
      className="dialog-backdrop provider-auth-backdrop"
      data-testid="provider-authorization-backdrop"
      onPointerDown={(event) => {
        if (event.target === event.currentTarget && status !== "pending") onClose();
      }}
    >
      <section
        ref={dialogRef}
        className="confirm-dialog provider-auth-dialog"
        role="dialog"
        aria-modal="true"
        aria-labelledby="provider-auth-title"
        aria-describedby="provider-auth-description"
        data-provider={provider}
        data-status={status}
        tabIndex={-1}
      >
        <header className="provider-auth-dialog__head">
          <span className="provider-auth-dialog__icon" aria-hidden="true"><PlatformIcon /></span>
          <div>
            <small>连接训练数据</small>
            <h2 id="provider-auth-title">授权 {provider}</h2>
          </div>
          <button
            type="button"
            className="provider-auth-dialog__close"
            aria-label={`关闭 ${provider} 授权页`}
            onClick={onClose}
            disabled={status === "pending"}
          >
            ×
          </button>
        </header>

        {status === "success" ? (
          <div className="provider-auth-success" role="status" aria-live="polite">
            <CheckCircle2 aria-hidden="true" />
            <b>{provider} 已授权</b>
            <p>连接状态正在回写，马上返回设备页面。</p>
          </div>
        ) : (
          <>
            <p id="provider-auth-description" className="provider-auth-description">
              {profile.description}
            </p>
            <div className="provider-auth-scopes" aria-label="本次申请读取的数据">
              <span><ShieldCheck aria-hidden="true" />仅申请读取</span>
              <div>{profile.scopes.map((scope) => <b key={scope}><Check aria-hidden="true" />{scope}</b>)}</div>
            </div>

            {mode === "choice" ? (
              <div className="provider-auth-choice">
                <Button
                  data-autofocus
                  disabled={status === "pending"}
                  onClick={() => authorize("native")}
                >
                  {status === "pending" ? <><LoaderCircle className="spin" />正在前往授权页</> : <><Link2 />{profile.nativeAction}</>}
                </Button>
                <div className="provider-auth-divider"><span>或</span></div>
                <Button
                  variant="secondary"
                  disabled={status === "pending"}
                  onClick={() => setMode("account")}
                >
                  <LockKeyhole />使用{profile.accountName}登录
                </Button>
              </div>
            ) : (
              <form
                className="provider-auth-form"
                onSubmit={(event) => {
                  event.preventDefault();
                  authorize("account");
                }}
              >
                <button type="button" className="provider-auth-back" onClick={() => setMode("choice")}>
                  <ChevronLeft />返回一键授权
                </button>
                <label>
                  <span>{profile.accountName}</span>
                  <input
                    data-autofocus
                    type="text"
                    value={account}
                    onChange={(event) => setAccount(event.target.value)}
                    autoComplete="username"
                    placeholder={`手机号、邮箱或${profile.accountName}`}
                    disabled={status === "pending"}
                  />
                </label>
                <label>
                  <span>密码</span>
                  <input
                    type="password"
                    value={password}
                    onChange={(event) => setPassword(event.target.value)}
                    autoComplete="current-password"
                    placeholder="请输入密码"
                    disabled={status === "pending"}
                  />
                </label>
                <Button type="submit" disabled={status === "pending"}>
                  {status === "pending" ? <><LoaderCircle className="spin" />正在登录并授权</> : "登录并授权"}
                </Button>
              </form>
            )}

            <p className="provider-auth-error" role="alert" aria-live="assertive">
              {error}
            </p>
            <footer className="provider-auth-privacy">
              <ShieldCheck aria-hidden="true" />
              <span>不会获取平台密码；正式版由对应平台安全页完成登录，授权可随时撤回。</span>
            </footer>
          </>
        )}
      </section>
    </div>
  );
}

function PermissionPage() {
  const { state, device, back } = useApp();
  const rows = [
    {
      key: "gpsGranted" as const,
      title: "定位与 GPS",
      note: "用于路线、距离和实时配速",
      icon: LocateFixed,
    },
    {
      key: "notificationGranted" as const,
      title: "训练提醒",
      note: "用于前台补给提醒和订阅消息",
      icon: Bell,
    },
  ];
  return (
    <div className="page">
      <PageHeader title="权限说明" />
      <div className="page-intro">
        <h1>只在需要时使用</h1>
        <p>拒绝权限不会阻塞基础浏览，但部分功能将不可用。</p>
      </div>
      {rows.map(({ key, title, note, icon: Icon }) => (
        <Card className="permission-row" key={key}>
          <Icon size={22} />
          <div>
            <b>{title}</b>
            <span>{note}</span>
          </div>
          <button
            role="switch"
            data-auth-required="device"
            aria-checked={state.device[key]}
            className={`switch ${state.device[key] ? "on" : ""}`}
            onClick={() => device({ [key]: !state.device[key] })}
          >
            <span />
          </button>
        </Card>
      ))}
      <Card className="scope-note">
        <LockKeyhole size={21} />
        <p>授权数据只用于训练、恢复和营养建议。你可以随时撤回。</p>
      </Card>
      <Button className="page-primary" onClick={back}>
        保存权限设置
      </Button>
    </div>
  );
}

function HomePage() {
  const {
    state,
    patch,
    sendMessage,
    retryLastMessage,
    refreshSuggestedQuestions,
    go,
    openPlanAgent,
  } = useApp();
  const conversationEndRef = useRef<HTMLDivElement>(null);
  const send = async (text = state.composerDraft, displayText?: string) => {
    await sendMessage(text, displayText);
  };
  const now = new Date();
  const todayIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
  const todayPlan =
    state.planItems.find((item) => item.date === todayIso) ?? null;
  const isRestDay = todayPlan === null;
  const todayPlanReceipt =
    todayPlan && state.planSyncReceipt?.planId === todayPlan.id
      ? state.planSyncReceipt
      : null;
  const todayFuelPlan =
    state.nutritionPlansByDate[todayIso] ?? state.nutritionPlan;
  const todayFuelCompleted =
    state.completedNutritionNodesByDate[todayIso] ??
    state.completedNutritionNodes;
  const todayFuelNodeCount = todayFuelPlan?.nodes.length ?? 3;
  const todayFuelCompletedCount = todayFuelPlan
    ? todayFuelPlan.nodes.filter((node) => todayFuelCompleted.includes(node.id))
        .length
    : 0;
  const todayFuelCompletion = todayFuelNodeCount
    ? Math.round((todayFuelCompletedCount / todayFuelNodeCount) * 100)
    : 0;
  const todayFuelPhases = [
    {
      phase: "训练前" as const,
      time: "前 30 分钟",
      target: "300 ml 水 + 25 g 碳水能量",
    },
    {
      phase: "训练中" as const,
      time: "每 45 分钟",
      target: "能量补给 + 150–250 ml 水",
    },
    {
      phase: "训练后" as const,
      time: "结束后 30 分钟",
      target: "20–25 g 蛋白质 + 碳水能量",
    },
  ].map((fallback) => {
    const nodes =
      todayFuelPlan?.nodes.filter((node) => node.phase === fallback.phase) ?? [];
    const completedCount = nodes.filter((node) =>
      todayFuelCompleted.includes(node.id),
    ).length;
    return {
      ...fallback,
      time: nodes.map((node) => node.time).join(" · ") || fallback.time,
      target: nodes.map((node) => node.target).join(" · ") || fallback.target,
      completedCount,
      nodeCount: nodes.length,
    };
  });
  const recoveryScore = Math.max(45, Math.min(96, 100 - state.recoveryFatigue * 4));
  const recoveryState = recoveryScore >= 78 ? "恢复良好" : recoveryScore >= 62 ? "谨慎训练" : "建议恢复";
  const recoveryAdvice = recoveryScore >= 78
    ? "可以执行今日训练；前段控制强度，后半程按体感维持能量。"
    : "建议采用恢复调整方案，降低主训练负荷并加强补水补能。";
  const frequencyCoachCopy = state.profile.frequency <= 1
    ? "一周一次也能积累进步，先把今天这次稳稳完成。"
    : state.profile.frequency === 2
      ? "先守住每周两次的节奏，稳定比临时加量更重要。"
      : `每周 ${state.profile.frequency} 次安排已经错开强弱，今天只完成该做的，不额外加码。`;
  const dynamicMessages = state.conversation.filter(
    (message) => !["m1", "m2", "m3", "m4"].includes(message.id),
  );
  const showInitialConversation =
    !state.conversationResetAt &&
    state.conversation.some((message) => message.id === "m1");
  const questionContextKey = useMemo(
    () =>
      JSON.stringify({
        profile: state.profile,
        device: {
          status: state.device.status,
          lastSyncAt: state.device.lastSyncAt,
          sources: state.device.syncSources,
          weather: state.device.weather,
          sleepHours: state.device.sleepHours,
          hrvMs: state.device.hrvMs,
          restingHeartRate: state.device.restingHeartRate,
        },
        recovery: [state.recoveryFatigue, state.recoveryUpdatedAt],
        plan: state.planItems.map(({ id, version, status, pace }) => ({
          id,
          version,
          status,
          pace,
        })),
        nutrition: [
          state.hydrationMl,
          state.nutritionCompletion,
          state.completedNutritionNodes,
        ],
        conversationResetAt: state.conversationResetAt,
      }),
    [
      state.profile,
      state.device.status,
      state.device.lastSyncAt,
      state.device.syncSources,
      state.device.weather,
      state.device.sleepHours,
      state.device.hrvMs,
      state.device.restingHeartRate,
      state.recoveryFatigue,
      state.recoveryUpdatedAt,
      state.planItems,
      state.hydrationMl,
      state.nutritionCompletion,
      state.completedNutritionNodes,
      state.conversationResetAt,
    ],
  );
  useEffect(() => {
    if (state.profileComplete && state.planGenerationStatus === "ready")
      void refreshSuggestedQuestions();
  }, [questionContextKey, state.profileComplete, state.planGenerationStatus]);
  useEffect(() => {
    const latestMessage = dynamicMessages.at(-1);
    const planAgentActive = state.planAgentFlow.stage !== "idle";
    if (latestMessage?.card !== "training_performance" && !planAgentActive) return;
    window.requestAnimationFrame(() =>
      conversationEndRef.current?.scrollIntoView({ block: "end" }),
    );
  }, [dynamicMessages.at(-1)?.id, state.planAgentFlow.stage]);
  if (!state.profileComplete)
    return (
      <div className="home-page home-page--v21 page">
        <PageHeader title="CPT AI 教练" back={false} />
        <div className="conversation-timeline home-v21-flow">
          <div className="message-row message-row--agent">
            <CoachAvatar />
            <div className="message-stack">
              <div className="message-bubble">
                很高兴认识你。先用几步让我了解你的目标和身体情况，之后才能给出真正贴合你的训练与补给建议。
              </div>
              <Card className="incomplete-chat-card">
                <CircleGauge />
                <div>
                  <b>先让我更懂你一点</b>
                  <p>
                    完成目标、近期训练、身体感受与补给习惯，大约需要 3 分钟。
                  </p>
                </div>
                <Button onClick={() => go("ONB-01")}>开始建立档案</Button>
              </Card>
            </div>
          </div>
        </div>
      </div>
    );
  return (
    <div className="home-page home-page--v21 page">
      <PageHeader
        title="CPT AI 教练"
        back={false}
        onTitleClick={() => go("AI-04")}
        titleActionLabel="助手说明"
        action={
          <div className="header-actions">
            <button aria-label="历史对话" onClick={() => go("AI-03")}>
              <History size={22} />
            </button>
            <span className="assistant-live-dot" aria-label="在线" />
          </div>
        }
      />
      <div className="home-assistant-status">
        <span
          className={
            state.device.status === "synced" ? "dot-live" : "dot-stale"
          }
        />
        <b>在线</b>
        <span>· 今天也陪你一起练</span>
        <small>陪伴型 AI 跑步教练</small>
      </div>
      <div className="conversation-timeline home-v21-flow">
        {state.network === "offline" && (
          <div className="offline-banner">
            <Database size={16} /> 网络暂时离开，先为你保留本地内容{" "}
            <button onClick={() => patch({ network: "online" })}>
              再次连接
            </button>
          </div>
        )}
        {showInitialConversation && state.planGenerationStatus === "ready" && (
          <>
            <div className="home-time-separator">今天&nbsp; 07:10</div>
            <div className="message-row message-row--agent home-intro-message">
              <CoachAvatar />
              <div className="message-stack">
                <div className="message-bubble">
                  {isRestDay
                    ? "数据已经同步。今天是计划休息日，放心慢下来，把恢复做好也是训练的一部分。"
                    : `状态看过了。${frequencyCoachCopy} 我们按今天的节奏来。`}
                </div>
              </div>
            </div>
            <div className="message-row message-row--agent home-plan-message">
              <CoachAvatar />
              <div className="message-stack">
                <Card className="home-coach-card home-decision-card">
                  <div className="home-decision-kicker">
                    <span>今天怎么练</span>
        <small><Watch size={13} /> 健康平台 · 训练后同步</small>
                  </div>
                  <div className="home-coach-card__head">
                    <div>
                      <small>恢复状态</small>
                      <h2>{isRestDay ? "今天优先恢复" : recoveryState}</h2>
                    </div>
                    <strong>{recoveryScore}<small> / 100</small></strong>
                  </div>
                  <p className="home-recovery-advice">{recoveryAdvice}</p>
                  <div className="readiness-track" aria-label={`恢复准备度 ${recoveryScore} 分`}>
                    <span style={{ width: `${recoveryScore}%` }} />
                  </div>
                  <div className="home-recovery-basis">
                    <span>睡眠 {state.device.sleepHours ?? "--"}h</span>
                    <span>HRV {state.device.hrvMs ?? "--"}ms</span>
                    <span>静息心率 {state.device.restingHeartRate ?? "--"}bpm</span>
                  </div>
                  {todayPlan ? (
                    <>
                      <div className="home-session-card home-session-card--compact">
                        <div className="home-session-title">
                          <img src="/running-shoe-unbranded.png" alt="跑鞋" />
                          <div>
                            <small>今日训练结构</small>
                            <h3>{todayPlan.title}</h3>
                          </div>
                          <StatusBadge
                            tone={todayPlanReceipt?.kind === "adjusted" ? "blue" : todayPlan.status === "confirmed" ? "green" : "gray"}
                          >
                            {todayPlanReceipt?.kind === "adjusted"
                              ? "AI 教练已调整"
                              : todayPlan.status === "confirmed"
                                ? "已确认"
                                : "待确认"}
                          </StatusBadge>
                        </div>
                        <div className="home-session-metrics">
                          <span><b>{todayPlan.distance.toFixed(1)} km</b>距离</span>
                          <span><b>{todayPlan.pace}/km</b>目标配速</span>
                          <span><b>{todayPlan.duration}</b>预计用时</span>
                        </div>
                      </div>
                      <div className="home-plan-choice" aria-label="今日训练方案操作">
                        <button onClick={() => go("PLAN-04")}>查看当前方案</button>
                        <button
                          className="is-selected"
                          onClick={() =>
                            openPlanAgent(
                              "HOME-01",
                              todayPlan.id,
                              todayPlan.title,
                            )
                          }
                        >和 AI 教练商量</button>
                      </div>
                    </>
                  ) : (
                    <div className="home-rest-copy">今天是恢复日，不急着加码。补好水、吃够蛋白质，今晚早点休息。</div>
                  )}
                </Card>
              </div>
            </div>
            {todayPlan && (
              <div className="message-row message-row--agent home-fueling-message">
                <CoachAvatar />
                <div className="message-stack">
                  <div className="message-bubble">
                    今天怎么补，我也按训练节奏单独排好了。出发前扫一眼，训练中照着时间点执行就好。
                  </div>
                  <Card className="home-fueling-cycle">
                    <div className="home-fueling-cycle__head">
                      <span><Droplets aria-hidden="true" /> 今日训练补给</span>
                      <StatusBadge tone={todayFuelCompletion > 0 ? "green" : "gray"}>
                        已完成 {todayFuelCompletedCount}/{todayFuelNodeCount}
                      </StatusBadge>
                    </div>
                    <p className="home-fueling-cycle__intro">
                      训练前、中、后三段都已展开，完成情况会同步到训练分析。
                    </p>
                    <div className="home-fueling-cycle__phases">
                      {todayFuelPhases.map((item, index) => {
                        const isComplete =
                          item.nodeCount > 0 && item.completedCount === item.nodeCount;
                        const isPartial =
                          item.completedCount > 0 && item.completedCount < item.nodeCount;
                        return (
                          <article className="home-fueling-phase" key={item.phase}>
                            <b>{index + 1}</b>
                            <span>
                              {item.phase}
                              <small>{item.time}</small>
                            </span>
                            <strong>{item.target}</strong>
                            <StatusBadge tone={isComplete ? "green" : isPartial ? "orange" : "gray"}>
                              {isComplete
                                ? "已完成"
                                : isPartial
                                  ? `${item.completedCount}/${item.nodeCount}`
                                  : "待执行"}
                            </StatusBadge>
                          </article>
                        );
                      })}
                    </div>
                    <button
                      type="button"
                      className="home-fueling-cycle__action"
                      onClick={() => go("NUT-01")}
                    >
                      查看完整方案并记录 <ChevronRight aria-hidden="true" />
                    </button>
                  </Card>
                </div>
              </div>
            )}
          </>
        )}
        {state.planGenerationStatus !== "ready" && (
          <div className="message-row message-row--agent plan-pending-message">
            <CoachAvatar />
            <div className="message-stack">
              <div className="message-bubble">
                你的档案已经准备好。什么时候想开始，告诉我“帮我生成训练计划”，我们就从这里出发。
              </div>
              <Card className="incomplete-chat-card">
                <Sparkles />
                <div>
                  <b>下一段训练，等你一起定</b>
                  <p>
                    AI 教练会结合你的完整档案与已授权数据生成新方案，不会拿旧计划凑数。
                  </p>
                </div>
                <Button
                  disabled={state.planGenerationStatus === "loading"}
                  onClick={() => send("请根据我的完整档案生成训练计划")}
                >
                  {state.planGenerationStatus === "loading" ? (
                    <>
                      <LoaderCircle className="spin" /> 正在为你排计划
                    </>
                  ) : (
                    "和 AI 教练一起规划"
                  )}
                </Button>
                <Button
                  variant="secondary"
                  onClick={() => go("PROFILE-01", "profile")}
                >
                  先补充我的档案
                </Button>
              </Card>
            </div>
          </div>
        )}
        {!showInitialConversation && dynamicMessages.length === 0 && (
          <EmptyState
            title="想聊点什么？"
            detail="可以直接说出今天的状态，也可以从下方选一个问题，我们重新开始。"
          />
        )}
        {dynamicMessages.map((message) => (
          <ConversationMessage key={message.id} message={message} />
        ))}
        <PlanAgentFlowPanel />
        <div ref={conversationEndRef} className="conversation-end-anchor" />
        {(state.agentFailure || state.agentRetrying) && (
          <Card className="error-inline">
            <AlertTriangle />
            <div>
              <b>
                {state.agentRetrying
                  ? "正在重新连接 AI 教练"
                  : "连接暂时中断"}
              </b>
              <p>你的输入已经替你保存，恢复后可以继续发送同一个问题。</p>
            </div>
            <button
              disabled={state.agentRetrying}
              onClick={() => void retryLastMessage()}
            >
              {state.agentRetrying ? "正在连接…" : "再次连接"}
            </button>
          </Card>
        )}
        {state.planGenerationStatus === "ready" && (
          <section
            className="agent-question-suggestions"
            aria-labelledby="agent-question-title"
            aria-busy={state.suggestedQuestionsStatus === "loading"}
          >
            <div className="agent-question-suggestions__head">
              <div>
                <b id="agent-question-title">接下来想聊什么</b>
                <span>
                  AI 教练根据你今天的数据整理
                  {state.suggestedQuestionsUpdatedAt
                    ? ` · ${state.suggestedQuestionsUpdatedAt}`
                    : ""}
                </span>
              </div>
              <button
                aria-label="重新生成快捷问题"
                disabled={state.suggestedQuestionsStatus === "loading"}
                onClick={() => void refreshSuggestedQuestions()}
              >
                <RefreshCw size={14} />
              </button>
            </div>
            {state.suggestedQuestionsStatus === "loading" && (
              <div
                className="quick-replies quick-replies--loading"
                role="status"
              >
                <span />
                <span />
                <span>正在看看你今天最需要什么</span>
              </div>
            )}
            {state.suggestedQuestionsStatus === "error" && (
              <Card className="error-inline" role="alert">
                <AlertTriangle />
                <div>
                  <b>暂时没能更新问题</b>
                  <p>现有对话不受影响，可以再次尝试。</p>
                </div>
                <button onClick={() => void refreshSuggestedQuestions()}>
                  再试一次
                </button>
              </Card>
            )}
            {state.suggestedQuestionsStatus === "ready" && (
              <div className="quick-replies">
                {state.suggestedQuestions.map((question) => (
                  <button
                    key={question.id}
                    title={`生成依据：${question.reason}`}
                    onClick={() => send(question.prompt, question.label)}
                  >
                    {question.label}
                  </button>
                ))}
              </div>
            )}
          </section>
        )}
      </div>
      <Composer
        value={state.composerDraft}
        onChange={(value) => patch({ composerDraft: value })}
        onSend={() => send()}
      />
    </div>
  );
}

function PlanAgentFlowPanel() {
  const { state, sendMessage, go } = useApp();
  const flow = state.planAgentFlow;
  if (flow.stage === "idle" || flow.stage === "collecting") return null;
  if (flow.stage === "generating" || flow.stage === "applying")
    return (
      <Card className="plan-agent-flow plan-agent-flow--loading" role="status">
        <LoaderCircle className="spin" />
        <div>
          <b>{flow.stage === "generating" ? "正在一起推演调整影响" : "正在把新计划同步到训练中心"}</b>
              <p>{flow.stage === "generating" ? "我会守住已完成训练，只调整生效日后的负荷与补给。" : "计划、日期和补给节点正在对齐，也会同步更新已连接平台与设备的课表状态。"}</p>
        </div>
      </Card>
    );
  if (flow.stage === "synced")
    return (
      <Card className="plan-agent-flow plan-agent-flow--synced">
        <CheckCircle2 />
        <div>
          <b>调整好了，训练界面已更新</b>
          <p>计划、日期和补给节点已经对齐，接下来按新方案走。</p>
        </div>
        <button onClick={() => go("TRAIN-HUB", "training")}>查看训练界面</button>
      </Card>
    );
  if (!flow.preview) return null;
  const addedItems = flow.preview.items.filter(
    (item) => !state.planItems.some((current) => current.id === item.id),
  );
  return (
    <Card className="plan-agent-flow plan-agent-flow--preview">
      <div className="plan-agent-flow__head">
        <span><Sparkles /> AI 教练调整建议</span>
        <StatusBadge tone="orange">等你确认</StatusBadge>
      </div>
      <p>{flow.preview.reason}。只调整以下训练，其他周期安排保持不变。</p>
      <div className="plan-agent-flow__changes">
        {flow.preview.changes.map((change) => {
          const item = state.planItems.find((plan) => plan.id === change.id);
          return (
            <div key={change.id}>
              <b>{item?.title ?? "训练安排"}</b>
              <span>{change.before.date}<ArrowRight />{change.after.date}</span>
              <small>{change.before.pace} / 负荷 {change.before.load}<ArrowRight />{change.after.pace} / 负荷 {change.after.load}</small>
            </div>
          );
        })}
        {addedItems.length > 0 && (
          <div className="plan-agent-flow__addition">
            <b>新增每周训练课次</b>
            <span>{addedItems.length} 次后续训练<ArrowRight />从生效日起加入</span>
            <small>已完成训练不变 · 新增课次以低强度有氧为主</small>
          </div>
        )}
      </div>
      <div className="plan-agent-flow__actions">
        <Button variant="secondary" onClick={() => void sendMessage("保持原计划")}>保留原计划</Button>
        <Button onClick={() => void sendMessage("应用这个调整", "应用变更")}>确认并同步</Button>
      </div>
      <small className="plan-agent-flow__hint">还想再改一点？继续告诉我就好。</small>
    </Card>
  );
}

function TrainingPerformanceReportBody({
  activity,
  plan,
  analysis,
}: {
  activity: ActivityRecord;
  plan?: PlanItem | null;
  analysis?: TrainingAnalysisData | null;
}) {
  const metrics = buildSessionPerformance(activity, plan);
  const matched = analysis?.rows.filter((row) => row.status === "met").length ?? 0;
  return (
    <div className="training-performance-report-body">
      <div className="training-performance-metrics training-performance-metrics--expanded">
        <div><span>距离</span><strong>{activity.distance.toFixed(2)}<small> km</small></strong></div>
        <div><span>平均配速</span><strong>{activity.pace}<small>/km</small></strong></div>
        <div><span>平均心率</span><strong>{activity.heartRate}<small> bpm</small></strong></div>
        <div><span>训练负荷</span><strong>{metrics.trainingLoad}<small> load</small></strong></div>
        <div><span>平均步频</span><strong>{metrics.cadence}<small> spm</small></strong></div>
        <div><span>本次 VDOT</span><strong>{metrics.vdot}</strong></div>
      </div>
      <div className="training-performance-function-summary">
        <div>
          <span>本次机能情况</span>
          <b>{metrics.trainingLoad <= metrics.recommendedLoad[1] ? "刺激有效，整体在个人可承受区间" : "刺激偏高，恢复需要优先安排"}</b>
        </div>
        <p>
          平均步频 {metrics.cadence} spm、步长 {metrics.strideLength} m；预计恢复 {metrics.recoveryHours} 小时。
          配速与心率后程保持稳定，训练刺激已被完整记录。
        </p>
      </div>
      <div className="training-performance-match">
        <div><span>计划执行</span><b>{plan?.title ?? "本次训练"}</b></div>
        <strong>{matched}<small> / {analysis?.rows.length ?? 5} 项达标</small></strong>
      </div>
      {analysis?.headline && <h3 className="training-performance-headline">{analysis.headline}</h3>}
      <p className="training-performance-summary">
        {analysis?.summary ?? "训练主体已完成，补充主观感受后可获得更完整的恢复与后续训练建议。"}
      </p>
      {analysis && (
        <div className="training-performance-insights">
          <div>
            <span>关键依据</span>
            <ul>{analysis.evidence.slice(0, 3).map((item) => <li key={item}>{item}</li>)}</ul>
          </div>
          <div><span>下一步建议</span><p>{analysis.adjustment}</p></div>
        </div>
      )}
    </div>
  );
}

function ConversationMessage({
  message,
}: {
  message: ReturnType<typeof useApp>["state"]["conversation"][number];
}) {
  const { state, patch, go, openPlanAgent, openNutritionFeedback } = useApp();
  const plan = state.planItems[0];
  const performanceActivity =
    state.activities.find((item) => item.id === message.activityId) ??
    state.activities.find((item) => item.id === state.lastCompletedActivityId) ??
    state.activities.find((item) => item.date === state.selectedDate) ??
    state.activities[0];
  const performancePlan =
    state.planItems.find((item) => item.id === performanceActivity?.planItemId) ??
    state.planItems.find((item) => item.date === performanceActivity?.date) ??
    state.planItems[0];
  const performanceAnalysis = message.analysis ?? state.trainingAnalysis;
  const checkInActivity =
    state.activities.find((item) => item.id === message.activityId) ??
    state.activities.find((item) => item.id === state.lastCompletedActivityId) ??
    null;
  const checkInState = checkInActivity
    ? state.postTrainingFeedbackByActivity[checkInActivity.id]
    : null;
  const checkInComplete = Boolean(
    checkInState?.training && checkInState?.nutrition,
  );
  const plainAgentAnswer = message.role === "agent" && !message.card;
  return (
    <div className={`message-row message-row--${message.role} conversation-message ${plainAgentAnswer ? "conversation-message--answer" : ""} ${message.card ? "conversation-message--with-card" : ""}`}>
      {message.role === "agent" ? (
        <CoachAvatar />
      ) : (
        <div className="message-avatar">我</div>
      )}
      <div className="message-stack">
        {plainAgentAnswer ? (
          <article className="home-agent-answer">
            <header>
              <span><Activity aria-hidden="true" />CPT 回答</span>
              <time>{message.time}</time>
            </header>
            <p>{message.text}</p>
            {message.sources && message.sources.length > 0 && (
              <footer title={message.contextVersion ? `数据版本：${message.contextVersion}` : undefined}>
                <span>依据</span>
                <p>{message.sources.slice(0, 4).join(" · ")}</p>
              </footer>
            )}
          </article>
        ) : (
          <>
            <span className="message-meta">
              {message.role === "agent" ? "跑步助手" : "我"} · {message.time}
            </span>
            <div className="message-bubble">{message.text}</div>
          </>
        )}
        {message.card === "recovery" && (
          <Card className="agent-card recovery-card">
            <div className="agent-card__head">
              <span>
                <HeartPulse /> 恢复状态
              </span>
              <StatusBadge tone="green">良好</StatusBadge>
            </div>
            <div className="metric-grid">
              <Metric value={state.recoveryFatigue} unit="/20" label="疲劳感" />
              <Metric value="7.2" unit="h" label="睡眠" />
              <Metric value="58" unit="ms" label="HRV" />
            </div>
            <Button variant="secondary" onClick={() => go("RECOVERY-01")}>
              查看恢复分析
            </Button>
          </Card>
        )}
        {message.card === "training" && (
          <Card className="agent-card training-card">
            <div className="agent-card__head">
              <span>
                <Footprints /> 今日训练
              </span>
              <b>{plan.title}</b>
            </div>
            <div className="training-brief">
              <div>
                <strong>
                  {plan.distance}
                  <small> km</small>
                </strong>
                <span>目标距离</span>
              </div>
              <div>
                <strong>
                  {plan.pace}
                  <small>/km</small>
                </strong>
                <span>目标配速</span>
              </div>
            </div>
            <div className="dual-actions">
              <Button
                onClick={() => {
                  go("TRAIN-READY");
                }}
              >
                前往设备确认
              </Button>
              <Button
                variant="secondary"
                onClick={() => openPlanAgent("PLAN-05", plan.id, plan.title)}
              >
                调整计划
              </Button>
            </div>
          </Card>
        )}
        {message.card === "nutrition" && (
          <Card className="agent-card nutrition-card">
            <div className="agent-card__head">
              <span>
                <Droplets /> 营养与补给
              </span>
              <b>补液 · 补能</b>
            </div>
            <div className="fuel-strip">
              <span>
                <Droplets size={16} />
                训练前 300 ml
              </span>
              <span>
                <Zap size={16} />第 45 分钟 1 支
              </span>
            </div>
            <Button variant="secondary" onClick={() => go("NUT-01")}>
              查看今日补给
            </Button>
          </Card>
        )}
        {message.card === "post_training_checkin" && checkInActivity && (
          <Card className="agent-card post-training-checkin-card">
            <div className="post-training-checkin-card__head">
              <span><Watch aria-hidden="true" /> 发现设备训练记录</span>
              <StatusBadge tone={checkInComplete ? "green" : "orange"}>
                {checkInComplete ? "分析已解锁" : "待补充反馈"}
              </StatusBadge>
            </div>
            <div className="post-training-checkin-card__metrics">
              <span><b>{checkInActivity.distance.toFixed(2)} km</b>距离</span>
              <span><b>{checkInActivity.pace}/km</b>平均配速</span>
              <span><b>{checkInActivity.heartRate} bpm</b>平均心率</span>
            </div>
            <p className="post-training-checkin-card__intro">
              客观数据已经保存。再补齐下面两项，我会在首页和训练记录中生成完整分析。
            </p>
            <div className="post-training-checkin-card__steps" aria-label="完整训练分析解锁进度">
              <button
                type="button"
                className={checkInState?.training ? "is-complete" : ""}
                disabled={checkInState?.training}
                onClick={() => {
                  patch({
                    lastCompletedActivityId: checkInActivity.id,
                    selectedDate: checkInActivity.date,
                  });
                  go("TRAIN-FEEDBACK");
                }}
              >
                <span>{checkInState?.training ? <CheckCircle2 /> : <MessageCircleMore />}</span>
                <b>训练感受</b>
                <small>{checkInState?.training ? "已完成" : "告诉我身体的感受"}</small>
                {!checkInState?.training && <ChevronRight />}
              </button>
              <button
                type="button"
                className={checkInState?.nutrition ? "is-complete" : ""}
                disabled={checkInState?.nutrition}
                onClick={() => openNutritionFeedback("HOME-01")}
              >
                <span>{checkInState?.nutrition ? <CheckCircle2 /> : <Droplets />}</span>
                <b>补给执行</b>
                <small>{checkInState?.nutrition ? "已完成" : "直接在对话里反馈"}</small>
                {!checkInState?.nutrition && <ChevronRight />}
              </button>
            </div>
            <div className="post-training-checkin-card__progress">
              <span style={{ width: `${(Number(checkInState?.training) + Number(checkInState?.nutrition)) * 50}%` }} />
            </div>
            {checkInComplete ? (
              <Button
                onClick={() => {
                  patch({ selectedDate: checkInActivity.date });
                  go("RECORD-03");
                }}
              >
                查看完整训练分析 <ChevronRight />
              </Button>
            ) : (
              <small className="post-training-checkin-card__hint">
                当前训练记录页只展示设备数据，完整分析将在 2/2 后出现。
              </small>
            )}
          </Card>
        )}
        {message.card === "biomarker" && (
          <Card className="agent-card biomarker-card">
            <div className="agent-card__head">
              <span>
                <FileChartColumn /> 生理生化指标全分析
              </span>
              <StatusBadge tone="blue">运动范围</StatusBadge>
            </div>
            <h3>本次报告对训练与恢复的影响</h3>
            <p>
              <b>血红蛋白 148 g/L：</b>
              位于报告参考区间，当前耐力训练无需因该指标主动降量。
            </p>
            <p>
              <b>铁蛋白 96 μg/L：</b>
              与当前训练量相匹配，继续结合疲劳感和周期负荷观察。
            </p>
            <p>
              <b>维生素 D 28.6 ng/mL：</b>
              可能影响恢复稳定性；仅建议从日常饮食与合规运动营养品补充角度关注。
            </p>
            <SafetyBanner>
              仅分析运动训练、生理状态与运动营养，不提供医疗建议或治疗方案；如有身体不适请寻求医疗帮助。
            </SafetyBanner>
          </Card>
        )}
        {message.card === "training_feedback_prompt" && (
          <Card className="agent-card training-feedback-prompt">
            <div className="agent-card__head">
              <span>
              <ClipboardCheck /> 再聊聊这次训练的感受
              </span>
            <StatusBadge tone="orange">等你说说</StatusBadge>
            </div>
          <p>告诉我体感、口渴、酸痛和肠胃感受，我会把身体的声音和训练数据放在一起看。</p>
            <Button onClick={() => go("TRAIN-FEEDBACK")}>
            去聊聊这次训练 <ChevronRight />
            </Button>
          </Card>
        )}
        {message.card === "training_performance" && performanceActivity && (
          <Card className="agent-card training-performance-card">
            <div className="training-performance-card__head">
              <div>
                <span><Activity /> POST-RUN REPORT</span>
                <b>本次训练表现</b>
              </div>
              <StatusBadge tone="green">已完成</StatusBadge>
            </div>
            <TrainingPerformanceReportBody
              activity={performanceActivity}
              plan={performancePlan}
              analysis={performanceAnalysis}
            />
            <div className="training-performance-actions">
              {!state.feedback.submitted && (
                <Button variant="secondary" onClick={() => go("TRAIN-FEEDBACK")}>
                  补充训练感受
                </Button>
              )}
              <Button onClick={() => go("TRAIN-ANALYSIS")}>
                查看完整分析 <ChevronRight />
              </Button>
            </div>
          </Card>
        )}
        {message.card === "safety" && (
          <SafetyBanner>
            建议暂停训练并寻求医疗建议。本助手不会继续给出训练或营养品建议。
          </SafetyBanner>
        )}
      </div>
      <TimelineRail tone={message.card === "nutrition" ? "orange" : "green"} />
    </div>
  );
}

function RecoveryPage() {
  const { state, patch, sendAnalysisToHome, syncDevice } = useApp();
  const update = async () => {
    await syncDevice();
    patch({
      conversation: [
        ...state.conversation,
        {
          id: crypto.randomUUID(),
          role: "agent",
          text: `恢复状态已在刚刚更新：主观疲劳 ${state.recoveryFatigue}/20，设备数据已重新同步。`,
          time: "刚刚",
          card: "recovery",
        },
      ],
      toast: "恢复状态已更新并同步到首页对话卡片",
    });
  };
  return (
    <div className="page">
      <PageHeader title="恢复分析" />
      <div className="recovery-hero">
        <span>今日准备度</span>
        <strong>良好</strong>
        <p>适合按计划训练，保持热身和补液。</p>
      </div>
      <Card className="metric-panel">
        <div className="metric-grid">
          <Metric
            value={state.device.sleepHours ?? "--"}
            unit={state.device.sleepHours == null ? "" : "h"}
            label="睡眠"
          />
          <Metric
            value={state.device.hrvMs ?? "--"}
            unit={state.device.hrvMs == null ? "" : "ms"}
            label="HRV"
          />
          <Metric
            value={state.device.restingHeartRate ?? "--"}
            unit={state.device.restingHeartRate == null ? "" : "bpm"}
            label="静息心率"
          />
        </div>
        <div className="data-source">
          <Database size={14} />{" "}
          {state.device.syncSources.join("、") || "无设备"} ·{" "}
          {state.recoveryUpdatedAt}
          <button aria-label="同步恢复设备数据" onClick={syncDevice}>
            <RefreshCw size={14} />
          </button>
        </div>
      </Card>
      <Card>
        <Score20Slider
          label="主观疲劳感"
          value={state.recoveryFatigue}
          onChange={(recoveryFatigue) => patch({ recoveryFatigue })}
          hint="1–5 几乎不疲劳 · 6–10 轻度 · 11–15 明显 · 16–20 非常疲劳"
        />
      </Card>
      <AiSummary
        summary="睡眠、HRV 与静息心率支持中等强度训练，主观疲劳处于轻度区间。"
        onClick={() =>
          sendAnalysisToHome(
            "恢复状态深度解读",
            "RECOVERY-01",
            `设备睡眠、HRV、静息心率与主观疲劳 ${state.recoveryFatigue}/20 联合评估，暂无需要降低训练强度的证据。`,
          )
        }
      />
      <Button className="page-primary" data-auth-required="device" onClick={update}>
        <RefreshCw size={16} /> 更新恢复状态
      </Button>
    </div>
  );
}

function ReminderPage() {
  const { state, patch, back } = useApp();
  const [time, setTime] = useState("18:30");
  const [saving, setSaving] = useState(false);
  const plan =
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  return (
    <div className="page">
      <PageHeader title="稍后提醒" />
      <div className="page-intro">
        <h1>本次训练准备延后到什么时候？</h1>
        <p>这里只设置“开始本次训练”的延后提醒，不会更改补液、补能节点。</p>
      </div>
      <div className="choice-grid reminder-presets">
        {["30 分钟后", "今天 18:30", "明早 07:00"].map((x) => (
          <button
            key={x}
            onClick={() => setTime(x)}
            className={time === x ? "selected" : ""}
          >
            {x}
          </button>
        ))}
      </div>
      <label className="field">
        <span>自定义时间</span>
        <input
          type="time"
          value={/^\d{2}:\d{2}$/.test(time) ? time : "18:30"}
          onChange={(e) => setTime(e.target.value)}
        />
      </label>
      <Card className="channel-card">
        <Bell />
        <div>
          <b>提醒通道</b>
          <p>小程序前台页内提醒；后台消息为尽力送达，不承诺秒级。</p>
        </div>
      </Card>
      <Button
        className="page-primary"
        data-auth-required="notification"
        disabled={saving}
        onClick={async () => {
          setSaving(true);
          try {
            const result = await services.plan.scheduleStartReminder(plan.id, time);
            patch({
              trainingStartReminder: {
                planId: plan.id,
                displayTime: time,
                scheduledAt: result.scheduledAt,
                status: "scheduled",
              },
              toast: `本次训练已延后，将在 ${time} 提醒开始`,
            });
            back();
          } catch {
            patch({ toast: "训练延后提醒设置失败，请重试" });
          } finally {
            setSaving(false);
          }
        }}
      >
        {saving ? "正在设置…" : "确认延后提醒"}
      </Button>
    </div>
  );
}

function PlanWizard() {
  const { state, generatePlan, go } = useApp();
  return (
    <div className="page">
      <PageHeader title="生成训练计划" />
      <div className="page-intro">
        <h1>把你的训练节奏定下来</h1>
        <p>
          我会结合最新档案、设备数据和恢复状态，从今天开始排出真正能执行的周期计划。
        </p>
      </div>
      <Card className="summary-list">
        <SummaryRow
          icon={<Trophy />}
          title="训练目标"
          value={state.profile.goal}
        />
        <SummaryRow
          icon={<CalendarCheck />}
          title="频率"
          value={`每周 ${state.profile.frequency} 次`}
        />
        <SummaryRow
          icon={<Footprints />}
          title="跑步经验"
          value={state.profile.experience}
        />
        <SummaryRow
          icon={<Droplets />}
          title="出汗情况"
          value={
            ["", "少汗", "轻微", "明显", "大量", "湿透"][state.profile.sweat]
          }
        />
      </Card>
      <Card className="agent-source-card">
        <Bot />
        <div>
          <b>这份计划为什么适合你</b>
          <p>
            输入：个人档案、周跑量、出汗程度、授权设备、恢复数据和可训练日。
          </p>
        </div>
      </Card>
      <Button
        disabled={state.planGenerationStatus === "loading"}
        className="page-primary"
        onClick={async () => {
          if (await generatePlan()) go("PLAN-03");
        }}
      >
        {state.planGenerationStatus === "loading" ? (
          <>
            <LoaderCircle className="spin" /> 正在为你排计划
          </>
        ) : (
          <>
            <Sparkles size={17} /> 生成我的训练计划
          </>
        )}
      </Button>
      {state.planGenerationStatus === "error" && (
        <p className="blocking-note">暂时没能生成新计划。旧计划没有被覆盖，请检查网络后再试。</p>
      )}
    </div>
  );
}

function PlanPreview() {
  const { state, patch, go } = useApp();
  if (state.planGenerationStatus !== "ready")
    return (
      <div className="page">
        <PageHeader title="计划草案" />
        <EmptyState
          title="计划还在路上"
          detail="回到上一步，让 AI 教练根据最新档案重新生成。"
          action={<Button onClick={() => go("PLAN-02")}>重新生成计划</Button>}
        />
      </div>
    );
  const previewWeek = Math.max(1, state.planProgram.currentWeek);
  const previewItems = state.planItems.filter((item) => (item.weekIndex ?? 1) === previewWeek);
  return (
    <div className="page">
      <PageHeader title="计划草案" />
      <div className="version-banner">
        <span>Agent 草案 · {state.planGeneratedAt}</span>
        <b>
          {state.planProgram.title} · 第 {state.planProgram.currentWeek} 周
        </b>
        <small>尚未生效</small>
      </div>
      <Card className="agent-plan-basis">
        <Bot />
        <div>
          <b>{state.planProgram.summary}</b>
          <p>{state.planProgram.basis.join(" · ")}</p>
        </div>
      </Card>
      <div className="plan-list">
        {previewItems.map((item) => (
          <PlanCard key={item.id} item={{ ...item, status: "draft" }} />
        ))}
      </div>
      <Card className="risk-note">
        <AlertTriangle />
        <div>
          <b>Agent 风险控制</b>
          <p>避免连续高强度；恢复或设备数据变化后需重新评估。</p>
        </div>
      </Card>
      <div className="bottom-actions">
        <Button variant="secondary" onClick={() => go("PLAN-02")}>
          重新生成
        </Button>
        <Button
          onClick={() => {
            patch({
              planItems: state.planItems.map((p) => ({
                ...p,
                status: "confirmed" as const,
              })),
              toast: `计划 V${state.planVersion} 已经定下来了，训练中心和手表课表都已同步`,
            });
            go("TRAIN-HUB", "training");
          }}
        >
          确认并使用这份计划
        </Button>
      </div>
    </div>
  );
}

function buildTrainingCycleStages(totalWeeks: number) {
  return [
    { title: "有氧基础期", detail: "建立稳定跑量与动作习惯" },
    { title: "心肺提升期", detail: "逐步增加有氧与阈值刺激" },
    { title: "专项强化期", detail: "靠近目标配速并提升专项耐力" },
    { title: "减量恢复期", detail: "降低负荷，保留状态迎接目标日" },
  ]
    .map((stage, stageIndex) => ({
      ...stage,
      weeks: Array.from({ length: totalWeeks }, (_, index) => index + 1).filter(
        (week) =>
          Math.min(3, Math.floor(((week - 1) * 4) / totalWeeks)) === stageIndex,
      ),
    }))
    .filter((stage) => stage.weeks.length > 0);
}

function TrainingHub() {
  const { state, patch, go, syncDevice } = useApp();
  const [scheduleScope, setScheduleScope] = useState<"week" | "cycle" | "status">("week");
  const [recordMonth, setRecordMonth] = useState(() =>
    state.selectedDate.slice(0, 7),
  );
  const records = state.activities;
  const todayIso = new Date().toISOString().slice(0, 10);
  const sortedPlans = [...state.planItems].sort((a, b) =>
    a.date.localeCompare(b.date),
  );
  const selectedPlan =
    sortedPlans.find((item) => item.date === state.selectedDate) ?? null;
  const cycleStart = new Date(
    `${state.planProgram.startDate ?? sortedPlans[0]?.date ?? state.selectedDate}T12:00:00`,
  );
  const selectedDay = new Date(`${state.selectedDate}T12:00:00`);
  const selectedWeek = Math.min(
    state.planProgram.totalWeeks,
    Math.max(
      1,
      Math.floor((selectedDay.getTime() - cycleStart.getTime()) / 604800000) +
        1,
    ),
  );
  const selectedWeekFeature =
    selectedWeek === state.planProgram.totalWeeks
      ? "减量周"
      : selectedWeek <= 2
        ? "适应周"
        : "建设周";
  const weekItems = sortedPlans.filter(
    (item) => (item.weekIndex ?? 1) === selectedWeek,
  );
  const completedIds = new Set([
    ...state.activities.map((activity) => activity.planItemId),
    ...state.planItems
      .filter((item) => item.status === "completed")
      .map((item) => item.id),
  ]);
  const completedCount = weekItems.filter((item) =>
    completedIds.has(item.id),
  ).length;
  const completionPercent = weekItems.length
    ? Math.round((completedCount / weekItems.length) * 100)
    : 0;
  const dueItems = sortedPlans.filter(
    (item) =>
      item.date <= todayIso &&
      !["draft", "proposed"].includes(item.status) &&
      item.executionSyncStatus !== "pending",
  );
  const dueCompletedCount = dueItems.filter((item) =>
    completedIds.has(item.id),
  ).length;
  const dueCompletionPercent = dueItems.length
    ? Math.round((dueCompletedCount / dueItems.length) * 100)
    : 0;
  const revisionBoundaryPercent = state.planRevision
    ? Math.min(
        100,
        Math.max(
          0,
          (sortedPlans.filter(
            (item) => item.date < state.planRevision!.effectiveDate,
          ).length /
            Math.max(1, sortedPlans.length)) *
            100,
        ),
      )
    : 0;
  const weekLoad = weekItems.reduce((sum, item) => sum + item.load, 0);
  const weekDistance = weekItems.reduce((sum, item) => sum + item.distance, 0);
  const cycleStages = buildTrainingCycleStages(state.planProgram.totalWeeks);
  const cyclePlanIds = new Set(sortedPlans.map((item) => item.id));
  const cycleActivities = records.filter((record) =>
    cyclePlanIds.has(record.planItemId),
  );
  const cyclePlannedDistance = sortedPlans.reduce(
    (sum, item) => sum + item.distance,
    0,
  );
  const cycleActualDistance = cycleActivities.reduce(
    (sum, record) => sum + record.distance,
    0,
  );
  const cycleDistancePercent = cyclePlannedDistance
    ? Math.min(100, Math.round((cycleActualDistance / cyclePlannedDistance) * 100))
    : 0;
  const cycleMileageComparison = cycleStages.map((stage) => {
    const stagePlans = sortedPlans.filter((item) =>
      stage.weeks.includes(item.weekIndex ?? 1),
    );
    const stagePlanIds = new Set(stagePlans.map((item) => item.id));
    return {
      phase: stage.title,
      plan: Number(
        stagePlans.reduce((total, item) => total + item.distance, 0).toFixed(1),
      ),
      actual: Number(
        cycleActivities
          .filter((record) => stagePlanIds.has(record.planItemId))
          .reduce((total, record) => total + record.distance, 0)
          .toFixed(1),
      ),
    };
  });
  const monthRecords = useMemo(
    () =>
      records
        .filter((record) => record.date.startsWith(recordMonth))
        .sort((a, b) => b.date.localeCompare(a.date)),
    [recordMonth, records],
  );
  const monthDistance = monthRecords.reduce(
    (total, record) => total + record.distance,
    0,
  );
  const monthDurationMinutes = monthRecords.reduce((total, record) => {
    const parts = record.duration.split(":").map(Number);
    const seconds =
      parts.length === 3
        ? parts[0] * 3600 + parts[1] * 60 + parts[2]
        : parts.length === 2
          ? parts[0] * 60 + parts[1]
          : 0;
    return total + Math.round(seconds / 60);
  }, 0);
  const calendarCells = useMemo(() => {
    const [year, month] = recordMonth.split("-").map(Number);
    const first = new Date(year, month - 1, 1);
    const days = new Date(year, month, 0).getDate();
    const mondayOffset = (first.getDay() + 6) % 7;
    return Array.from({ length: 42 }, (_, index) => {
      const day = index - mondayOffset + 1;
      if (day < 1 || day > days) return null;
      return `${recordMonth}-${String(day).padStart(2, "0")}`;
    });
  }, [recordMonth]);
  const shiftRecordMonth = (offset: number) => {
    const [year, month] = recordMonth.split("-").map(Number);
    const next = new Date(year, month - 1 + offset, 1);
    setRecordMonth(
      `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`,
    );
  };
  const programOverview =
    state.planItems.length ? (
      <Card className="training-program-card">
        <div className="training-program-copy">
          <img src="/runner-illustration-unbranded.png" alt="跑者" />
          <div>
            <span className="training-plan-version">当前计划 V{state.planVersion}</span>
            <h2>{state.planProgram.title.replace(" · ", " ·\u200B")}</h2>
            <span>
              {state.planProgram.phase} · 第 {selectedWeek} 周 / {state.planProgram.totalWeeks}
            </span>
          </div>
        </div>
        <div className="training-program-score">
          <span>本周完成</span>
          <strong>
            {completedCount}
            <small> / {weekItems.length}</small>
          </strong>
        </div>
        <div className="readiness-track training-program-track">
          <span style={{ width: `${dueCompletionPercent}%` }} />
          {state.planRevision && (
            <i
              className="training-program-revision-tick"
              style={{ left: `${revisionBoundaryPercent}%` }}
              aria-label={`${state.planRevision.effectiveDate} 计划调整生效`}
            />
          )}
        </div>
        <div className="training-program-metrics">
          <span><small>截至今日</small><b>{dueCompletedCount}/{dueItems.length} · {dueCompletionPercent}%</b></span>
          <span><small>周期累计</small><b>{cycleActualDistance.toFixed(1)} km</b></span>
          <span><small>本周 · 负荷</small><b>{weekDistance.toFixed(1)} km · {weekLoad}</b></span>
        </div>
        {state.planRevision && (
          <div className="training-program-revision">
            <RefreshCw />
            <span>
              <b>{state.planRevision.effectiveDate.slice(5).replace("-", "月")}日已调整</b>
              <small>
                {state.planRevision.kind === "new_cycle"
                  ? "新周期从生效日开始，原计划已归档"
                  : `已继承此前完成 ${state.planRevision.inheritedCompletedCount} 次 · 生效日前按 V${state.planRevision.previousVersion} 冻结`}
              </small>
            </span>
          </div>
        )}
        {state.planGenerationStatus === "idle" && (
          <div className="training-profile-pending">
            你的档案有了新变化。新方案确认前，先安心按 V{state.planVersion} 训练。
          </div>
        )}
        <small className="agent-plan-stamp">
          <Bot size={12} /> AI 教练根据最新档案生成于 {state.planGeneratedAt}
        </small>
      </Card>
    ) : state.planGenerationStatus === "loading" ? (
      <Card
        className="training-program-loading"
        role="status"
        aria-live="polite"
      >
        <LoaderCircle className="spin" />
        <div>
          <b>正在把新档案融入本周计划</b>
          <p>我会重新核对负荷与补给，旧计划不会覆盖你的新档案。</p>
        </div>
      </Card>
    ) : (
      <EmptyState
        title="档案有了新变化，计划也该一起更新"
        detail="更新后，后续训练会按最新状态安排，已经完成的记录不会被改动。"
        action={
          <Button onClick={() => go("PLAN-02")}>更新训练计划</Button>
        }
      />
    );
  if (!state.profileComplete)
    return (
      <div className="page">
        <PageHeader title="训练中心" back={false} />
        <EmptyState
          title="先让我更懂你的训练起点"
          detail="用大约 3 分钟完成目标、近期训练、身体感受与补给习惯，之后的计划才会真正贴合你。"
          action={<Button onClick={() => go("ONB-01")}>开始建立档案</Button>}
        />
      </div>
    );
  if (state.planGenerationStatus !== "ready" && !state.planItems.length)
    return <PlanGenerationGate title="训练中心" />;
  return (
    <div className="page hub-page training-hub-v21">
      <PageHeader
        title="训练中心"
        back={false}
        action={<span className="assistant-live-dot" aria-label="在线" />}
      />
      <div className="segmented training-tabs">
        <button
          className={state.trainingView === "plan" ? "active" : ""}
          onClick={() => patch({ trainingView: "plan" })}
        >
          计划
        </button>
        <button
          className={state.trainingView === "record" ? "active" : ""}
          onClick={() => {
            setRecordMonth(state.selectedDate.slice(0, 7));
            patch({ trainingView: "record" });
          }}
        >
          记录
        </button>
      </div>
      {state.trainingView === "plan" ? (
        <>
          <div className="schedule-scope-tabs" aria-label="计划查看范围">
            <button
              className={scheduleScope === "week" ? "active" : ""}
              onClick={() => setScheduleScope("week")}
            >
              周安排
            </button>
            <button
              className={scheduleScope === "cycle" ? "active" : ""}
              onClick={() => setScheduleScope("cycle")}
            >
              完整周期
            </button>
            <button
              className={scheduleScope === "status" ? "active" : ""}
              onClick={() => setScheduleScope("status")}
            >
              机能趋势
            </button>
          </div>
          {scheduleScope === "status" && <TrainingCycleComparison />}
          {scheduleScope === "cycle" && (
            <TrainingCycleOverview
              items={sortedPlans}
              totalWeeks={state.planProgram.totalWeeks}
              selectedDate={state.selectedDate}
              completedIds={completedIds}
              onSelect={(selectedDate) => {
                patch({ selectedDate });
                setScheduleScope("week");
              }}
            />
          )}
          {scheduleScope === "week" && (
            <>
              {programOverview}
              {state.planGenerationStatus === "ready" && (
                <section
                  className="training-week-priority"
                  aria-labelledby="weekly-training-heading"
                >
                  <div className="training-week-priority__head">
                    <div>
                      <span>本周执行入口</span>
                      <h2 id="weekly-training-heading">
                        第 {selectedWeek} 周安排 · {selectedWeekFeature}
                      </h2>
              <p>{weekItems.length} 次训练 · 选一项看看今天怎么练</p>
                    </div>
                  </div>
                  <Card className="training-week-card training-week-card--priority">
                    {weekItems.map((item) => {
                      const icon = trainingTypeIcon(item);
                      const isSelected = item.id === selectedPlan?.id;
                      const isAdjusted = item.changed || item.status === "adjusted";
                      const executionState = deriveTrainingWeekStatus(item, {
                        todayIso,
                        hasActivity: completedIds.has(item.id),
                      });
                      const actionLabel =
                        executionState.tone === "completed"
                          ? "查看结果"
                          : executionState.tone === "today" ||
                              executionState.tone === "in-progress"
                      ? "去设备执行"
                            : executionState.tone === "missed"
                              ? "补充记录"
                              : "查看详情";
                      return (
                        <button
                          className={[
                            isSelected ? "is-current" : "",
                            executionState.tone === "today" ? "is-today" : "",
                          ]
                            .filter(Boolean)
                            .join(" ")}
                          key={item.id}
                          aria-label={`${item.title}，${executionState.label}，${actionLabel}`}
                          onClick={() => {
                            patch({ selectedDate: item.date });
                            go("PLAN-04");
                          }}
                        >
                          <img src={icon.src} alt={icon.alt} />
                          <span className="week-copy">
                            <b>
                              {new Date(
                                `${item.date}T00:00:00`,
                              ).toLocaleDateString("zh-CN", {
                                weekday: "short",
                              })}{" "}
                              · {item.title}
                              {isAdjusted ? (
                                <span className="week-plan-flag">已调整</span>
                              ) : null}
                            </b>
                            <small>
                              {item.distance} km · {item.pace}/km
                            </small>
                          </span>
                          <span className="week-row-trailing">
                            <span
                              className={`week-state week-state--${executionState.tone}`}
                              aria-label={`训练状态：${executionState.label}`}
                            >
                              <i aria-hidden="true" />
                              {executionState.label}
                            </span>
                            <span
                              className={`week-row-action week-row-action--${executionState.tone}`}
                            >
                              {actionLabel}
                            </span>
                          </span>
                          <ChevronRight aria-hidden="true" />
                        </button>
                      );
                    })}
                  </Card>
                </section>
              )}
              <Card
                className="training-plan-actual"
                id="cycle-mileage"
                aria-label="训练周期各阶段计划与实际跑量柱状图"
              >
                <div className="training-plan-actual__head">
                  <div>
                    <small>当前训练周期</small>
                    <b>跑量对比</b>
                  </div>
                  <span className={cycleActivities.length ? "" : "is-pending"}>
                    {cycleActivities.length
                      ? `${cycleActivities.length} 次训练已回传`
                      : "待设备回传"}
                  </span>
                </div>
                <div className="training-plan-actual__metrics">
                  <div>
                    <span>实际跑量</span>
                    <strong>{cycleActualDistance.toFixed(1)}<small> km</small></strong>
                  </div>
                  <div>
                    <span>计划跑量</span>
                    <strong>{cyclePlannedDistance.toFixed(1)}<small> km</small></strong>
                  </div>
                  <div>
                    <span>周期达成</span>
                    <strong>{cycleDistancePercent}<small>%</small></strong>
                  </div>
                </div>
                <div className="training-plan-actual__figure-title">
                  <span>各训练阶段总跑量（计划 vs 实际）</span>
                </div>
                {state.planRevision && (
                  <div className="training-mileage-revision-note">
                    <i />
                    <span>
                      {state.planRevision.effectiveDate} 调整分界 · 此前按 V{state.planRevision.previousVersion}，此后按 V{state.planRevision.version}
                    </span>
                  </div>
                )}
                <div
                  className="training-plan-actual__chart"
                  role="img"
                  aria-label={`本周期计划跑量 ${cyclePlannedDistance.toFixed(1)} 公里，实际跑量 ${cycleActualDistance.toFixed(1)} 公里`}
                >
                  <ResponsiveContainer width="100%" height="100%">
                    <ComposedChart
                      data={cycleMileageComparison}
                      margin={{ top: 16, right: 5, bottom: 2, left: 0 }}
                      barGap={2}
                      barCategoryGap="25%"
                    >
                      <XAxis
                        dataKey="phase"
                        axisLine={{ stroke: "#26322b", strokeWidth: 1 }}
                        tickLine={{ stroke: "#26322b", strokeWidth: 1 }}
                        tick={{ fill: "#26322b", fontSize: 8, fontWeight: 650 }}
                      />
                      <YAxis
                        allowDecimals={false}
                        width={34}
                        axisLine={{ stroke: "#26322b", strokeWidth: 1 }}
                        tickLine={{ stroke: "#26322b", strokeWidth: 1 }}
                        tick={{ fill: "#26322b", fontSize: 7 }}
                        label={{
                          value: "跑量 (km)",
                          angle: -90,
                          position: "insideLeft",
                          offset: 7,
                          fill: "#26322b",
                          fontSize: 7,
                          fontWeight: 650,
                        }}
                      />
                      <Tooltip
                        cursor={{ fill: "rgba(89, 128, 157, .06)" }}
                        contentStyle={{
                          border: "1px solid rgba(20, 66, 42, .12)",
                          borderRadius: 5,
                          background: "rgba(255, 255, 252, .98)",
                          boxShadow: "0 8px 20px rgba(13, 55, 34, .1)",
                          fontSize: 9,
                        }}
                      />
                      <Bar
                        dataKey="plan"
                        name="计划跑量"
                        unit=" km"
                        fill="#7193ad"
                        stroke="#4f7088"
                        strokeWidth={0.8}
                        maxBarSize={24}
                        radius={[2, 2, 0, 0]}
                        isAnimationActive={false}
                      >
                        <LabelList
                          dataKey="plan"
                          position="top"
                          fill="#26322b"
                          fontSize={7}
                          fontWeight={650}
                        />
                      </Bar>
                      <Bar
                        dataKey="actual"
                        name="实际跑量"
                        unit=" km"
                        fill="#e47d27"
                        stroke="#b75b15"
                        strokeWidth={0.8}
                        maxBarSize={24}
                        radius={[2, 2, 0, 0]}
                        isAnimationActive={false}
                      >
                        {cycleActivities.length > 0 && (
                          <LabelList
                            dataKey="actual"
                            position="top"
                            fill="#26322b"
                            fontSize={7}
                            fontWeight={650}
                          />
                        )}
                      </Bar>
                    </ComposedChart>
                  </ResponsiveContainer>
                </div>
                <div className="training-plan-actual__legend" aria-hidden="true">
                  <span><i className="is-plan" />计划跑量</span>
                  <span><i className="is-actual" />实际跑量</span>
                  <small>单位：km</small>
                </div>
                <p>
                  {cycleActivities.length
                    ? "设备回传后按训练阶段累计实际跑量，阶段结束后自动生成 AI 分析"
                    : "实际跑量将在设备回传后按训练阶段累计，不会将未回传判定为未完成"}
                </p>
              </Card>
              {state.planSyncReceipt && (
                <Card className="training-agent-sync-receipt">
                  <div className="training-agent-sync-receipt__icon" aria-hidden="true">
                    <RefreshCw />
                  </div>
                  <div className="training-agent-sync-receipt__body">
                    <b>
                      {state.planSyncReceipt.kind === "adjusted"
                  ? "AI 教练的调整已同步"
                        : "计划确认已同步"}
                    </b>
                    <span>{state.planSyncReceipt.summary}</span>
                    <div className="training-agent-sync-receipt__meta">
                      <small>
                        计划 V{state.planSyncReceipt.version} · {state.planSyncReceipt.updatedAt}
                      </small>
                      <StatusBadge tone="green">训练与设备一致</StatusBadge>
                    </div>
                  </div>
                </Card>
              )}
              {(state.planRevision || state.planHistory.length > 0) && (
                <details className="training-plan-history">
                  <summary>
                    <History />
                    <span>
                      <b>计划版本与历史训练</b>
                      <small>已完成成果永久保留，计划调整不回溯</small>
                    </span>
                    <ChevronRight />
                  </summary>
                  <div className="training-plan-history__content">
                    {state.planRevision && (
                      <div className="training-plan-history__revision">
                        <span>当前计划</span>
                        <b>V{state.planRevision.version} · {state.planRevision.kind === "new_cycle" ? "新周期" : state.planRevision.kind === "structure" ? "结构调整" : "负荷微调"}</b>
                        <small>{state.planRevision.effectiveDate} 生效 · {state.planRevision.reason}</small>
                        {state.planRevision.kind !== "new_cycle" && (
                          <small>
                            本周 {state.planRevision.previousWeekSessions} → {state.planRevision.currentWeekSessions} 次；继承 {state.planRevision.inheritedCompletedCount} 次已完成训练
                          </small>
                        )}
                      </div>
                    )}
                    {state.planHistory.map((plan) => (
                      <div className="training-plan-history__item" key={plan.id}>
                        <span><b>{plan.title}</b><small>{plan.phase} · V{plan.version}</small></span>
                        <strong>{plan.completedWeeks}/{plan.totalWeeks} 周</strong>
                        <p>{plan.completedSessions} 次训练 · {plan.actualDistance.toFixed(1)} km · {plan.endedAt} 结束</p>
                        <small>{plan.archiveReason}</small>
                      </div>
                    ))}
                  </div>
                </details>
              )}
            </>
          )}
        </>
      ) : (
        <div className="training-history-view">
          <Card className="history-month-card">
            <div className="history-month-head">
              <button aria-label="上一个月" onClick={() => shiftRecordMonth(-1)}>
                <ChevronLeft />
              </button>
              <div>
                <span>训练记录</span>
                <h2>
                  {new Date(`${recordMonth}-01T12:00:00`).toLocaleDateString(
                    "zh-CN",
                    { year: "numeric", month: "long" },
                  )}
                </h2>
              </div>
              <button aria-label="下一个月" onClick={() => shiftRecordMonth(1)}>
                <ChevronRight />
              </button>
            </div>
            <div className="history-month-metrics">
              <Metric value={monthDistance.toFixed(1)} unit="km" label="总距离" />
              <Metric value={String(monthRecords.length)} unit="次" label="训练次数" />
              <Metric value={String(monthDurationMinutes)} unit="分钟" label="总时长" />
            </div>
            <details className="history-calendar-disclosure">
              <summary><CalendarCheck /> 按日期查看</summary>
              <div className="history-calendar" role="grid" aria-label={`${recordMonth}训练日历`}>
              {['一', '二', '三', '四', '五', '六', '日'].map((label) => (
                <span className="history-calendar__weekday" key={label}>{label}</span>
              ))}
              {calendarCells.map((date, index) => {
                if (!date) return <span className="history-calendar__empty" key={`empty-${index}`} />;
                const hasActivity = records.some((record) => record.date === date);
                const dayPlan = state.planItems.find((item) => item.date === date);
                const hasPlan = Boolean(dayPlan);
                return (
                  <button
                    type="button"
                    key={date}
                    aria-label={`${date}${hasActivity ? '，有训练记录' : hasPlan ? '，有训练计划' : ''}`}
                    className={`${state.selectedDate === date ? 'selected' : ''} ${hasActivity ? 'has-activity' : ''} ${hasPlan ? 'has-plan' : ''} ${dayPlan?.changed ? 'has-adjusted-plan' : ''}`}
                    onClick={() => {
                      const activity = records.find((record) => record.date === date);
                      patch({
                        selectedDate: date,
                        ...(activity ? { lastCompletedActivityId: activity.id } : {}),
                      });
                      if (hasActivity) go("RECORD-02");
                    }}
                  >
                    <span>{Number(date.slice(-2))}</span>
                    {(hasActivity || hasPlan) && <i aria-hidden="true" />}
                  </button>
                );
              })}
              </div>
              <div className="history-calendar-legend">
                <span><i className="activity" />已完成训练</span>
                <span><i className="plan" />仅有计划</span>
              </div>
            </details>
          </Card>
          <SectionTitle
            title={`${Number(recordMonth.slice(5))} 月训练 · ${monthRecords.length} 次`}
            action={
              state.device.connectedProviders.length ?
                <button data-auth-required="cloudSync" className="text-link" onClick={syncDevice} disabled={state.device.status === "syncing"}>
                  {state.device.status === "syncing" ? "同步中…" : "同步设备记录"}
                </button>
                : <button className="text-link" onClick={() => go("DEV-01")}>连接设备</button>
            }
          />
          {monthRecords.length ? (
            <div className="history-activity-list">
              {monthRecords.map((record) => {
                const linkedPlan = state.planItems.find((item) => item.id === record.planItemId);
                return (
                  <Card
                    className="history-activity-card"
                    key={record.id}
                    onClick={() => {
                      patch({
                        selectedDate: record.date,
                        lastCompletedActivityId: record.id,
                      });
                      go("RECORD-02");
                    }}
                  >
                    <div className="history-activity-date">
                      <strong>{Number(record.date.slice(-2))}</strong>
                      <span>{new Date(`${record.date}T12:00:00`).toLocaleDateString('zh-CN', { weekday: 'short' })}</span>
                    </div>
                    <div className="history-activity-copy">
                      <b>{linkedPlan?.title ?? '自由训练'}</b>
                      <span>{record.distance.toFixed(2)} km · {record.duration} · {record.pace}/km</span>
                      <small>{record.source} · 平均心率 {record.heartRate} bpm</small>
                    </div>
                    <StatusBadge tone="green">已完成</StatusBadge>
                    <ChevronRight />
                  </Card>
                );
              })}
            </div>
          ) : (
            <EmptyState
              title="这个月还没有训练记录"
              detail="切换月份查看历史训练；连接设备后，云端实绩会按日期写入记录层，不覆盖原计划。"
              action={state.device.connectedProviders.length ? <Button data-auth-required="cloudSync" onClick={syncDevice}>同步设备记录</Button> : <Button onClick={() => go("DEV-01")}>连接设备</Button>}
            />
          )}
          {state.planItems.some(
            (item) => item.date.startsWith(recordMonth) && item.changed,
          ) && (
            <details className="history-plan-change-disclosure">
              <summary><RefreshCw /> 查看本月计划调整</summary>
              <Card className="history-plan-changes">
                {state.planItems
                  .filter((item) => item.date.startsWith(recordMonth) && item.changed)
                  .sort((a, b) => a.date.localeCompare(b.date))
                  .map((item) => (
                    <button
                      type="button"
                      key={item.id}
                      onClick={() => {
                        patch({ selectedDate: item.date, trainingView: "plan" });
                      }}
                    >
                      <RefreshCw />
                      <span>
                        <b>{item.title}</b>
                        <small>{item.date} · {item.distance} km · {item.pace}/km</small>
                      </span>
                      <StatusBadge tone="blue">V{item.version}</StatusBadge>
                      <ChevronRight />
                    </button>
                  ))}
              </Card>
            </details>
          )}
        </div>
      )}
    </div>
  );
}

type CycleCompareRange = CycleComparisonRange;

const trainingFunctionTrend = Array.from({ length: 28 }, (_, index) => {
  const date = new Date("2026-08-14T12:00:00");
  date.setDate(date.getDate() - (27 - index) * 7);
  const progressive = index / 27;
  return {
    date: `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`,
    rhr: Math.round(54 - progressive * 4 + Math.sin(index * 1.2) * 1.6),
    hrv: Math.round(26 + progressive * 7 + Math.sin(index * 0.95) * 3),
    distance: Number((6.2 + progressive * 3.8 + Math.sin(index * 1.5) * 1.7).toFixed(1)),
    durationMinutes: Math.round(38 + progressive * 17 + Math.sin(index * 1.15) * 6),
    paceSeconds: Math.round(342 - progressive * 22 + Math.sin(index * 0.9) * 7),
    fastestPaceSeconds: Math.round(312 - progressive * 24 + Math.sin(index * 0.73) * 6),
    heartRate: Math.round(143 + progressive * 3 + Math.sin(index * 1.05) * 4),
    load: Math.round(58 + progressive * 23 + Math.sin(index * 1.35) * 9),
    loadRatio: Number((0.84 + progressive * 0.17 + Math.sin(index * 0.72) * 0.08).toFixed(2)),
    cadence: Math.round(176 + progressive * 6 + Math.sin(index * 1.4) * 2),
    strideLength: Number((0.91 + progressive * 0.08 + Math.sin(index * 0.8) * 0.02).toFixed(2)),
    vdot: Number((41.2 + progressive * 3.5 + Math.sin(index * 0.55) * 0.35).toFixed(1)),
    elevationGain: Math.round(42 + progressive * 28 + Math.abs(Math.sin(index * 0.75)) * 30),
    recoveryHours: Math.round(15 + progressive * 3 + Math.sin(index * 1.1) * 3),
  };
});

type LongTermMetricKey =
  | "distance"
  | "durationMinutes"
  | "paceSeconds"
  | "fastestPaceSeconds"
  | "heartRate"
  | "load"
  | "loadRatio"
  | "cadence"
  | "strideLength"
  | "vdot"
  | "elevationGain"
  | "recoveryHours";

const averageOf = (values: number[]) =>
  values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;

const totalOf = (values: number[]) =>
  values.reduce((sum, value) => sum + value, 0);

const paceZones = [
  ["recovery", "恢复跑", "≥ 5′41″", "恢复"],
  ["easy", "轻松跑", "4′52″–5′41″", "轻松"],
  ["marathon", "马拉松配速", "4′13″–4′52″", "稳态"],
  ["threshold", "阈值跑", "3′58″–4′13″", "质量"],
  ["interval", "间歇跑", "3′41″–3′58″", "高强度"],
];

const heartRateZones = [
  ["recovery", "心率 1 区", "≤ 135", "恢复"],
  ["easy", "心率 2 区", "135–148", "轻松"],
  ["marathon", "心率 3 区", "148–158", "稳态"],
  ["threshold", "心率 4 区", "158–170", "阈值"],
  ["interval", "心率 5 区", "170–179", "间歇"],
];

function TrainingCycleComparison() {
  const { state } = useApp();
  const [range, setRange] = useState<CycleCompareRange>(30);
  const [zoneType, setZoneType] = useState<CycleComparisonZoneType>("pace");
  const [summaryRetry, setSummaryRetry] = useState(0);
  const [summaryState, setSummaryState] = useState<{
    status: "loading" | "ready" | "error";
    data: AgentCycleComparisonSummary | null;
    agentVersion: string;
    basis: string;
  }>({ status: "loading", data: null, agentVersion: "", basis: "" });
  const summarySequence = useRef(0);
  const visibleCount = ({ 14: 5, 30: 8, 60: 11, 90: 14 } as const)[range];
  const trainingData = trainingFunctionTrend.slice(-visibleCount);
  const previousTrainingData = trainingFunctionTrend.slice(-visibleCount * 2, -visibleCount);
  const summarizePerformance = (data: typeof trainingFunctionTrend) => ({
    distance: totalOf(data.map((point) => point.distance)),
    durationMinutes: totalOf(data.map((point) => point.durationMinutes)),
    paceSeconds: averageOf(data.map((point) => point.paceSeconds)),
    fastestPaceSeconds: Math.min(...data.map((point) => point.fastestPaceSeconds)),
    heartRate: averageOf(data.map((point) => point.heartRate)),
    load: totalOf(data.map((point) => point.load)),
    loadRatio: averageOf(data.map((point) => point.loadRatio)),
    cadence: averageOf(data.map((point) => point.cadence)),
    strideLength: averageOf(data.map((point) => point.strideLength)),
    vdot: data.at(-1)?.vdot ?? 0,
    elevationGain: totalOf(data.map((point) => point.elevationGain)),
    recoveryHours: averageOf(data.map((point) => point.recoveryHours)),
  });
  const performanceCurrent = summarizePerformance(trainingData);
  const performancePrevious = summarizePerformance(previousTrainingData);
  const performanceSummaryMetrics = [
    { key: "distance", label: "累计距离", value: `${performanceCurrent.distance.toFixed(1)} km`, delta: `${performanceCurrent.distance >= performancePrevious.distance ? "+" : ""}${(performanceCurrent.distance - performancePrevious.distance).toFixed(1)} km` },
    { key: "duration", label: "运动时长", value: `${(performanceCurrent.durationMinutes / 60).toFixed(1)} h`, delta: `${performanceCurrent.durationMinutes >= performancePrevious.durationMinutes ? "+" : ""}${Math.round(performanceCurrent.durationMinutes - performancePrevious.durationMinutes)} min` },
    { key: "load", label: "累计训练负荷", value: `${Math.round(performanceCurrent.load)}`, delta: `${performanceCurrent.load >= performancePrevious.load ? "+" : ""}${Math.round(performanceCurrent.load - performancePrevious.load)}` },
    { key: "ratio", label: "平均负荷比", value: performanceCurrent.loadRatio.toFixed(2), delta: `${performanceCurrent.loadRatio >= performancePrevious.loadRatio ? "+" : ""}${(performanceCurrent.loadRatio - performancePrevious.loadRatio).toFixed(2)}` },
    { key: "cadence", label: "平均步频", value: `${Math.round(performanceCurrent.cadence)} spm`, delta: `${performanceCurrent.cadence >= performancePrevious.cadence ? "+" : ""}${Math.round(performanceCurrent.cadence - performancePrevious.cadence)} spm` },
    { key: "stride", label: "平均步长", value: `${performanceCurrent.strideLength.toFixed(2)} m`, delta: `${performanceCurrent.strideLength >= performancePrevious.strideLength ? "+" : ""}${(performanceCurrent.strideLength - performancePrevious.strideLength).toFixed(2)} m` },
    { key: "vdot", label: "当前 VDOT", value: performanceCurrent.vdot.toFixed(1), delta: `${performanceCurrent.vdot >= performancePrevious.vdot ? "+" : ""}${(performanceCurrent.vdot - performancePrevious.vdot).toFixed(1)}` },
    { key: "elevation", label: "累计爬升", value: `${Math.round(performanceCurrent.elevationGain)} m`, delta: `${performanceCurrent.elevationGain >= performancePrevious.elevationGain ? "+" : ""}${Math.round(performanceCurrent.elevationGain - performancePrevious.elevationGain)} m` },
    { key: "recovery", label: "平均恢复时间", value: `${Math.round(performanceCurrent.recoveryHours)} h`, delta: `${Math.abs(Math.round(performanceCurrent.recoveryHours - performancePrevious.recoveryHours))} h${performanceCurrent.recoveryHours <= performancePrevious.recoveryHours ? "缩短" : "增加"}` },
  ];
  const longTermMetricOptions: Array<{ key: LongTermMetricKey; label: string; unit: string }> = [
    { key: "distance", label: "距离", unit: " km" },
    { key: "durationMinutes", label: "时长", unit: " min" },
    { key: "load", label: "负荷", unit: "" },
    { key: "loadRatio", label: "负荷比", unit: "" },
    { key: "cadence", label: "步频", unit: " spm" },
    { key: "strideLength", label: "步长", unit: " m" },
    { key: "vdot", label: "VDOT", unit: "" },
    { key: "elevationGain", label: "爬升", unit: " m" },
    { key: "recoveryHours", label: "恢复", unit: " h" },
  ];
  const metricByKey = Object.fromEntries(
    performanceSummaryMetrics.map((metric) => [metric.key, metric]),
  );
  const optionByKey = Object.fromEntries(
    longTermMetricOptions.map((metric) => [metric.key, metric]),
  );
  const metrics = [
    { label: "静息心率", en: "RHR", value: "49", unit: "bpm", delta: "-2", tone: "cyan" },
    { label: "心率变异性", en: "HRV", value: "32", unit: "ms", delta: "+4", tone: "purple" },
    { label: "阈值配速", en: "THRESHOLD PACE", value: "4′05″", unit: "/km", delta: "快 7秒", tone: "green" },
    { label: "阈值心率", en: "THRESHOLD HR", value: "169", unit: "bpm", delta: "-2", tone: "orange" },
  ];
  const zones = zoneType === "pace" ? paceZones : heartRateZones;
  const agentContext = useMemo(() => buildAgentContext(state), [state]);
  const summaryRequest = useMemo<CycleComparisonSummaryRequest>(
    () => ({
      rangeDays: range,
      zoneType,
      metrics: metrics.map(({ tone: _tone, ...metric }) => metric),
      trend: trainingData.map((point) => ({ ...point })),
      profileGoal: state.profile.goal,
      lastSyncAt: state.device.lastSyncAt,
    }),
    [range, zoneType, state.profile.goal, state.device.lastSyncAt],
  );

  useEffect(() => {
    const sequence = ++summarySequence.current;
    let active = true;
    setSummaryState((current) => ({ ...current, status: "loading" }));
    services.agent
      .summarizeCycleComparison(summaryRequest, agentContext)
      .then((result) => {
        if (!active || sequence !== summarySequence.current) return;
        setSummaryState({
          status: "ready",
          data: result.data,
          agentVersion: result.agentVersion ?? "CPT Agent",
          basis: result.basis ?? `根据最近 ${range} 天数据动态生成`,
        });
      })
      .catch(() => {
        if (!active || sequence !== summarySequence.current) return;
        setSummaryState((current) => ({ ...current, status: "error" }));
      });
    return () => {
      active = false;
    };
  }, [summaryRequest, agentContext, summaryRetry]);

  return (
    <section className="cycle-status-dashboard" aria-labelledby="cycle-status-title">
      <div className="cycle-status-heading">
        <div>
          <span>训练状态追踪</span>
          <h2 id="cycle-status-title">训练机能趋势</h2>
          <p>最近 {range} 天，自动对照此前 {range} 天</p>
        </div>
        <StatusBadge tone="green">机能数据已同步</StatusBadge>
      </div>

      <div className="cycle-range-tabs" aria-label="时间范围">
        {([14, 30, 60, 90] as const).map((value) => (
          <button key={value} className={range === value ? "active" : ""} aria-pressed={range === value} onClick={() => setRange(value)}>{value} 天</button>
        ))}
      </div>

      <div className="cycle-metric-strip" aria-label="训练机能摘要">
        {metrics.map((metric) => (
          <article className={`cycle-metric-card cycle-metric-card--${metric.tone}`} key={metric.label}>
            <div><b>{metric.label}</b><small>{metric.en}</small></div>
            <strong>{metric.value}<em>{metric.unit}</em></strong>
            <span>{metric.delta} <small>较此前 {range} 天</small></span>
          </article>
        ))}
      </div>

      <article
        className={`cycle-agent-summary cycle-agent-summary--${summaryState.status}`}
        data-status={summaryState.status}
        data-context-version={summaryState.data?.contextVersion ?? "pending"}
        aria-live="polite"
        aria-busy={summaryState.status === "loading"}
      >
        <div className="cycle-agent-summary__head">
          <div className="cycle-agent-summary__identity">
            <span className="cycle-agent-summary__mark" aria-hidden="true">CPT</span>
            <div><b>AI 教练解读训练机能</b><small>结合最近 {range} 天变化，判断下一步怎么练</small></div>
          </div>
          <span className="cycle-agent-summary__state">
            {summaryState.status === "loading" ? <LoaderCircle aria-hidden="true" /> : <Activity aria-hidden="true" />}
            {summaryState.status === "loading" ? "正在更新" : summaryState.status === "error" ? "更新失败" : "随数据更新"}
          </span>
        </div>

        {summaryState.status === "loading" ? (
          <div className="cycle-agent-summary__loading" role="status">
            <span /><span /><span />
            <small>AI 教练正在梳理这 {range} 天的训练机能变化…</small>
          </div>
        ) : summaryState.status === "error" ? (
          <div className="cycle-agent-summary__error">
            <div><AlertTriangle aria-hidden="true" /><span>这次总结暂时没生成出来，图表数据不受影响。</span></div>
            <button type="button" onClick={() => setSummaryRetry((value) => value + 1)}><RefreshCw aria-hidden="true" />再看一次</button>
          </div>
        ) : summaryState.data ? (
          <div className="cycle-agent-summary__content">
            <span className="cycle-agent-summary__eyebrow">当前判断 <small>OBSERVATION</small></span>
            <h3>{summaryState.data.headline}</h3>
            <p>{summaryState.data.summary}</p>
            <div className="cycle-agent-summary__evidence">
              <span>关键变化 <small>KEY SHIFTS</small></span>
              <ul aria-label="总结依据">
                {summaryState.data.evidence.map((item, index) => (
                  <li key={item}><span>{String(index + 1).padStart(2, "0")}</span><b>{item}</b></li>
                ))}
              </ul>
            </div>
            <div className="cycle-agent-summary__action">
              <span>执行建议 <small>NEXT ACTION</small></span>
              <p>{summaryState.data.action}</p>
              <ArrowRight aria-hidden="true" />
            </div>
            <small className="cycle-agent-summary__basis" title={summaryState.agentVersion}>{summaryState.basis}</small>
          </div>
        ) : null}
      </article>

      <div className="cycle-chart-stack">
        <CycleTrendChart
          title="静息心率趋势"
          subtitle="观察恢复基础是否保持稳定"
          data={trainingData}
          dataKey="rhr"
          color="#2a9aaa"
          unit=" bpm"
          reference={47}
        />
        <CycleTrendChart
          title="HRV 趋势"
          subtitle="优先观察个人区间内的连续变化"
          data={trainingData}
          dataKey="hrv"
          color="#7959d6"
          unit=" ms"
          rangeBand={[25, 35]}
        />
      </div>

      <section className="cycle-period-report" aria-labelledby="cycle-period-report-title">
        <div className="record-section-head">
          <div><span>周期训练表现</span><h2 id="cycle-period-report-title">最近 {range} 天 vs 此前 {range} 天</h2></div>
          <small>同口径对比</small>
        </div>
        <p>从训练总量、训练刺激、跑步效率与恢复负担四个维度，查看本周期变化。</p>
        <CyclePerformanceGroup
          eyebrow="训练执行"
          title="训练总量与 VDOT"
          metrics={[metricByKey.distance, metricByKey.duration, metricByKey.vdot]}
          options={[optionByKey.distance, optionByKey.durationMinutes, optionByKey.vdot]}
          data={trainingData}
          range={range}
        />
        <CyclePerformanceGroup
          eyebrow="训练刺激"
          title="负荷、负荷比与恢复"
          metrics={[metricByKey.load, metricByKey.ratio, metricByKey.recovery]}
          options={[optionByKey.load, optionByKey.loadRatio, optionByKey.recoveryHours]}
          data={trainingData}
          range={range}
        />
        <CyclePerformanceGroup
          eyebrow="跑步效率"
          title="步频与步长"
          metrics={[metricByKey.cadence, metricByKey.stride]}
          options={[optionByKey.cadence, optionByKey.strideLength]}
          data={trainingData}
          range={range}
        />
        <CyclePerformanceGroup
          eyebrow="路线负荷"
          title="累计爬升"
          metrics={[metricByKey.elevation]}
          options={[optionByKey.elevationGain]}
          data={trainingData}
          range={range}
        />
      </section>

      <section className="cycle-zone-distribution" aria-labelledby="cycle-zone-distribution-title">
        <div className="cycle-performance-compare__head">
          <div><span>强度结构</span><h3 id="cycle-zone-distribution-title">区间占比对比</h3></div>
          <small>最近 vs 此前</small>
        </div>
        <ZoneComparisonBar label="配速区间" current={[20, 48, 19, 10, 3]} previous={[25, 51, 15, 7, 2]} />
        <ZoneComparisonBar label="心率区间" current={[8, 57, 23, 10, 2]} previous={[12, 61, 19, 7, 1]} />
      </section>

      <section className="cycle-zone-card">
        <div className="cycle-zone-head">
          <div><span>训练区间</span><b>由当前阈值自动换算</b></div>
          <div role="tablist" aria-label="训练区间类型">
            <button role="tab" aria-selected={zoneType === "pace"} className={zoneType === "pace" ? "active" : ""} onClick={() => setZoneType("pace")}>配速</button>
            <button role="tab" aria-selected={zoneType === "heart"} className={zoneType === "heart" ? "active" : ""} onClick={() => setZoneType("heart")}>心率</button>
          </div>
        </div>
        <div className="cycle-comparison-table" role="table" aria-label={`${zoneType === "pace" ? "配速" : "心率"}训练区间`}>
          <div className="cycle-comparison-table__head" role="row">
            <span role="columnheader">Zone</span><span role="columnheader">名称</span><span role="columnheader">区间</span><span role="columnheader">用途</span>
          </div>
          {zones.map(([zone, name, interval, use]) => (
            <div className="cycle-comparison-table__row" role="row" key={zone}>
              <b className={`zone-${zone}`} role="cell">{zone}</b><span role="cell">{name}</span><strong role="cell">{interval}</strong><small role="cell">{use}</small>
            </div>
          ))}
        </div>
      </section>
      <p className="cycle-status-footnote">训练机能数据来自已连接的健康平台与训练设备 · 最后同步 今天 07:10</p>
    </section>
  );
}

function CycleTrendChart({
  title,
  subtitle,
  data,
  dataKey,
  color,
  unit,
  reference,
  rangeBand,
}: {
  title: string;
  subtitle: string;
  data: Array<Record<string, string | number>>;
  dataKey: string;
  color: string;
  unit: string;
  reference?: number;
  rangeBand?: [number, number];
}) {
  return (
    <article className="cycle-trend-card">
      <div className="cycle-trend-card__head"><div><b>{title}</b><span>{subtitle}</span></div><small>{data.at(-1)?.[dataKey]}{unit}</small></div>
      <div className="cycle-trend-chart" aria-label={`${title}折线图`}>
        <ResponsiveContainer width="100%" height="100%">
          <LineChart data={data} margin={{ top: 12, right: 8, bottom: 0, left: -12 }}>
            <CartesianGrid stroke="rgba(24, 61, 42, .08)" strokeDasharray="2 4" vertical={false} />
            <XAxis dataKey="date" tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} minTickGap={16} />
            <YAxis tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} domain={["dataMin - 2", "dataMax + 2"]} />
            {rangeBand && <ReferenceArea y1={rangeBand[0]} y2={rangeBand[1]} fill={color} fillOpacity={0.08} strokeOpacity={0} />}
            {reference !== undefined && <ReferenceLine y={reference} stroke={color} strokeOpacity={0.45} strokeDasharray="4 4" />}
            <Tooltip contentStyle={{ border: "1px solid rgba(24, 61, 42, .12)", borderRadius: 10, background: "rgba(251, 252, 249, .97)", boxShadow: "0 8px 22px rgba(17, 45, 30, .10)", fontSize: 10 }} formatter={(value) => [`${value}${unit}`, title]} />
            <Line type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2.2} dot={{ r: 2.5, fill: color, strokeWidth: 0 }} activeDot={{ r: 4 }} isAnimationActive={false} />
          </LineChart>
        </ResponsiveContainer>
      </div>
    </article>
  );
}

function CyclePerformanceGroup({
  eyebrow,
  title,
  metrics,
  options,
  data,
  range,
}: {
  eyebrow: string;
  title: string;
  metrics: Array<{ key: string; label: string; value: string; delta: string }>;
  options: Array<{ key: LongTermMetricKey; label: string; unit: string }>;
  data: Array<Record<string, string | number>>;
  range: number;
}) {
  const [activeMetric, setActiveMetric] = useState<LongTermMetricKey>(options[0].key);
  const activeOption = options.find((option) => option.key === activeMetric) ?? options[0];
  return (
    <section className="cycle-performance-group" aria-label={title}>
      <div className="cycle-performance-group__head">
        <div><span>{eyebrow}</span><h3>{title}</h3></div>
        <small>最近 {range} 天</small>
      </div>
      <div className={`cycle-performance-group__metrics ${metrics.length === 3 ? "is-three" : ""}`}>
        {metrics.map((metric) => (
          <article key={metric.key}>
            <span>{metric.label}</span>
            <b>{metric.value}</b>
            <small>{metric.delta} · 较此前</small>
          </article>
        ))}
      </div>
      <div className="cycle-performance-group__tabs" role="tablist" aria-label={`${title}趋势指标`}>
        {options.map((option) => (
          <button
            key={option.key}
            type="button"
            role="tab"
            aria-selected={activeMetric === option.key}
            className={activeMetric === option.key ? "active" : ""}
            onClick={() => setActiveMetric(option.key)}
          >
            {option.label}
          </button>
        ))}
      </div>
      <LongTermPerformanceChart
        data={data}
        dataKey={activeMetric}
        label={activeOption.label}
        unit={activeOption.unit}
      />
    </section>
  );
}

function LongTermPerformanceChart({
  data,
  dataKey,
  label,
  unit,
}: {
  data: Array<Record<string, string | number>>;
  dataKey: LongTermMetricKey;
  label: string;
  unit: string;
}) {
  const formatValue = (raw: unknown) => {
    const value = Number(raw ?? 0);
    if (dataKey === "paceSeconds" || dataKey === "fastestPaceSeconds") return `${paceFromSeconds(value)}/km`;
    if (dataKey === "strideLength" || dataKey === "loadRatio" || dataKey === "vdot") {
      return `${value.toFixed(dataKey === "vdot" ? 1 : 2)}${unit}`;
    }
    return `${Math.round(value)}${unit}`;
  };
  return (
    <div className="cycle-long-term-chart" aria-label={`${label}长期变化折线图`}>
      <ResponsiveContainer width="100%" height="100%">
        <LineChart data={data} margin={{ top: 14, right: 8, bottom: 0, left: -13 }}>
          <CartesianGrid stroke="rgba(24, 61, 42, .08)" strokeDasharray="2 4" vertical={false} />
          <XAxis dataKey="date" tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} minTickGap={18} />
          <YAxis tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} domain={["dataMin - 2", "dataMax + 2"]} tickFormatter={(value) => dataKey === "paceSeconds" || dataKey === "fastestPaceSeconds" ? paceFromSeconds(Number(value)).replace("″", "") : String(value)} />
          {dataKey === "loadRatio" && <ReferenceArea y1={0.8} y2={1.2} fill="#2b8055" fillOpacity={0.08} strokeOpacity={0} />}
          <Tooltip contentStyle={{ border: "1px solid rgba(24, 61, 42, .12)", borderRadius: 8, background: "rgba(251, 252, 249, .98)", boxShadow: "0 8px 22px rgba(17, 45, 30, .10)", fontSize: 10 }} formatter={(value) => [formatValue(value), label]} />
          <Line type="monotone" dataKey={dataKey} stroke="#176d45" strokeWidth={2.2} dot={{ r: 2.4, fill: "#176d45", strokeWidth: 0 }} activeDot={{ r: 4 }} isAnimationActive={false} />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

const zoneComparisonColors = ["#5a9ec4", "#6ca447", "#d5a82d", "#de6d37", "#c44b4b"];

function ZoneComparisonBar({
  label,
  current,
  previous,
}: {
  label: string;
  current: number[];
  previous: number[];
}) {
  return (
    <div className="cycle-zone-distribution__group">
      <b>{label}</b>
      {[
        ["最近周期", current],
        ["此前周期", previous],
      ].map(([period, values]) => (
        <div className="cycle-zone-distribution__row" key={period as string}>
          <span>{period as string}</span>
          <div className="cycle-zone-distribution__bar" aria-label={`${period} ${label}分布`}>
            {(values as number[]).map((value, index) => (
              <i
                key={`${period}-${index}`}
                style={{ width: `${value}%`, background: zoneComparisonColors[index] }}
                title={`区间 ${index + 1}：${value}%`}
              />
            ))}
          </div>
        </div>
      ))}
      <div className="cycle-zone-distribution__legend" aria-label={`${label}图例`}>
        {zoneComparisonColors.map((color, index) => <span key={color}><i style={{ background: color }} />Z{index + 1}</span>)}
      </div>
    </div>
  );
}

function TrainingCycleOverview({
  items,
  totalWeeks,
  selectedDate,
  completedIds,
  onSelect,
}: {
  items: PlanItem[];
  totalWeeks: number;
  selectedDate: string;
  completedIds: Set<string>;
  onSelect: (date: string) => void;
}) {
  const stageDefinitions = buildTrainingCycleStages(totalWeeks);
  return (
    <div className="training-cycle-overview">
      <div className="cycle-overview-head">
        <div>
          <span>Agent 训练周期</span>
          <h2>{totalWeeks} 周具体安排</h2>
        </div>
        <strong>
          {items.filter((item) => completedIds.has(item.id)).length}
          <small> / {items.length}</small>
        </strong>
      </div>
      {stageDefinitions.map((stage, stageIndex) => (
        <section className="cycle-stage" key={stage.title}>
          <div className="cycle-stage__head">
            <span>{stageIndex + 1}</span>
            <div><b>{stage.title}</b><small>{stage.detail}</small></div>
            <em>{stage.weeks.map((week) => `第${week}周`).join(" · ")}</em>
          </div>
          {stage.weeks.map((week) => {
            const weekItems = items.filter(
              (item) => (item.weekIndex ?? 1) === week,
            );
            const completed = weekItems.filter((item) =>
              completedIds.has(item.id),
            ).length;
            return (
              <Card className="cycle-week-card" key={week}>
                <div className="cycle-week-title">
                  <span>
                    <b>第 {week} 周</b>
                    <small>
                      {weekItems[0]
                        ? `${new Date(weekItems[0].date + "T12:00:00").toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })} 起`
                        : ""}
                    </small>
                  </span>
                  <em>{completed}/{weekItems.length} 完成</em>
                </div>
                <div className="cycle-week-days">
                  {weekItems.map((item) => {
                    const done = completedIds.has(item.id);
                    return (
                      <button
                        key={item.id}
                        className={`${item.date === selectedDate ? "is-selected" : ""} ${done ? "is-done" : ""}`}
                        onClick={() => onSelect(item.date)}
                      >
                        <span>
                          {new Date(item.date + "T12:00:00").toLocaleDateString(
                            "zh-CN",
                            { weekday: "short" },
                          )}
                        </span>
                        <b>{new Date(item.date + "T12:00:00").getDate()}</b>
                        <small>{item.distance} km</small>
                      </button>
                    );
                  })}
                </div>
              </Card>
            );
          })}
        </section>
      ))}
    </div>
  );
}

function PlanCard({ item, onClick }: { item: PlanItem; onClick?: () => void }) {
  const tone =
    item.status === "completed"
      ? "green"
      : item.status === "adjusted"
        ? "blue"
        : item.status === "proposed" || item.status === "draft"
          ? "orange"
          : "gray";
  const icon = trainingTypeIcon(item);
  return (
    <Card className="plan-card" onClick={onClick}>
      <div className="plan-card__icon plan-card__icon--image">
        <img src={icon.src} alt={icon.alt} />
      </div>
      <div className="plan-card__body">
        <div>
          <b>{item.title}</b>
          <StatusBadge tone={tone}>{statusLabel(item.status)}</StatusBadge>
        </div>
        <span>
          {new Date(item.date).toLocaleDateString("zh-CN", {
            month: "numeric",
            day: "numeric",
            weekday: "short",
          })}
        </span>
        <div className="plan-card__metrics">
          <strong>
            {item.distance}
            <small> km</small>
          </strong>
          <strong>
            {item.pace}
            <small>/km</small>
          </strong>
          <strong>
            {item.load}
            <small> 负荷</small>
          </strong>
        </div>
        {item.changed && (
          <p className="change-line">
            <RefreshCw size={13} /> 已在 V{item.version} 中调整
          </p>
        )}
      </div>
    </Card>
  );
}

function PlanDetail() {
  const { state, go, openPlanAgent } = useApp();
  const plan =
    state.planItems.find((p) => p.date === state.selectedDate) ??
    state.planItems[0];
  const paceParts = plan.pace.match(/(\d+)'(\d+)/);
  const paceMinutes = paceParts
    ? Number(paceParts[1]) + Number(paceParts[2]) / 60
    : 6;
  const estimatedMinutes = Math.max(1, Math.round(plan.distance * paceMinutes));
  const sessionName = plan.title.split("·").at(-1)?.trim() ?? plan.title;
  const mainDistance = Math.max(0, plan.distance - 2);
  const mainSessionName = plan.title.includes("阈值")
    ? "阈值巡航"
    : plan.title.includes("长距离")
      ? "耐力巡航"
      : plan.title.includes("间歇")
        ? "间歇主训练"
        : "轻松巡航";
  const prescriptionPhases = [
    {
      key: "warmup",
      index: 1,
      title: "动态热身",
      summary: "唤醒关节与肌群，让心率平稳进入训练区间",
      rows: [
        { icon: Footprints, name: "轻松慢跑", detail: "约 6'40\"–7'00\"/km", amount: "5 分钟" },
        { icon: Activity, name: "动态活动", detail: "高抬腿 · 开合跳", amount: "3 分钟" },
      ],
    },
    {
      key: "main",
      index: 2,
      title: "主训练",
      summary: "从起步过渡到目标区间，保持动作和呼吸稳定",
      rows: [
        { icon: Footprints, name: "起步过渡", detail: "略慢于目标配速", amount: "2 km" },
        {
          icon: Route,
          name: mainSessionName,
          detail: `${plan.pace}/km · 心率 125–150`,
          amount: `${mainDistance.toFixed(1)} km`,
        },
      ],
    },
    {
      key: "cooldown",
      index: 3,
      title: "冷身恢复",
      summary: "逐步降低心率，完成下肢放松",
      rows: [
        { icon: Footprints, name: "放松慢走", detail: "呼吸恢复自然", amount: "3 分钟" },
        { icon: Waves, name: "下肢拉伸", detail: "每个动作保持 30 秒", amount: "6 分钟" },
      ],
    },
  ];
  return (
    <div className="page plan-detail-page">
      <PageHeader title="训练详情" />
      <section className="plan-detail-hero" aria-labelledby="plan-detail-title">
        <div className="plan-detail-hero__meta">
          <span>
            {new Date(plan.date).toLocaleDateString("zh-CN", {
              month: "long",
              day: "numeric",
              weekday: "long",
            })}
          </span>
          <small>AI 教练生成 · V{plan.version}</small>
        </div>
        <h1 id="plan-detail-title">{plan.distance} 公里 · {sessionName}</h1>
        <p>约 {estimatedMinutes} 分钟 · 目标配速 {plan.pace}/km</p>
        <div className="plan-detail-context-grid">
          <article>
            <CloudSun />
            <span><small>训练天气</small><b>多云 · 27°C</b></span>
          </article>
          <article>
            <Waves />
            <span><small>空气质量</small><b>良好 · AQI 42</b></span>
          </article>
        </div>
        <div className="plan-detail-sync-line">
          <CheckCircle2 />
          <span>
            <b>{state.planSyncReceipt?.planId === plan.id
              ? state.planSyncReceipt.kind === "adjusted"
                ? "AI 教练的调整已同步"
                : "当前计划已确认"
              : plan.status === "confirmed"
                ? "当前计划已确认"
                : "当前方案待设备确认"}</b>
            <small>{plan.status === "confirmed" ? "可开始训练" : "确认后下发设备"}</small>
          </span>
        </div>
      </section>

      <section className="plan-detail-intro">
        <span>训练详情</span>
        <h2>按三个阶段完成今天的训练</h2>
        <p>先充分热身，再进入目标训练区间；全程优先保证动作稳定，不需要主动加量。</p>
      </section>

      <div className="plan-detail-phase-stack">
        {prescriptionPhases.map((phase) => (
          <article className={`plan-detail-phase plan-detail-phase--${phase.key}`} key={phase.key}>
            <header>
              <span>{phase.index}</span>
              <div><b>{phase.title}</b><small>{phase.summary}</small></div>
            </header>
            <div className="plan-detail-phase__rows">
              {phase.rows.map((row) => {
                const RowIcon = row.icon;
                return (
                  <div key={`${phase.key}-${row.name}`}>
                    <RowIcon />
                    <span><b>{row.name}</b><small>{row.detail}</small></span>
                    <strong>{row.amount}</strong>
                  </div>
                );
              })}
            </div>
          </article>
        ))}
      </div>

      <section className="plan-detail-ai-advice">
        <div className="plan-detail-ai-advice__head">
          <span className="ai-summary__icon"><img src="/cpt-ai-coach-avatar-v4.png" alt="" aria-hidden="true" /></span>
          <div><b>CPT AI 教练建议</b><small>结合今日恢复状态与训练计划</small></div>
        </div>
        <p>跑前补足水分并完成动态热身。主训练按 {plan.pace}/km 起步；如果热身后呼吸明显急促，可将配速放慢 10–20 秒，以心率不持续超过 150 bpm 为准。</p>
        <p>跑后慢走至呼吸恢复，再完成下肢拉伸。今天的目标是把有氧基础练扎实，不追求额外里程。</p>
      </section>

      <div className="plan-detail-final-actions">
        <Button onClick={() => go("TRAIN-READY")}>
          <Play />
          {state.device.status === "synced" ? "去手表开始训练" : "连接设备并开始训练"}
        </Button>
        <button type="button" onClick={() => openPlanAgent("PLAN-04", plan.id, plan.title)}>
          <MessageCircleMore />和 AI 教练商量调整
        </button>
      </div>
    </div>
  );
}

function PlanAgentRedirect() {
  const { state, openPlanAgent } = useApp();
  const plan =
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  useEffect(() => {
    if (plan) openPlanAgent("PLAN-05", plan.id, plan.title);
  }, [plan?.id]);
  return (
    <div className="page plan-agent-redirect" role="status">
      <PageHeader title="回首页和 AI 教练聊" />
      <LoaderCircle className="spin" />
      <b>正在把这堂训练和周期安排一起带过去</b>
      <p>到首页后直接说出想替代、顺延或调整的原因，我会接着和你聊。</p>
    </div>
  );
}

function PlanConflict() {
  const { state, patch, go } = useApp();
  const [resolving, setResolving] = useState<"cloud" | "current" | null>(null);
  const resolve = async (version: "cloud" | "current") => {
    setResolving(version);
    await services.sync.resolveConflict("training-plan", version);
    if (version === "cloud") {
      const moved = new Date(state.planItems[0].date);
      moved.setDate(moved.getDate() + 1);
      patch({
        planVersion: 4,
        selectedDate: moved.toISOString().slice(0, 10),
        planItems: state.planItems.map((item, index) =>
          index === 0
            ? {
                ...item,
                date: moved.toISOString().slice(0, 10),
                version: 4,
                status: "adjusted",
                changed: true,
              }
            : { ...item, version: 4 },
        ),
        toast: "已采用云端 V4，并同步到首页、训练与记录视图",
      });
    } else patch({ toast: "已保留当前 V3，并将当前版本上传云端" });
    go("TRAIN-HUB");
  };
  return (
    <div className="page">
      <PageHeader title="计划同步冲突" />
      <SafetyBanner>
        App 与小程序在相近时间修改了同一计划，请选择保留版本。
      </SafetyBanner>
      <Card className="conflict-card">
        <span>云端版本 · V4</span>
        <b>节奏跑调整至周五</b>
        <p>修改于 10:21 · App</p>
        <Button
          disabled={Boolean(resolving)}
          onClick={() => void resolve("cloud")}
        >
          {resolving === "cloud" ? (
            <>
              <LoaderCircle className="spin" /> 写入中
            </>
          ) : (
            "采用云端版本"
          )}
        </Button>
      </Card>
      <Card className="conflict-card">
        <span>当前版本 · V3</span>
        <b>节奏跑仍在周四</b>
        <p>修改于 10:19 · 小程序</p>
        <Button
          disabled={Boolean(resolving)}
          variant="secondary"
          onClick={() => void resolve("current")}
        >
          {resolving === "current" ? "上传中…" : "保留当前版本"}
        </Button>
      </Card>
    </div>
  );
}

function TrainingDeviceHandoff() {
  const { state, go, confirmReadyPlan } = useApp();
  const plan =
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const connected = state.device.status === "synced";
  return (
    <div className="page device-handoff-page">
      <PageHeader title="设备确认" />
      <Card className="device-handoff-hero">
        <Watch />
        <span>{connected ? "运动设备已经准备好" : "连接健康平台后就能出发"}</span>
        <h1>{plan?.title ?? "今日训练计划"}</h1>
        <p>
          这堂训练会在运动设备端执行。小程序通过已连接的健康平台同步计划和完成数据，练完回来再一起复盘。
        </p>
        <StatusBadge tone={connected ? "green" : "gray"}>
          {connected ? "可在手表端调用" : "连接后自动下发"}
        </StatusBadge>
      </Card>
      <Card className="device-handoff-flow">
        <div><b>1</b><span>训练计划同步到已连接的健康平台与运动设备</span></div>
        <div><b>2</b><span>在设备端执行训练并记录配速、心率</span></div>
        <div><b>3</b><span>训练结束后数据自动回传，AI 教练在首页陪你复盘</span></div>
      </Card>
      <div className="device-sync-limit">
        <Info />
        <p>当前原型按“训练后同步”展示，不模拟实时配速与心率。标准健康数据接口兼容性仍需与设备方联调确认。</p>
      </div>
      <div className="bottom-actions">
        <Button variant="secondary" onClick={() => go("TRAIN-HUB", "training")}>返回训练界面</Button>
        <Button onClick={() => confirmReadyPlan(plan.id, plan.title)}>确认好了，返回首页</Button>
      </div>
    </div>
  );
}

function TrainingReady() {
  const { state, patch, go } = useApp();
  const [starting, setStarting] = useState(false);
  const plan =
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const canStart = state.device.gpsGranted;
  const preparedNutrition =
    state.nutritionPlansByDate[plan.date] ??
    (state.nutritionPlan?.planDate === plan.date ? state.nutritionPlan : null);
  const preparedNodes = preparedNutrition?.nodes ?? [];
  const nutritionNeedsReview =
    preparedNutrition?.contextVersion !== undefined &&
    !preparedNutrition.contextVersion.includes(`-${plan.version}-`);
  const startTraining = async () => {
    if (!canStart || starting) return;
    setStarting(true);
    try {
      const session = await services.liveTraining.start(plan.id);
      const enabledNodes = preparedNodes
        .filter(
          (node) =>
            node.phase === "训练中" &&
            node.reminder?.enabled &&
            !state.completedNutritionNodes.includes(node.id),
        )
        .sort(
          (first, second) =>
            first.reminder!.offsetSecond - second.reminder!.offsetSecond,
        );
      const scheduled = await Promise.all(
        enabledNodes.map((node) =>
          services.fuelReminder.schedule(
            session.sessionId,
            node.id,
            reminderFromNode(node).triggerSecond,
            node.reminder!.channel,
          ),
        ),
      );
      const next = enabledNodes[0] ?? null;
      const reminder = next
        ? {
            ...reminderFromNode(next),
            id: scheduled[0]?.reminderId ?? `fuel-${next.id}`,
          }
        : {
            ...state.reminder,
            enabled: false,
            status: "scheduled" as const,
            vibrationCount: 0,
            lastAction: undefined,
          };
      patch({
        live: {
          sessionId: session.sessionId,
          status: "recording",
          elapsed: 0,
          distance: 0,
          heartRate: 128,
          energy: 0,
          currentStepIndex: 0,
          stepStartedAt: 0,
          stepStartedDistance: 0,
        },
        reminder,
        planItems: state.planItems.map((p) =>
          p.id === plan.id ? { ...p, status: "in_progress" as const } : p,
        ),
      });
      go("TRAIN-LIVE");
    } catch {
      patch({ toast: "这次训练暂时没能启动。请检查网络后再试，计划不会丢失。" });
    } finally {
      setStarting(false);
    }
  };
  return (
    <div className="page">
      <PageHeader title="训练准备" />
      <div className="ready-hero">
        <span>
          {new Date(plan.date).toLocaleDateString("zh-CN", {
            month: "long",
            day: "numeric",
            weekday: "short",
          })}{" "}
          · {plan.duration}
        </span>
        <h1>{plan.title}</h1>
        <p>
          {plan.distance} km · {plan.pace}/km · 负荷 {plan.load}
        </p>
      </div>
      <div className="readiness-list">
        <StatusRow
          icon={<LocateFixed />}
          title="GPS 与定位"
          value={state.device.gpsGranted ? "已授权" : "未授权"}
          ok={state.device.gpsGranted}
          onClick={() => go("DEV-02")}
        />
        <StatusRow
          icon={<Watch />}
          title="运动设备"
          value={
            state.device.status === "synced"
              ? `${state.device.syncSources.join("、")} · 已同步`
              : "未连接"
          }
          ok={state.device.status === "synced"}
          onClick={() => go("DEV-01")}
        />
        <StatusRow
          icon={<CloudSun />}
          title="训练天气"
          value={
            state.device.weather
              ? `${state.device.weather.condition} · ${state.device.weather.temperature}°C · 湿度 ${state.device.weather.humidity}%`
              : "授权并同步设备后获取"
          }
          ok={Boolean(state.device.weather)}
        />
        <StatusRow
          icon={<Droplets />}
          title="补给方案"
          value={
            state.nutritionPlanStatus === "loading"
                ? "正在把补给方案准备好"
              : preparedNodes.length
                ? nutritionNeedsReview
                  ? "方案已随计划更新 · 一起看看"
                  : `已随计划生成 · ${preparedNodes.length} 个节点`
                : "补给还没准备好 · 去营养页看看"
          }
          ok={preparedNodes.length > 0 && !nutritionNeedsReview}
          onClick={() => go("NUT-02")}
        />
      </div>
      <Card className="fuel-ready">
        <b>补给准备提醒</b>
        {preparedNodes.length ? (
          <>
            <div>
              {preparedNodes
                .filter((node) => node.phase === "训练中")
                .map((node) => (
                  <span key={node.id}>
                    {node.product.name} · {node.product.dose}
                  </span>
                ))}
            </div>
            <button type="button" className="text-link" onClick={() => go("NUT-02")}>
              查看完整补给方案 <ChevronRight />
            </button>
          </>
        ) : (
          <p>补给方案还差一步，去营养页看一眼，准备好再出发。</p>
        )}
      </Card>
      <div className="bottom-actions">
        <Button variant="secondary" onClick={() => go("REMIND-01")}>
          延后训练提醒
        </Button>
        <Button onClick={() => go("DEV-01")}>
          <Watch size={17} /> 查看设备与平台状态
        </Button>
      </div>
      {!canStart && (
        <p className="blocking-note">还需要完成健康平台与设备授权，之后这份计划才能下发。</p>
      )}
    </div>
  );
}

function TrainingLive() {
  const { state, patch, tick, triggerReminder, reminderAction, go } = useApp();
  const [finishing, setFinishing] = useState(false);
  useEffect(() => {
    if (state.live.status !== "recording") return;
    const id = window.setInterval(tick, 1000);
    return () => window.clearInterval(id);
  }, [state.live.status, tick]);
  useEffect(() => {
    if (
      state.reminder.enabled &&
      state.live.elapsed >= state.reminder.triggerSecond &&
      state.reminder.status === "scheduled"
    ) {
      navigator.vibrate?.(160);
      triggerReminder();
    }
  }, [
    state.live.elapsed,
    state.reminder.enabled,
    state.reminder.status,
    state.reminder.triggerSecond,
    triggerReminder,
  ]);
  const elapsed = formatTime(state.live.elapsed);
  const activePlan =
    state.planItems.find((item) => item.status === "in_progress") ??
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const progress = resolveTrainingProgress(activePlan, state.live);
  const averagePaceSeconds =
    state.live.distance > 0
      ? Math.round(state.live.elapsed / state.live.distance)
      : 0;
  const remainingMetric =
    progress.step.target.metric === "time"
      ? `剩余 ${formatTime(Math.max(0, progress.stepSeconds - progress.stepElapsed))}`
      : `本段 ${Math.max(0, progress.step.target.value - (state.live.distance - state.live.stepStartedDistance)).toFixed(2)} km`;
  const overlay =
    state.reminder.enabled && state.reminder.status === "triggered";
  const handleReminderAction = async (
    action: "acknowledged" | "delayed" | "skipped",
  ) => {
    try {
      await services.fuelReminder.acknowledge(
        state.reminder.id,
        action === "acknowledged" ? "completed" : action,
      );
    } catch {
      patch({ toast: "提醒处理同步失败，请重新操作" });
      return;
    }
    reminderAction(action);
    if (action === "delayed") return;
    const next = createReminderNode(state, state.reminder.nodeId);
    if (next)
      window.setTimeout(
        () => patch({ reminder: reminderFromNode(next) }),
        action === "acknowledged" ? 1800 : 0,
      );
  };
  const finishTraining = async () => {
    if (finishing) return false;
    setFinishing(true);
    const activePlan =
      state.planItems.find((item) => item.status === "in_progress") ??
      state.planItems.find((item) => item.date === state.selectedDate) ??
      state.planItems[0];
    try {
      const result = await services.liveTraining.finish(
        state.live.sessionId || `session-${activePlan.id}`,
        activePlan,
        state.nutritionCompletion,
        state.device.syncSources.join("、") || "本机 GPS",
      );
      patch({
        activities: [
          result.activity,
          ...state.activities.filter((item) => item.id !== result.activity.id),
        ],
        lastCompletedActivityId: result.activity.id,
        postTrainingFeedbackByActivity: {
          ...state.postTrainingFeedbackByActivity,
          [result.activity.id]: {
            training: false,
            nutrition: false,
            analysisReady: false,
            detectedAt: new Date().toISOString(),
          },
        },
        feedback: { ...state.feedback, submitted: false },
        trainingAnalysis: null,
        selectedDate: activePlan.date,
        live: { ...state.live, status: "completed" },
        toast: "训练已结束，设备实绩已保存",
      });
      go("TRAIN-SUMMARY");
      return true;
    } catch {
      patch({ toast: "训练结束保存失败，请保持页面并重新长按结束" });
      setFinishing(false);
      return false;
    }
  };
  return (
    <div className="live-page">
      <div className="live-head">
        <span>
          <MapPin size={15} /> 户外跑
        </span>
        <StatusBadge tone="green">GPS 强</StatusBadge>
      </div>
      <div className="live-timer">
        <span>运动时长</span>
        <strong>{elapsed}</strong>
      </div>
      <div className="live-grid">
        <Metric value={state.live.distance.toFixed(2)} unit="km" label="距离" />
        <Metric
          value={formatPace(averagePaceSeconds)}
          unit="/km"
          label="平均配速"
        />
        <Metric value={state.live.heartRate} unit="bpm" label="心率" />
        <Metric value={state.live.energy} unit="kcal" label="消耗" />
      </div>
      <Card className="step-card">
        <div className="step-card__head">
          <span>当前步骤</span>
          <small>全程 {progress.totalProgress}%</small>
        </div>
        <b>
          {progress.step.title} · 第 {progress.index + 1}/
          {progress.steps.length} 段
        </b>
        <div className="progress-bar">
          <span style={{ width: `${progress.stepProgress}%` }} />
        </div>
        <div className="step-card__meta">
          <span>{remainingMetric}</span>
          {progress.step.pace && <span>目标 {progress.step.pace}/km</span>}
        </div>
        <p>{progress.step.instruction}</p>
        {progress.nextStep && (
          <small className="step-card__next">
            下一步：{progress.nextStep.title}
          </small>
        )}
      </Card>
      {!state.reminder.enabled && (
        <div className="live-status">
          <Bell /> 训练中补给提醒已关闭
        </div>
      )}
      {["acknowledged", "skipped"].includes(state.reminder.status) && (
        <div className="live-status">
          <CheckCircle2 />{" "}
          {state.reminder.status === "acknowledged"
            ? "补给已记录 · 手机与手表已同步"
            : "本次补给已跳过"}
        </div>
      )}
      {state.reminder.status === "scheduled" &&
        state.reminder.lastAction === "delayed" && (
          <div className="live-status">
            <CheckCircle2 /> 已延后 10 分钟 · 将在{" "}
            {formatTime(state.reminder.triggerSecond)} 再次提醒
          </div>
        )}
      {state.reminder.status === "minimized_pending" && (
        <button
          className="pending-reminder"
          onClick={() =>
            patch({ reminder: { ...state.reminder, status: "triggered" } })
          }
        >
          <Bell /> 有 1 条待处理补给提醒
        </button>
      )}
      <div className="live-controls">
        <button
          onClick={() =>
            patch({
              live: {
                ...state.live,
                status: state.live.status === "paused" ? "recording" : "paused",
              },
            })
          }
        >
          {state.live.status === "paused" ? <Play /> : <Pause />}
        </button>
        <HoldToFinishButton disabled={finishing} onComplete={finishTraining} />
      </div>
      {overlay && (
        <FuelOverlay
          reminder={state.reminder}
          onAction={handleReminderAction}
          onMinimize={() =>
            patch({
              reminder: { ...state.reminder, status: "minimized_pending" },
            })
          }
        />
      )}
    </div>
  );
}

function HoldToFinishButton({
  disabled = false,
  onComplete,
}: {
  disabled?: boolean;
  onComplete: () => boolean | Promise<boolean>;
}) {
  const holdDuration = 2000;
  const [holding, setHolding] = useState(false);
  const [progress, setProgress] = useState(0);
  const startedAt = useRef(0);
  const frame = useRef<number | null>(null);
  const completed = useRef(false);

  const stopFrame = () => {
    if (frame.current !== null) window.cancelAnimationFrame(frame.current);
    frame.current = null;
  };
  const cancelHold = () => {
    if (completed.current) return;
    stopFrame();
    startedAt.current = 0;
    setHolding(false);
    setProgress(0);
  };
  const finishHold = () => {
    if (completed.current) return;
    completed.current = true;
    stopFrame();
    setProgress(100);
    setHolding(false);
    navigator.vibrate?.(45);
    void Promise.resolve(onComplete()).then((success) => {
      if (success) return;
      completed.current = false;
      setProgress(0);
    });
  };
  const updateProgress = (now: number) => {
    const elapsed = now - startedAt.current;
    const nextProgress = Math.min(100, (elapsed / holdDuration) * 100);
    setProgress(nextProgress);
    if (elapsed >= holdDuration) finishHold();
    else frame.current = window.requestAnimationFrame(updateProgress);
  };
  const startHold = () => {
    if (disabled || holding || completed.current) return;
    startedAt.current = performance.now();
    setHolding(true);
    setProgress(0);
    frame.current = window.requestAnimationFrame(updateProgress);
  };
  useEffect(() => () => stopFrame(), []);

  const secondsLeft = Math.max(0, (holdDuration * (1 - progress / 100)) / 1000);
  return (
    <button
      type="button"
      disabled={disabled}
      className={`hold-finish-button ${holding ? "is-holding" : ""}`}
      aria-label="持续按住两秒结束训练"
      aria-describedby="hold-finish-help"
      onContextMenu={(event) => event.preventDefault()}
      onPointerDown={(event) => {
        if (event.button !== 0) return;
        event.preventDefault();
        event.currentTarget.setPointerCapture(event.pointerId);
        startHold();
      }}
      onPointerUp={cancelHold}
      onPointerCancel={cancelHold}
      onPointerLeave={cancelHold}
      onPointerMove={(event) => {
        if (!holding) return;
        const rect = event.currentTarget.getBoundingClientRect();
        if (
          event.clientX < rect.left ||
          event.clientX > rect.right ||
          event.clientY < rect.top ||
          event.clientY > rect.bottom
        )
          cancelHold();
      }}
      onKeyDown={(event) => {
        if ((event.key === " " || event.key === "Enter") && !event.repeat) {
          event.preventDefault();
          startHold();
        }
      }}
      onKeyUp={(event) => {
        if (event.key === " " || event.key === "Enter") cancelHold();
      }}
      onBlur={cancelHold}
    >
      <span className="hold-finish-button__track" aria-hidden="true">
        <i style={{ transform: `scaleX(${progress / 100})` }} />
      </span>
      <span className="hold-finish-button__content">
        <b>
          {disabled
            ? "正在保存训练…"
            : holding
              ? `继续按住 ${secondsLeft.toFixed(1)} 秒`
              : "按住 2 秒结束"}
        </b>
        <small id="hold-finish-help">
          {holding ? "松开或移出即可取消" : "防止训练中误触"}
        </small>
      </span>
      <span
        className="sr-only"
        role="progressbar"
        aria-label="结束训练长按进度"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Math.round(progress)}
      />
    </button>
  );
}

function FuelOverlay({
  reminder,
  onAction,
  onMinimize,
}: {
  reminder: ReturnType<typeof useApp>["state"]["reminder"];
  onAction: (action: "acknowledged" | "delayed" | "skipped") => void;
  onMinimize: () => void;
}) {
  useEffect(() => {
    const id = window.setTimeout(onMinimize, 12000);
    return () => window.clearTimeout(id);
  }, [onMinimize]);
  return (
    <div
      className="fuel-overlay"
      role="dialog"
      aria-modal="true"
      aria-label={`${reminder.productName}补给提醒`}
    >
      <div className="fuel-overlay__pulse">
        <Zap />
      </div>
      <span>补给时机已到 · 单次震动完成</span>
      <h2>{reminder.triggerLabel}</h2>
      <Card>
        <div className="product-token">
          <Zap />
        </div>
        <div>
          <b>{reminder.productName}</b>
          <p>{reminder.amount}</p>
          <small>来自当前 Agent 补给方案节点</small>
        </div>
      </Card>
      <Button onClick={() => onAction("acknowledged")}>已补充</Button>
      <Button variant="secondary" onClick={() => onAction("delayed")}>
        延后 10 分钟
      </Button>
      <button className="text-action" onClick={() => onAction("skipped")}>
        跳过本次
      </button>
    </div>
  );
}

function TrainingSummary() {
  const { state, patch, go, sendAnalysisToHome } = useApp();
  const activity =
    state.activities.find(
      (item) => item.id === state.lastCompletedActivityId,
    ) ??
    state.activities.find((item) => item.date === state.selectedDate) ??
    state.activities[0];
  const plan =
    state.planItems.find((item) => item.id === activity?.planItemId) ??
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const durationSeconds = activity?.duration.includes(":")
    ? activity.duration
        .split(":")
        .reduce(
          (sum, value, index, values) =>
            sum + Number(value) * (index === values.length - 1 ? 1 : 60),
          0,
        )
    : Number(activity?.duration.match(/\d+/)?.[0] ?? 0) * 60;
  const plannedDurationSeconds =
    Number(plan?.duration.match(/\d+/)?.[0] ?? 0) * 60;
  const distanceDelta = (activity?.distance ?? 0) - (plan?.distance ?? 0);
  const durationDelta = durationSeconds - plannedDurationSeconds;
  const nutritionNodes =
    state.nutritionPlansByDate[plan?.date ?? ""]?.nodes ??
    state.nutritionPlan?.nodes ??
    [];
  const postTrainingState = activity
    ? state.postTrainingFeedbackByActivity[activity.id]
    : null;
  const fullAnalysisUnlocked = Boolean(
    postTrainingState?.training && postTrainingState?.nutrition,
  );
  const openTrainingRecords = () => {
    const activePlan =
      state.planItems.find((item) => item.status === "in_progress") ??
      state.planItems.find((item) => item.date === state.selectedDate) ??
      state.planItems[0];
    patch({
      trainingView: "record",
      selectedDate: activePlan.date,
      live: { ...state.live, status: "completed" },
    });
    go("TRAIN-HUB", "training");
  };
  return (
    <div className="page">
      <PageHeader title="训练总结" onBack={openTrainingRecords} />
      <div className="summary-hero">
        <CheckCircle2 />
        <span>今天这堂，稳稳拿下</span>
        <h1>{activity?.distance.toFixed(2) ?? "0.00"} 公里</h1>
        <p>
          {activity?.duration ?? "00:00"} · 平均配速{" "}
          {activity?.pace ?? "--′--″"}/km
        </p>
      </div>
      <Card className="metric-panel">
        <div className="metric-grid">
          <Metric
            value={activity?.heartRate ?? 0}
            unit="bpm"
            label="平均心率"
          />
          <Metric value={plan?.load ?? 0} label="训练负荷" />
          <Metric
            value={activity?.energy ?? Math.round(state.live.energy)}
            unit="kcal"
            label="消耗"
          />
        </div>
      </Card>
      <Card className="comparison-box">
        <b>计划 vs 实际</b>
        <div>
          <span>距离</span>
          <strong>
            {distanceDelta >= 0 ? "+" : ""}
            {distanceDelta.toFixed(2)} km
          </strong>
        </div>
        <div>
          <span>用时</span>
          <strong>
            {durationDelta === 0
              ? "与计划一致"
              : `${durationDelta > 0 ? "多" : "少"} ${formatTime(Math.abs(durationDelta))}`}
          </strong>
        </div>
        <div>
          <span>补给完成度</span>
          <strong>{activity?.nutritionCompletion ?? 0}%</strong>
        </div>
      </Card>
      <AiSummary
        title={fullAnalysisUnlocked ? "完整训练分析已经生成" : "补齐两项后解锁完整分析"}
        summary={fullAnalysisUnlocked && state.trainingAnalysis
          ? state.trainingAnalysis.summary
          : "设备数据已经保存；训练感受与补给执行都完成后，首页和训练记录会同步出现完整分析。"}
        onClick={async () => {
          if (!activity || !plan) return;
          if (!fullAnalysisUnlocked) {
            go("TRAIN-FEEDBACK");
            return;
          }
          try {
            const result = await services.agent.analyzeTraining(
              plan,
              activity,
              nutritionNodes,
              state.completedNutritionNodesByDate[plan.date] ?? state.completedNutritionNodes,
              state.feedback,
            );
            patch({ trainingAnalysis: result.data });
            sendAnalysisToHome(
              "本次训练表现",
              "TRAIN-SUMMARY",
              `${result.data.summary}\n\n依据：${result.data.evidence.join("、")}\n上下文版本：${result.data.contextVersion}`,
              result.data,
            );
          } catch {
            patch({ toast: "这次分析暂时没生成出来，训练数据已经保存，可以稍后再看。" });
          }
        }}
      />
      <Button className="page-primary" onClick={() => go("TRAIN-FEEDBACK")}>
        告诉我这次跑得怎么样
      </Button>
    </div>
  );
}

function TrainingFeedback() {
  const { state, feedback, patch, go } = useApp();
  const [submitting, setSubmitting] = useState(false);
  const [fatigueCause, setFatigueCause] = useState("尚未选择");
  const completedActivity =
    state.activities.find(
      (item) => item.id === state.lastCompletedActivityId,
    ) ?? state.activities.find((item) => item.date === state.selectedDate);
  const activePlan =
    state.planItems.find((item) => item.id === completedActivity?.planItemId) ??
    state.planItems.find((item) => item.status === "in_progress") ??
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const paceSeconds = (value?: string) => {
    const match = value?.match(/(\d+)[′'](\d+)/);
    return match ? Number(match[1]) * 60 + Number(match[2]) : 0;
  };
  const paceDelta = completedActivity
    ? paceSeconds(completedActivity.pace) - paceSeconds(activePlan.pace)
    : 0;
  const paceFinding = Math.abs(paceDelta) <= 15
    ? "配速符合目标"
    : paceDelta < 0
      ? `平均每公里快 ${Math.abs(paceDelta)} 秒`
      : `平均每公里慢 ${paceDelta} 秒`;
  const paceFindingTone = Math.abs(paceDelta) <= 15 ? "green" : "orange";
  const submitFeedback = async () => {
    setSubmitting(true);
    try {
      if (!completedActivity) throw new Error("missing completed activity");
      await services.sync.push(["activity", "plan", "nutrition"]);
      const submittedFeedback = { ...state.feedback, submitted: true };
      const nutritionNodes =
        state.nutritionPlansByDate[activePlan.date]?.nodes ??
        state.nutritionPlan?.nodes ??
        [];
      const feedbackCompletedNodes =
        state.completedNutritionNodesByDate[activePlan.date] ??
        state.completedNutritionNodes;
      const postTrainingState = state.postTrainingFeedbackByActivity[
        completedActivity.id
      ] ?? {
        training: false,
        nutrition: false,
        analysisReady: false,
        detectedAt: new Date().toISOString(),
      };
      const nutritionFeedbackReady = postTrainingState.nutrition;
      const analysisResult = nutritionFeedbackReady
        ? await services.agent.analyzeTraining(
            activePlan,
            completedActivity,
            nutritionNodes,
            feedbackCompletedNodes,
            submittedFeedback,
          )
        : null;
      const completed = state.planItems.map((item) =>
        item.id === activePlan.id ? { ...item, status: "completed" as const } : item,
      );
      patch({
        planItems: completed,
        selectedDate: activePlan.date,
        trainingView: "record",
        feedback: submittedFeedback,
        postTrainingFeedbackByActivity: {
          ...state.postTrainingFeedbackByActivity,
          [completedActivity.id]: {
            ...postTrainingState,
            training: true,
            analysisReady: nutritionFeedbackReady,
          },
        },
        trainingAnalysis: analysisResult?.data ?? null,
        live: { ...state.live, status: "completed" },
        toast: nutritionFeedbackReady
          ? "两项反馈都已补齐，完整训练分析已经生成。"
          : "训练感受已保存，再补充补给执行就能生成完整分析。",
      });
      if (analysisResult) {
        patch({
          conversation: [
            ...state.conversation,
            {
              id: crypto.randomUUID(),
              role: "user",
              text: "我已经完成训练感受反馈",
              time: "刚刚",
            },
            {
              id: crypto.randomUUID(),
              role: "agent",
              text: "两项反馈都收到了。我已经把设备实绩、训练感受和补给执行放在一起，下面是这次训练的机能情况与完整总结。",
              time: "刚刚",
              card: "training_performance",
              sources: ["设备训练实绩", "训练感受反馈", "补给执行反馈", "本次训练计划"],
              contextVersion: analysisResult.data.contextVersion,
              analysis: analysisResult.data,
              activityId: completedActivity.id,
            },
          ],
        });
        go("HOME-01", "home");
      } else {
        patch({
          conversation: [
            ...state.conversation,
            {
              id: crypto.randomUUID(),
              role: "user",
              text: "我已经完成训练感受反馈",
              time: "刚刚",
            },
            {
              id: crypto.randomUUID(),
              role: "agent",
              text: "训练感受收到。再告诉我这次补给完成得怎么样，我就能结合设备数据生成完整训练分析。",
              time: "刚刚",
              card: "post_training_checkin",
              sources: ["训练感受反馈", "设备训练实绩", "训练后反馈状态"],
              contextVersion: `post-training-${completedActivity.id}-training-ready`,
              activityId: completedActivity.id,
            },
          ],
        });
        go("HOME-01", "home");
      }
    } catch {
      patch({ toast: "体感反馈暂时没同步上，但训练数据不会丢失，请再试一次。" });
    } finally {
      setSubmitting(false);
    }
  };
  return (
    <div className="page feedback-conversation-page">
      <PageHeader title="训练后 AI 复盘" />
      <div className="feedback-conversation">
        <div className="message-row message-row--agent">
          <CoachAvatar />
          <div className="message-stack">
            <div className="message-bubble"><span className="feedback-step-label">先为你庆祝 · 1/3</span><b>今天这堂练完了，辛苦了。</b><br />数据已经回来，我们先对比目标，再听听身体怎么说。</div>
          </div>
        </div>
        <Card className="feedback-objective-card">
          <div className="feedback-objective-card__head">
            <span>客观数据对比 · 2/3</span>
            <StatusBadge tone={paceFindingTone}>{paceFinding}</StatusBadge>
          </div>
          <div><b>{activePlan.distance.toFixed(1)} / {completedActivity?.distance.toFixed(1) ?? "--"} km</b><small>距离</small></div>
          <div><b>{activePlan.pace} / {completedActivity?.pace ?? "--"}</b><small>平均配速</small></div>
          <div><b>{completedActivity?.heartRate ?? "--"} bpm</b><small>平均心率</small></div>
          <p>这是训练完成后的汇总判断，不模拟实时同步；AI 会把配速偏差与下方体感一起分析。</p>
        </Card>
        <div className="message-row message-row--agent feedback-question">
          <CoachAvatar />
          <div className="message-stack"><div className="message-bubble"><span className="feedback-step-label">补充体感 · 3/3</span>这次整体体感最接近哪一种？</div></div>
        </div>
        <div className="feedback-quick-replies">
          {[
            ["轻松", 6],
            ["符合预期", 11],
            ["后程吃力", 15],
            ["非常疲劳", 18],
          ].map(([label, rpe]) => (
            <button
              key={label}
              className={state.feedback.rpe === rpe ? "is-selected" : ""}
              onClick={() => feedback({ rpe: Number(rpe) })}
            >{label}</button>
          ))}
        </div>
        <div className="message-row message-row--agent feedback-question">
          <CoachAvatar />
          <div className="message-stack"><div className="message-bubble">如果这次有些累，你觉得最可能卡在哪里？</div></div>
        </div>
        <div className="feedback-cause-grid">
          {[
            ["强度或配速", "中等", "舒适"],
            ["睡眠与恢复", "明显", "舒适"],
            ["补水与能量", "轻微", "舒适"],
            ["肠胃不适", "轻微", "明显不适"],
          ].map(([label, soreness, gi]) => (
            <button
              key={label}
              className={fatigueCause === label ? "is-selected" : ""}
              onClick={() => {
                setFatigueCause(label);
                feedback({ soreness, gi });
              }}
            >{label}</button>
          ))}
        </div>
        <Card className="feedback-ai-preview">
          <Sparkles />
          <div><b>训练感受会和设备数据放在一起</b><p>{paceFinding} + 心率 {completedActivity?.heartRate ?? "--"} bpm + 体感 RPE {state.feedback.rpe} + 疲劳归因「{fatigueCause}」</p></div>
        </Card>
      </div>
      <Button className="page-primary" disabled={submitting || !completedActivity} onClick={() => void submitFeedback()}>
        {submitting ? "正在保存训练感受…" : "保存训练感受"}
      </Button>
    </div>
  );
}

function TrainingAnalysis() {
  const { state, patch, applyAdjustment, go, sendAnalysisToHome, openNutritionFeedback } = useApp();
  const [adjusting, setAdjusting] = useState(false);
  const [analysis, setAnalysis] = useState<TrainingAnalysisData | null>(
    state.trainingAnalysis,
  );
  const [analysisStatus, setAnalysisStatus] = useState<
    "loading" | "ready" | "error"
  >(state.trainingAnalysis ? "ready" : "loading");
  const activity =
    state.activities.find((item) => item.date === state.selectedDate) ??
    state.activities.find(
      (item) =>
        item.planItemId ===
        state.planItems.find((plan) => plan.status === "completed")?.id,
    ) ??
    null;
  const plan =
    state.planItems.find((item) => item.id === activity?.planItemId) ??
    state.planItems.find((item) => item.date === state.selectedDate) ??
    null;
  const feedbackContextSuffix = state.feedback.submitted
    ? `feedback-${state.feedback.rpe}-${state.feedback.thirst}-${state.feedback.soreness}-${state.feedback.gi}`
    : "objective-only";
  const analysisNutritionNodes =
    state.nutritionPlansByDate[plan?.date ?? ""]?.nodes ??
    state.nutritionPlan?.nodes ??
    [];
  const completedNutritionNodes =
    state.completedNutritionNodesByDate[plan?.date ?? ""] ??
    state.completedNutritionNodes;
  const completedNutritionCount = analysisNutritionNodes.filter((node) =>
    completedNutritionNodes.includes(node.id),
  ).length;
  const analysisContextSuffix = `nutrition-${completedNutritionCount}-of-${analysisNutritionNodes.length}-${activity?.nutritionCompletion ?? 0}-${feedbackContextSuffix}`;
  const storedAnalysisIsCurrent = Boolean(
    state.trainingAnalysis &&
      state.trainingAnalysis.planItemId === plan?.id &&
      state.trainingAnalysis.activityId === activity?.id &&
      state.trainingAnalysis.contextVersion.endsWith(analysisContextSuffix),
  );
  const loadAnalysis = (force = false) => {
    if (!plan || !activity) {
      setAnalysisStatus("error");
      return;
    }
    if (!force && storedAnalysisIsCurrent && state.trainingAnalysis) {
      setAnalysis(state.trainingAnalysis);
      setAnalysisStatus("ready");
      return;
    }
    setAnalysisStatus("loading");
    services.agent
      .analyzeTraining(
        plan,
        activity,
        analysisNutritionNodes,
        completedNutritionNodes,
        state.feedback,
      )
      .then((result) => {
        setAnalysis(result.data);
        patch({ trainingAnalysis: result.data });
        // Keep the rendered analysis and the Home conversation handoff on one Agent result.
        setAnalysisStatus("ready");
      })
      .catch(() => setAnalysisStatus("error"));
  };
  useEffect(() => {
    if (storedAnalysisIsCurrent && state.trainingAnalysis) {
      setAnalysis(state.trainingAnalysis);
      setAnalysisStatus("ready");
      return;
    }
    loadAnalysis();
  }, [
    plan?.id,
    activity?.id,
    completedNutritionNodes.join("|"),
    analysisNutritionNodes.map((node) => node.id).join("|"),
    activity?.nutritionCompletion,
    state.feedback.submitted,
    state.feedback.rpe,
    state.feedback.thirst,
    state.feedback.soreness,
    state.feedback.gi,
    storedAnalysisIsCurrent,
    state.trainingAnalysis?.contextVersion,
  ]);
  if (!plan || !activity)
    return (
      <div className="page">
        <PageHeader title="训练分析" />
        <EmptyState
          title="还在等你的第一条训练实绩"
          detail="练完并同步设备后，我会保留原计划，把实际表现逐项对齐给你看。"
          action={<Button onClick={() => go("TRAIN-HUB")}>返回训练中心</Button>}
        />
      </div>
    );
  if (analysisStatus === "loading")
    return (
      <div className="page">
        <PageHeader title="训练分析" />
        <div className="skeleton-stack" role="status">
          <div />
          <div />
          <div />
        </div>
        <p className="loading-copy">AI 教练正在把计划、设备实绩和补给记录放在一起看…</p>
      </div>
    );
  if (analysisStatus === "error" || !analysis)
    return (
      <div className="page">
        <PageHeader title="训练分析" />
        <EmptyState
          title="这次对比暂时没整理出来"
          detail="计划和实际记录都已经保留，不会覆盖原数据。"
          action={<Button onClick={() => loadAnalysis(true)}>再分析一次</Button>}
        />
      </div>
    );
  const matchedRows = analysis.rows.filter((row) => row.status === "met").length;
  const executionScore = Math.round((matchedRows / analysis.rows.length) * 100);
  return (
    <div className="page training-analysis-page">
      <PageHeader title="训练分析" eyebrow="POST-RUN REPORT" />
      <Card className="training-analysis-hero">
        <div className="training-analysis-hero__topline">
          <span><CheckCircle2 /> 设备实绩已同步</span>
          <small>{new Date(plan.date).toLocaleDateString("zh-CN", { month: "long", day: "numeric" })}</small>
        </div>
        <div className="training-analysis-hero__title">
          <div>
            <span>{plan.title}</span>
            <h1>{analysis.headline}</h1>
          </div>
          <div className="execution-score" aria-label={`计划执行匹配度 ${executionScore} 分`}>
            <strong>{executionScore}</strong>
            <small>匹配度</small>
          </div>
        </div>
        <div className="training-analysis-stats" aria-label="本次训练实绩">
          <div>
            <Route />
            <span>完成距离</span>
            <strong>{activity.distance.toFixed(2)}<small> km</small></strong>
          </div>
          <div>
            <Gauge />
            <span>平均配速</span>
            <strong>{activity.pace}<small> /km</small></strong>
          </div>
          <div>
            <HeartPulse />
            <span>平均心率</span>
            <strong>{activity.heartRate}<small> bpm</small></strong>
          </div>
        </div>
      </Card>
      <Card className="analysis-copy">
        <div className="analysis-section-label">
          <span><Sparkles /> CPT AI 教练结论</span>
          <StatusBadge tone="green">已经看完</StatusBadge>
        </div>
        <p>{analysis.summary}</p>
        <div className="analysis-evidence" aria-label="分析依据">
          {analysis.evidence.map((item) => (
            <span key={item}>{item}</span>
          ))}
        </div>
      </Card>
      <Card className="analysis-plan">
        <div className="analysis-plan__head">
          <div>
            <span>EXECUTION DETAILS</span>
            <b>计划与实绩</b>
          </div>
          <small>同一训练逐项对齐</small>
        </div>
        <div
          className="training-compare-table"
          role="table"
          aria-label="计划与实际训练完成情况对比"
        >
          <div className="training-compare-head" role="row">
            <span role="columnheader">指标</span>
            <span role="columnheader">计划</span>
            <span role="columnheader">实际</span>
            <span role="columnheader">结果</span>
          </div>
          {analysis.rows.map((row) => (
            <div className="training-compare-row" role="row" key={row.key}>
              <b role="rowheader">{row.label}</b>
              <span role="cell">{row.planned}</span>
              <span role="cell">{row.actual}</span>
              <strong role="cell" className={`is-${row.status}`}>
                <i aria-hidden="true" />{row.difference}
              </strong>
            </div>
          ))}
        </div>
      </Card>
      <div className="analysis-followup">
        <AiSummary
          title="还有哪里想一起聊聊？"
          summary="可以追问训练表现、分析依据，也可以直接告诉我这次补给完成得怎么样。"
          onClick={() => {
            patch({ trainingAnalysis: analysis });
            sendAnalysisToHome(
              "训练计划与实际深度分析",
              "TRAIN-ANALYSIS",
              `${analysis.summary}\n\n依据：${analysis.evidence.join("、")}\n上下文版本：${analysis.contextVersion}`,
            );
          }}
        />
        <button
          type="button"
          className="analysis-nutrition-feedback"
          onClick={() => openNutritionFeedback("TRAIN-ANALYSIS")}
        >
          <MessageCircleMore aria-hidden="true" />
          <span><b>和 AI 教练说说补给情况</b><small>一句话告诉我完成了哪些、漏了哪些</small></span>
          <ChevronRight aria-hidden="true" />
        </button>
      </div>
      <Card className="adjustment-card">
        <div className="adjustment-card__head">
          <span><TrendingUp /></span>
          <div>
            <small>NEXT SESSION</small>
            <b>下一次训练建议</b>
          </div>
        </div>
        <p>{analysis.adjustment}</p>
        <div className="bottom-actions">
          <Button variant="secondary" onClick={() => go("TRAIN-HUB")}>
            先按原计划走
          </Button>
          <Button
            disabled={adjusting}
            onClick={async () => {
              setAdjusting(true);
              const applied = await applyAdjustment(analysis.adjustment);
              setAdjusting(false);
              if (applied) go("TRAIN-HUB");
            }}
          >
            {adjusting ? "正在调整…" : "采用这个建议"}
          </Button>
        </div>
      </Card>
    </div>
  );
}

function NutritionAgentRedirect() {
  const { state, patch, go } = useApp();
  useEffect(() => {
    const prompts: Partial<Record<RouteId, string>> = {
      "NUT-04": "请结合今天的训练告诉我还需要喝多少水",
      "NUT-05": "我想反馈今天吃了什么，请结合训练给我建议",
      "NUT-06": "请帮我判断这份食物是否适合今天的训练补给",
      "NUT-07": "请结合今天的训练生成简单的补给和饮食建议",
    };
    patch({ composerDraft: prompts[state.route] ?? "请结合今天训练给我营养建议" });
    go("HOME-01", "home");
  }, []);
  return (
    <div className="page">
      <PageHeader title="回首页问 AI 教练" />
      <Card className="plan-agent-redirect" role="status">
        <LoaderCircle className="spin" />
        <div><b>正在把今天的训练一起带过去</b><p>到首页直接说想吃什么、哪里不舒服或想怎么调整，我会接着回答。</p></div>
      </Card>
    </div>
  );
}

function TodayFuelingPage() {
  const { state, patch, go, openNutritionFeedback } = useApp();
  const todayIso = new Date().toISOString().slice(0, 10);
  const plan = state.nutritionPlansByDate[todayIso] ?? state.nutritionPlan;
  const completed = state.completedNutritionNodesByDate[todayIso] ?? state.completedNutritionNodes;
  const fallback = [
    { phase: "训练前", time: "前 30 分钟", target: "300 ml 水 + 25 g 碳水能量" },
    { phase: "训练中", time: "每 45 分钟", target: "能量补给 + 150–250 ml 水" },
    { phase: "训练后", time: "结束后 30 分钟", target: "20–25 g 蛋白质 + 碳水能量" },
  ];
  const completion = plan?.nodes.length
    ? Math.round((plan.nodes.filter((node) => completed.includes(node.id)).length / plan.nodes.length) * 100)
    : 0;
  return (
    <div className="page today-fueling-page">
      <PageHeader title="训练补给" />
      <Card className="today-fueling-summary">
        <div>
          <small>跟随今日训练</small>
          <h1>今天怎么补</h1>
        <p>按训练前、中、后排好水和能量；日常饮食问题回首页问 AI 教练。</p>
        </div>
        <strong>{completion}<small>%</small></strong>
      </Card>
      <div className="today-fueling-timeline">
        {fallback.map((item, index) => {
          const nodes = plan?.nodes.filter((node) => node.phase === item.phase) ?? [];
          return (
            <article className="today-fueling-phase" key={item.phase}>
              <b>{index + 1}</b>
              <div>
                <span>{item.phase}<small>{item.time}</small></span>
                <h2>{nodes.map((node) => `${node.product.name} ${node.product.dose}`).join(" + ") || item.target}</h2>
                <p>{nodes.map((node) => node.product.reason).filter(Boolean).join("；") || "根据今日训练负荷与恢复状态动态调整。"}</p>
              </div>
              <StatusBadge tone={nodes.some((node) => completed.includes(node.id)) ? "green" : "gray"}>
                {nodes.some((node) => completed.includes(node.id)) ? "已完成" : "待执行"}
              </StatusBadge>
            </article>
          );
        })}
      </div>
      <button
        type="button"
        className="today-fueling-gi"
        aria-label="查看肠胃训练方法"
        onClick={() => go("NUT-10")}
      >
        <Waves />
        <span>
          <b>让肠胃跟上补给</b>
          <small>从少量多次开始，练出更舒服、更稳定的补给节奏</small>
        </span>
        <em>查看方法</em>
        <ChevronRight />
      </button>
      <div className="today-fueling-actions">
        <Button variant="secondary" onClick={() => go("NUT-02")}>查看补给安排</Button>
          <Button onClick={() => openNutritionFeedback("NUT-01")}>告诉 AI 教练完成得怎样</Button>
      </div>
        <p className="today-fueling-footnote">不记得也没关系：未反馈节点会按 0 显示或暂不纳入分析，不需要再填传统表单。</p>
    </div>
  );
}

function GutTrainingGuide() {
  const { back } = useApp();
  return (
    <div className="page gut-training-page">
      <PageHeader title="肠胃训练方法" onBack={back} />

      <section className="gut-training-hero" aria-labelledby="gut-training-title">
        <span className="gut-training-hero__icon"><Waves aria-hidden="true" /></span>
        <div>
          <small>补给适应练习</small>
          <h1 id="gut-training-title">让每一次补给，都更从容</h1>
          <p>不用一次吃很多。通过少量、多次的练习，让身体逐步适应运动中的能量和水分补充。</p>
        </div>
        <span className="gut-training-preserved"><CheckCircle2 /> 本次训练已保留</span>
      </section>

      <section className="gut-training-section" aria-labelledby="gut-principles-title">
        <div className="gut-training-section__heading">
          <span><BookOpen aria-hidden="true" /></span>
          <div><small>01 · PRINCIPLES</small><h2 id="gut-principles-title">基本原则</h2></div>
        </div>
        <ul className="gut-training-principles">
          <li><Check aria-hidden="true" /><span><b>少量多次</b><small>避免一次摄入过多，减轻肠胃压力。</small></span></li>
          <li><Check aria-hidden="true" /><span><b>循序渐进</b><small>从容易耐受的剂量开始，再根据反馈逐步增加。</small></span></li>
          <li><Check aria-hidden="true" /><span><b>训练中先试</b><small>比赛使用的饮料、能量胶或食物，都应提前练习。</small></span></li>
          <li><Check aria-hidden="true" /><span><b>身体反馈优先</b><small>出现胀气、恶心或腹痛时及时减量或停止。</small></span></li>
        </ul>
      </section>

      <section className="gut-training-section" aria-labelledby="gut-why-title">
        <div className="gut-training-section__heading">
          <span><HeartPulse aria-hidden="true" /></span>
          <div><small>02 · WHY</small><h2 id="gut-why-title">为什么要练</h2></div>
        </div>
        <div className="gut-training-benefits">
          <div><b>更好耐受</b><p>逐渐适应运动中摄入碳水和液体。</p></div>
          <div><b>更少不适</b><p>降低补给后胀气、反胃等情况发生的可能。</p></div>
          <div><b>更稳补给</b><p>提前找到适合自己的种类、频率和剂量。</p></div>
        </div>
      </section>

      <section className="gut-training-section" aria-labelledby="gut-how-title">
        <div className="gut-training-section__heading">
          <span><Route aria-hidden="true" /></span>
          <div><small>03 · HOW TO</small><h2 id="gut-how-title">这次怎么练</h2></div>
        </div>
        <ol className="gut-training-steps">
          <li><b>训练前</b><p>准备熟悉的补给，确认本次要练习的摄入目标。</p></li>
          <li><b>训练中</b><p>按计划少量多次补充，例如每 30 分钟一次，不追求一步到位。</p></li>
          <li><b>训练后</b><p>记录摄入量，以及是否出现胀气、恶心、反胃或腹痛。</p></li>
          <li><b>下次调整</b><p>耐受良好再小幅增加；出现不适则减少或更换补给。</p></li>
        </ol>
      </section>

      <div className="gut-training-safety">
        <Info aria-hidden="true" />
        <p>短时间训练通常不必强行补给；如果不适持续或明显加重，请停止练习并咨询专业人员。</p>
      </div>

      <div className="bottom-actions gut-training-actions">
        <Button onClick={back}>了解了，继续本次训练</Button>
      </div>
    </div>
  );
}

function NutritionHome() {
  const { state, patch, go, openNutritionFeedback } = useApp();
  const [reminderBusy, setReminderBusy] = useState("");
  const [calendarScope, setCalendarScope] = useState<"week" | "cycle">("week");
  const todayIso = new Date().toISOString().slice(0, 10);
  const selectedTrainingPlan = state.planItems.find((item) => item.date === state.selectedDate) ?? null;
  const selectedNutritionPlan = state.nutritionPlansByDate[state.selectedDate] ?? null;
  const selectNutritionDate = (selectedDate: string) => {
    const nutritionPlan = state.nutritionPlansByDate[selectedDate] ?? null;
    patch({
      selectedDate,
      nutritionPlan,
      nutritionPlanStatus: nutritionPlan ? "ready" : "idle",
    });
  };
  const returnNutritionToToday = () => {
    selectNutritionDate(todayIso);
    setCalendarScope("week");
  };
  useEffect(() => {
    if (
      !state.profileComplete ||
      state.planGenerationStatus !== "ready" ||
      selectedNutritionPlan ||
      state.nutritionPlanStatus === "loading"
    )
      return;
    patch({ nutritionPlanStatus: "loading" });
    const planId = selectedTrainingPlan?.id ?? `recovery-${state.selectedDate}`;
    services.nutrition
      .buildPlan(planId, buildAgentContext(state), state.selectedDate)
      .then((result) => {
        const dailyPlan = selectedTrainingPlan
          ? result.data
          : {
              ...result.data,
              title: "恢复日营养计划",
              nodes: [],
            };
        const completed = dailyPlan.nodes
          .filter((node) => node.completedAt)
          .map((node) => node.id);
        patch({
          nutritionPlan: dailyPlan,
          nutritionPlansByDate: {
            ...state.nutritionPlansByDate,
            [state.selectedDate]: dailyPlan,
          },
          nutritionPlanStatus: "ready",
          completedNutritionNodes:
            state.selectedDate === new Date().toISOString().slice(0, 10)
              ? completed
              : state.completedNutritionNodes,
          completedNutritionNodesByDate: {
            ...state.completedNutritionNodesByDate,
            [state.selectedDate]: completed,
          },
        });
      })
      .catch(() =>
        patch({
          nutritionPlanStatus: "error",
          toast: "补给方案暂时没加载出来，去完整方案页再试一次。",
        }),
      );
  }, [state.selectedDate, selectedTrainingPlan?.id, selectedNutritionPlan?.planId]);
  if (!state.profileComplete)
    return (
      <div className="page">
        <PageHeader title="营养" back={false} />
        <EmptyState
          title="先让我了解你的补给起点"
          detail="完成基础档案后，我才能结合训练和出汗情况给出更舒服的补给建议。"
          action={<Button onClick={() => go("ONB-01")}>开始建立档案</Button>}
        />
      </div>
    );
  if (state.planGenerationStatus !== "ready")
    return <PlanGenerationGate title="营养" />;
  const nutritionPlan = selectedNutritionPlan ?? state.nutritionPlan;
  const targets = nutritionPlan?.dailyTargets ?? {
    caloriesKcal: 2350,
    hydrationMl: 2200,
    carbohydrateG: Math.round(4.4 * state.profile.weightKg),
    proteinG: Math.round(1.65 * state.profile.weightKg),
    fatG: Math.round(0.9 * state.profile.weightKg),
    generatedBy: "agent" as const,
    contextVersion: "pending-agent-target",
  };
  const selectedMeals = state.mealEntries.filter((meal) => meal.date === state.selectedDate);
  const dayHydration = state.hydrationByDate[state.selectedDate] ?? 0;
  const isFutureDate = state.selectedDate > todayIso;
  const completedForDate =
    state.completedNutritionNodesByDate[state.selectedDate] ??
    (state.selectedDate === todayIso ? state.completedNutritionNodes : []);
  const mealIntake = sumMealNutrition(selectedMeals);
  const trainingIntake = (nutritionPlan?.nodes ?? [])
    .filter((node) => completedForDate.includes(node.id))
    .map((node) => nutritionForProduct(node.product))
    .reduce(addNutritionIntake, emptyNutritionIntake());
  const intake = {
    ...addNutritionIntake(mealIntake, trainingIntake),
    hydrationMl: dayHydration,
  };
  const nutritionRows = [
    { key: "water", label: "饮水", value: intake.hydrationMl, target: targets.hydrationMl, unit: "ml", icon: <Droplet /> },
    { key: "carb", label: "碳水", value: intake.carbohydrateG, target: targets.carbohydrateG, unit: "g", icon: <Wheat /> },
    { key: "protein", label: "蛋白质", value: intake.proteinG, target: targets.proteinG, unit: "g", icon: <Egg /> },
    { key: "fat", label: "脂肪", value: intake.fatG, target: targets.fatG, unit: "g", icon: <CircleGauge /> },
  ];
  const addHydrationForSelectedDate = (amount: number, label: string) => {
    if (isFutureDate) {
      patch({ toast: "未来的饮水先不用记，切回今天就可以添加。" });
      return;
    }
    const hydrationMl = dayHydration + amount;
    patch({
      hydrationByDate: {
        ...state.hydrationByDate,
        [state.selectedDate]: hydrationMl,
      },
      ...(state.selectedDate === todayIso ? { hydrationMl } : {}),
      toast: `已为${state.selectedDate === todayIso ? "今天" : "所选日期"}记录${label}，饮水进度已更新`,
    });
  };
  return (
    <div className="page hub-page nutrition-home-v21">
      <PageHeader
        title="营养"
        back={false}
        action={<span className="assistant-live-dot" aria-label="数据已同步" />}
      />
      <p className="hub-subtitle">
        {new Date(state.selectedDate + "T12:00:00").toLocaleDateString("zh-CN", {
          month: "long",
          day: "numeric",
          weekday: "short",
        })}
      </p>
      <DateStrip selected={state.selectedDate} onSelect={selectNutritionDate} planItems={state.planItems} />
      {state.selectedDate !== todayIso && (
        <button
          className="training-return-today"
          onClick={returnNutritionToToday}
        >
          <LocateFixed />
          <span>回到今天</span>
          <small>
            {new Date(`${todayIso}T12:00:00`).toLocaleDateString("zh-CN", {
              month: "numeric",
              day: "numeric",
            })}
          </small>
        </button>
      )}
      <div className="schedule-scope-tabs nutrition-schedule-tabs" aria-label="营养计划查看范围">
        <button className={calendarScope === "week" ? "active" : ""} onClick={() => setCalendarScope("week")}>本周营养</button>
        <button className={calendarScope === "cycle" ? "active" : ""} onClick={() => setCalendarScope("cycle")}>完整周期</button>
      </div>
      {calendarScope === "cycle" && <NutritionCycleOverview items={state.planItems} totalWeeks={state.planProgram.totalWeeks} selectedDate={state.selectedDate} onSelect={(selectedDate) => { selectNutritionDate(selectedDate); setCalendarScope("week"); }} />}
      {calendarScope === "week" && <>
      <Card className="nutrition-day-context">
        <CalendarCheck /><div><b>{selectedTrainingPlan ? selectedTrainingPlan.title : "恢复日营养"}</b><span>{selectedTrainingPlan ? "Agent 已结合当天训练负荷生成目标与补给" : "Agent 已降低训练补给需求并安排日常恢复营养"}</span></div><StatusBadge tone={selectedTrainingPlan ? "green" : "gray"}>{selectedTrainingPlan ? "训练日" : "恢复日"}</StatusBadge>
      </Card>
      <Card className="nutrition-status-card">
        <div className="nutrition-status-head">
          <h2>今天补得怎么样</h2>
          <span>AI 教练目标 · 你的实际进度</span>
        </div>
        {nutritionRows.map((row) => {
          const percent = Math.min(100, Math.round((row.value / Math.max(1, row.target)) * 100));
          return (
            <div className={`nutrition-status-row nutrition-status-row--${row.key}`} key={row.key}>
              <span className={`nutrition-icon nutrition-icon--${row.key}`}>{row.icon}</span>
              <div>
                <span>{row.label}</span>
                <b>{Math.round(row.value).toLocaleString()} <small>/ {row.target.toLocaleString()} {row.unit}</small></b>
                <div className="progress-bar"><span style={{ width: `${percent}%` }} /></div>
              </div>
              <strong>{percent}%</strong>
            </div>
          );
        })}
      </Card>
      <div className="hydration-quick-actions" aria-label="快捷添加饮水">
        <button aria-disabled={isFutureDate} onClick={() => addHydrationForSelectedDate(250, "水瓶 250 ml")}>
          <span className="hydration-vessel hydration-vessel--bottle"><Droplet /></span>
          <b>水瓶</b><small>+250 ml</small>
        </button>
        <button aria-disabled={isFutureDate} onClick={() => addHydrationForSelectedDate(500, "水壶 500 ml")}>
          <span className="hydration-vessel hydration-vessel--jug"><Droplets /></span>
          <b>水壶</b><small>+500 ml</small>
        </button>
        <button className="hydration-more" aria-disabled={isFutureDate} onClick={() => isFutureDate ? patch({ toast: "未来日期不能提前记录饮水，可切回今天后添加" }) : go("NUT-04")}>
          <span className="hydration-vessel"><Plus /></span><b>自定义</b><small>选择容量</small>
        </button>
      </div>
      {isFutureDate && <p className="hydration-future-note">未来日期不可提前记录饮水</p>}
      {selectedTrainingPlan && <>
      <SectionTitle title="今日训练补给" />
      {/* 训练前后节点没有 reminder；显示文案必须保持空值安全。 */}
      <Card className="nutrition-plan-entry">
        <div className="nutrition-plan-head">
          <img src="/running-shoe-unbranded.png" alt="无品牌跑鞋" />
          <h2>{selectedTrainingPlan.title}</h2>
        </div>
        {state.nutritionPlanStatus === "loading" && (
          <div className="fuel-plan-loading" role="status">
          <LoaderCircle className="spin" /> AI 教练正在把补给节点准备好
          </div>
        )}
        {state.nutritionPlanStatus === "error" && (
          <div className="fuel-plan-loading fuel-plan-loading--error">
          <AlertTriangle /> 补给方案暂时没准备好
          </div>
        )}
        {nutritionPlan && (
          <div className="fuel-plan-list">
            {nutritionPlan.nodes.map((node) => {
              const done = Boolean(node.completedAt) || completedForDate.includes(node.id);
              const during = node.phase === "训练中" && node.reminder;
              return (
                <div
                  className={`fuel-plan-row ${done ? "is-complete" : ""}`}
                  key={node.id}
                >
                  <img src={node.product.image} alt={node.product.name} />
                  <div>
                    <b>
                      {node.phase} {node.time}
                    </b>
                    <p>
                      {node.product.name} · {node.product.dose}
                    </p>
                  </div>
                  {during ? (
                    <label className="nutrition-remind-switch">
                      <button
                        role="switch"
                        disabled={reminderBusy === node.id || done}
                        aria-label={`${node.product.name}训练中补给提醒`}
                        aria-checked={Boolean(node.reminder?.enabled)}
                        onClick={async () => {
                          if (!nutritionPlan || !node.reminder) return;
                          const enabled = !node.reminder.enabled;
                          setReminderBusy(node.id);
                          try {
                            await services.nutrition.updateReminder(
                              nutritionPlan.planId,
                              node.id,
                              enabled,
                            );
                            const nodes = nutritionPlan.nodes.map(
                              (item) =>
                                item.id === node.id && item.reminder
                                  ? {
                                      ...item,
                                      reminder: { ...item.reminder, enabled },
                                    }
                                  : item,
                            );
                            const nextState = {
                              ...state,
                              nutritionPlan: { ...nutritionPlan, nodes },
                            };
                            const next = enabled
                              ? (nodes.find((item) => item.id === node.id) ??
                                null)
                              : (createReminderNode(nextState, node.id) ??
                                createReminderNode(nextState));
                            patch({
                              nutritionPlan: { ...nutritionPlan, nodes },
                              nutritionPlansByDate: { ...state.nutritionPlansByDate, [state.selectedDate]: { ...nutritionPlan, nodes } },
                              reminder:
                                next && next.reminder?.enabled
                                  ? reminderFromNode(next)
                                  : {
                                      ...state.reminder,
                                      nodeId: node.id,
                                      enabled: false,
                                      status: "scheduled",
                                      vibrationCount: 0,
                                    },
                              toast: enabled
                                ? `${node.time}补给提醒已开启，将在训练中弹出`
                                : `${node.time}补给提醒已关闭，训练中不会弹出`,
                            });
                          } catch {
                            patch({
                              toast: "提醒设置同步失败，原设置保持不变",
                            });
                          } finally {
                            setReminderBusy("");
                          }
                        }}
                      >
                        <span />
                      </button>
                      <small>
                        {done
                          ? "已完成"
                          : node.reminder?.enabled
                            ? "提醒我"
                            : "已关闭"}
                      </small>
                    </label>
                  ) : (
                    <StatusBadge tone={done ? "green" : "gray"}>
                      {done ? "已完成" : "待完成"}
                    </StatusBadge>
                  )}
                </div>
              );
            })}
          </div>
        )}
        <div className="nutrition-entry-actions">
          <Button variant="secondary" onClick={() => go("NUT-02")}>查看完整方案</Button>
        <Button onClick={() => openNutritionFeedback("NUT-01")}>告诉 AI 教练完成得怎样</Button>
        </div>
      </Card>
      </>}
      <SectionTitle title="今日餐次" />
      <Card className="meal-summary">
        {selectedMeals.map((meal) => {
          const calories = sumMealNutrition([meal]).caloriesKcal;
          return <button key={meal.id} onClick={() => { patch({ selectedMealId: meal.id }); go("NUT-05"); }}>
            <img
              className={`meal-image meal-image--${mealImageKeyByName[meal.name] ?? "snack"}`}
              src={mealImageForName(meal.name)}
              alt={`${meal.name}餐食`}
            />
            <span>{meal.name}</span><b>{calories} kcal</b><StatusBadge tone="green">已记录</StatusBadge><ChevronRight />
          </button>;
        })}
        {selectedMeals.length > 0 && <button className="empty-meal" onClick={() => go("NUT-05")}>
          <span className="first-meal-icon"><Plus /></span>
          <span>继续记录</span>
          <b>添加餐次</b>
          <ChevronRight />
        </button>}
        {selectedMeals.length === 0 && <button className="empty-meal empty-meal--first" onClick={() => go("NUT-05")}>
          <span className="first-meal-icon"><Plus /></span>
          <span>还没有餐次记录</span>
          <b>添加第一餐</b>
          <ChevronRight />
        </button>}
      </Card>
      <Card className="nutrition-ai-entry">
        <CoachAvatar />
        <div>
          <p>Agent 将结合今日训练、热量目标和已记录餐次，补全其余饮食方案。</p>
        </div>
        <button
          aria-label="AI 生成训练食谱"
          onClick={() => {
            patch({ toast: "正在结合今日训练与餐次生成食谱…" });
            go("NUT-07");
          }}
        >
          去看看 <ChevronRight />
        </button>
      </Card>
      </>}
    </div>
  );
}

function NutritionCycleOverview({
  items,
  totalWeeks,
  selectedDate,
  onSelect,
}: {
  items: PlanItem[];
  totalWeeks: number;
  selectedDate: string;
  onSelect: (date: string) => void;
}) {
  const start = new Date(`${items[0]?.date ?? selectedDate}T12:00:00`);
  const startOffset = (start.getDay() + 6) % 7;
  start.setDate(start.getDate() - startOffset);
  const plans = new Map(items.map((item) => [item.date, item]));
  const localIso = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
  return (
    <div className="nutrition-cycle-overview">
      <div className="cycle-overview-head"><div><span>Agent 营养周期</span><h2>{totalWeeks} 周逐日计划</h2></div><strong>{totalWeeks}<small> 周</small></strong></div>
      {Array.from({ length: totalWeeks }, (_, weekIndex) => (
        <Card className="nutrition-cycle-week" key={weekIndex}>
          <div className="cycle-week-title"><span><b>第 {weekIndex + 1} 周</b><small>训练日与恢复日分别规划</small></span></div>
          <div className="nutrition-cycle-days">
            {Array.from({ length: 7 }, (_, dayIndex) => {
              const date = new Date(start); date.setDate(start.getDate() + weekIndex * 7 + dayIndex);
              const iso = localIso(date); const plan = plans.get(iso);
              return <button key={iso} aria-label={`${iso}，${plan ? "训练日营养" : "恢复日营养"}`} className={`${iso === selectedDate ? "is-selected" : ""} ${plan ? "is-training" : ""}`} onClick={() => onSelect(iso)}>
                <span>{"一二三四五六日"[dayIndex]}</span><b>{date.getDate()}</b><small>{plan ? "训练" : "恢复"}</small>
              </button>;
            })}
          </div>
        </Card>
      ))}
    </div>
  );
}

function NutritionPlan() {
  const {
    state,
    patch,
    go,
    sendAnalysisToHome,
    publishTrainingAnalysisUpdate,
  } = useApp();
  const [loadError, setLoadError] = useState("");
  const [analysisUpdatePrompt, setAnalysisUpdatePrompt] = useState<{
    previousCompletion: number;
    nextCompletion: number;
  } | null>(null);
  const todayIso = new Date().toISOString().slice(0, 10);
  const activePlan =
    state.planItems.find((item) => item.date === state.selectedDate) ??
    state.planItems[0];
  const weather = state.device.weather
    ? `${state.device.weather.temperature}°C · 湿度 ${state.device.weather.humidity}%`
    : "设备天气未同步";
  const loadPlan = async () => {
    if (!activePlan) return;
    setLoadError("");
    patch({ nutritionPlanStatus: "loading" });
    try {
      const result = await services.nutrition.buildPlan(
        activePlan.id,
        buildAgentContext(state),
        state.selectedDate,
      );
      const completed = result.data.nodes
        .filter((node) => node.completedAt)
        .map((node) => node.id);
      patch({
        nutritionPlan: result.data,
        nutritionPlansByDate: {
          ...state.nutritionPlansByDate,
          [result.data.planDate]: result.data,
        },
        nutritionPlanStatus: "ready",
        completedNutritionNodes:
          result.data.planDate === todayIso
            ? completed
            : state.completedNutritionNodes,
        completedNutritionNodesByDate: {
          ...state.completedNutritionNodesByDate,
          [result.data.planDate]: completed,
        },
      });
    } catch {
      setLoadError("Agent 暂时无法生成补给方案，未使用固定方案替代。");
      patch({ nutritionPlanStatus: "error" });
    }
  };
  useEffect(() => {
    if (
      !state.nutritionPlan ||
      state.nutritionPlan.planId !== activePlan?.id ||
      state.nutritionPlan.planDate !== state.selectedDate
    )
      void loadPlan();
  }, [activePlan?.id, state.selectedDate]);
  if (
    state.nutritionPlanStatus === "loading" ||
    (!state.nutritionPlan && state.nutritionPlanStatus === "idle")
  )
    return (
      <div className="page nutrition-plan-page">
        <PageHeader title="营养与补给方案" />
        <div className="skeleton-stack nutrition-plan-skeleton" role="status">
          <div />
          <div />
          <div />
          <div />
        </div>
        <p className="loading-copy">
          Agent 正在根据训练、天气与个人画像生成节点…
        </p>
      </div>
    );
  if (!state.nutritionPlan || state.nutritionPlanStatus === "error")
    return (
      <div className="page nutrition-plan-page">
        <PageHeader title="营养与补给方案" />
        <EmptyState
          title="补给方案生成失败"
          detail={loadError || "没有可展示的补给节点。"}
          action={<Button onClick={() => void loadPlan()}>重新生成</Button>}
        />
      </div>
    );
  const nutritionPlan = state.nutritionPlan;
  const completedForDate =
    state.completedNutritionNodesByDate[nutritionPlan.planDate] ??
    (nutritionPlan.planDate === todayIso ? state.completedNutritionNodes : []);
  return (
    <div className="page nutrition-plan-page">
      <PageHeader title={nutritionPlan.title} />
      <div className="agent-proof">
        <Bot /> Agent 已生成{" "}
        <span>{nutritionPlan.basis.slice(0, 3).join(" · ")}</span>
      </div>
      <div className="nutrition-replace-hint">
        <Info size={14} />
        <span>本页只用于查看方案与替换补给品；训练后的实际完成情况请在今日训练补给页或首页 AI 对话中反馈。</span>
      </div>
      <Card className="basis-card">
        <div>
          <span>补能区间</span>
          <b>
            {nutritionPlan.carbohydratePerHour[0]}–
            {nutritionPlan.carbohydratePerHour[1]} g/h
          </b>
        </div>
        <div>
          <span>补液目标</span>
          <b>
            {nutritionPlan.hydrationMl[0]}–{nutritionPlan.hydrationMl[1]} ml
          </b>
        </div>
        <div>
          <span>天气</span>
          <b>{weather}</b>
        </div>
      </Card>
      <div className="nutrition-timeline nutrition-product-timeline">
        {nutritionPlan.nodes.map((node) => {
          const done = Boolean(node.completedAt) || completedForDate.includes(node.id);
          return (
            <section
              className={`nutrition-node-v2 nutrition-node-v2--${node.tone}`}
              key={node.id}
            >
              <div className="nutrition-node-rail">
                <span>{node.phase}</span>
                <small>{node.time}</small>
                <b>{node.target}</b>
              </div>
              <div className="nutrition-product-wrap">
                <button
                  type="button"
                  className="nutrition-product-card"
                  disabled={done}
                  aria-label={`${node.product.name}，点击选择替代`}
                  onClick={() => {
                    patch({ selectedNutritionNodeId: node.id });
                    go("NUT-08");
                  }}
                >
                  <img src={node.product.image} alt={node.product.name} />
                  <div className="nutrition-product-meta">
                    <dl>
                      <div>
                        <dt>类型</dt>
                        <dd>{node.product.type}</dd>
                      </div>
                      <div>
                        <dt>剂量</dt>
                        <dd>{node.product.dose}</dd>
                      </div>
                      <div>
                        <dt>时机</dt>
                        <dd>{node.product.timing}</dd>
                      </div>
                      <div>
                        <dt>原因</dt>
                        <dd>{node.product.reason}</dd>
                      </div>
                    </dl>
                  </div>
                  <ChevronRight aria-hidden="true" />
                </button>
                {false && <button
                  type="button"
                  className={`nutrition-complete-button ${done ? "is-done" : ""}`}
                  disabled={done}
                  onClick={async () => {
                    try {
                      const result = await services.nutrition.recordIntake(
                        nutritionPlan.planId,
                        nutritionPlan.planDate,
                        node.id,
                        node.product.servingAmount,
                      );
                      const completed = Array.from(
                        new Set([...completedForDate, node.id]),
                      );
                      const nodes = nutritionPlan.nodes.map((item) =>
                        item.id === node.id
                          ? { ...item, completedAt: result.recordedAt }
                          : item,
                      );
                      const updatedPlan = { ...nutritionPlan, nodes };
                      const isHydration =
                        node.product.category === "water" ||
                        node.product.category === "electrolyte";
                      const currentHydration =
                        state.hydrationByDate[nutritionPlan.planDate] ?? 0;
                      const nextHydration = isHydration
                        ? currentHydration + node.product.servingAmount
                        : currentHydration;
                      const nextCompletion = Math.round(
                        (completed.length / nutritionPlan.nodes.length) * 100,
                      );
                      const completedActivity =
                        state.activities.find(
                          (item) =>
                            item.planItemId === nutritionPlan.planId &&
                            item.date === nutritionPlan.planDate,
                        ) ??
                        state.activities.find(
                          (item) =>
                            item.id === state.lastCompletedActivityId &&
                            item.date === nutritionPlan.planDate,
                        );
                      const completedTrainingPlan = state.planItems.find(
                        (item) => item.id === completedActivity?.planItemId,
                      );
                      const previousCompletion =
                        completedActivity?.nutritionCompletion ?? nextCompletion;
                      const updatedActivity = completedActivity
                        ? {
                            ...completedActivity,
                            nutritionCompletion: nextCompletion,
                          }
                        : null;
                      const activities = updatedActivity
                        ? state.activities.map((item) =>
                            item.id === updatedActivity.id
                              ? updatedActivity
                              : item,
                          )
                        : state.activities;
                      patch({
                        nutritionPlan: updatedPlan,
                        nutritionPlansByDate: {
                          ...state.nutritionPlansByDate,
                          [nutritionPlan.planDate]: updatedPlan,
                        },
                        completedNutritionNodes:
                          nutritionPlan.planDate === todayIso
                            ? completed
                            : state.completedNutritionNodes,
                        completedNutritionNodesByDate: {
                          ...state.completedNutritionNodesByDate,
                          [nutritionPlan.planDate]: completed,
                        },
                        nutritionCompletion:
                          nutritionPlan.planDate === todayIso
                            ? nextCompletion
                            : state.nutritionCompletion,
                        activities,
                        hydrationByDate: isHydration
                          ? {
                              ...state.hydrationByDate,
                              [nutritionPlan.planDate]: nextHydration,
                            }
                          : state.hydrationByDate,
                        hydrationMl:
                          isHydration && nutritionPlan.planDate === todayIso
                            ? nextHydration
                            : state.hydrationMl,
                        toast: `${node.phase}补给已完成，并同步到首页与营养状态`,
                      });
                      if (
                        updatedActivity &&
                        completedTrainingPlan &&
                        previousCompletion !== nextCompletion
                      ) {
                        try {
                          const analysisResult =
                            await services.agent.analyzeTraining(
                              completedTrainingPlan,
                              updatedActivity,
                              nodes,
                              completed,
                              state.feedback,
                            );
                          publishTrainingAnalysisUpdate(
                            analysisResult.data,
                            previousCompletion,
                            nextCompletion,
                          );
                          setAnalysisUpdatePrompt({
                            previousCompletion,
                            nextCompletion,
                          });
                        } catch {
                          patch({
                            toast:
                              "补给已记录，但训练分析暂未更新；可稍后重新标记或进入分析页重试",
                          });
                        }
                      }
                    } catch {
                      patch({ toast: "补给记录失败，请重新操作" });
                    }
                  }}
                >
                  {done ? (
                    <>
                      <Check size={14} /> 已完成
                    </>
                  ) : (
                    "标记完成"
                  )}
                </button>}
                <div className="nutrition-feedback-managed">
                  <MessageCircleMore aria-hidden="true" />
                  <span>{done ? "已由训练记录或 AI 反馈更新" : "完成情况由 AI 对话反馈"}</span>
                </div>
              </div>
            </section>
          );
        })}
      </div>
      <AiSummary
        summary="剂量由 Agent 根据训练时长、天气、出汗自评和耐受生成。"
        onClick={() =>
          sendAnalysisToHome(
            "训练营养方案依据",
            "NUT-02",
            `本次方案结合训练时长、${weather}、出汗自评与既往胃肠耐受。`,
          )
        }
      />
      {analysisUpdatePrompt && false && (
        <div
          className="dialog-backdrop nutrition-analysis-update-backdrop"
          onClick={() => setAnalysisUpdatePrompt(null)}
        >
          <section
            className="confirm-dialog nutrition-analysis-update-dialog"
            role="dialog"
            aria-modal="true"
            aria-labelledby="nutrition-analysis-update-title"
            onClick={(event) => event.stopPropagation()}
          >
            <div className="nutrition-analysis-update-dialog__icon">
              <Sparkles aria-hidden="true" />
            </div>
            <small>REAL-TIME SYNC</small>
            <h2 id="nutrition-analysis-update-title">补给情况已更新</h2>
            <p>
              本次训练补给完成度已由 {analysisUpdatePrompt?.previousCompletion}%
              更新为 {analysisUpdatePrompt?.nextCompletion}%。CPT AI 教练已基于最新记录重新生成本次训练分析。
            </p>
            <div className="nutrition-analysis-update-progress" aria-label="补给完成度变化">
              <span>{analysisUpdatePrompt?.previousCompletion}%</span>
              <ArrowRight aria-hidden="true" />
              <strong>{analysisUpdatePrompt?.nextCompletion}%</strong>
            </div>
            <div className="bottom-actions">
              <Button
                variant="secondary"
                onClick={() => setAnalysisUpdatePrompt(null)}
              >
                稍后查看
              </Button>
              <Button
                onClick={() => {
                  setAnalysisUpdatePrompt(null);
                  go("HOME-01", "home");
                }}
              >
                去 HOME-01 查看
              </Button>
            </div>
          </section>
        </div>
      )}
    </div>
  );
}

function NutritionReminderSettings() {
  const { state, patch, back } = useApp();
  const plan = state.nutritionPlan;
  const reminderNodes = plan?.nodes.filter(
    (node) => node.phase === "训练中" && node.reminder,
  ) ?? [];
  const [timings, setTimings] = useState<Record<string, number>>(() =>
    Object.fromEntries(
      reminderNodes.map((node) => [
        node.id,
        Math.max(5, Math.round(node.reminder!.offsetSecond / 60)),
      ]),
    ),
  );
  const [saving, setSaving] = useState(false);
  useEffect(() => {
    if (!state.selectedNutritionNodeId) return;
    window.requestAnimationFrame(() =>
      document
        .getElementById(`nutrition-reminder-${state.selectedNutritionNodeId}`)
        ?.scrollIntoView({ block: "center" }),
    );
  }, [state.selectedNutritionNodeId]);
  if (!plan || !reminderNodes.length)
    return (
      <div className="page nutrition-reminder-settings">
        <PageHeader title="补给提醒时间" />
        <EmptyState
        title="今天跑中不用额外补给"
        detail="恢复日或短时训练通常不需要训练中提醒，按身体感受补水就好。"
          action={<Button onClick={back}>返回补给方案</Button>}
        />
      </div>
    );
  return (
    <div className="page nutrition-reminder-settings">
      <PageHeader title="补给提醒时间" />
      <div className="page-intro">
        <h1>调整训练中的补给时机</h1>
        <p>时间从手表端开始运动后计算，只改变补液、补能提醒节点，不会设置训练开始时间。</p>
      </div>
      <div className="nutrition-reminder-list">
        {reminderNodes.map((node) => {
          const minute = timings[node.id] ?? 30;
          return (
            <Card
              id={`nutrition-reminder-${node.id}`}
              className={`nutrition-reminder-editor${state.selectedNutritionNodeId === node.id ? " is-targeted" : ""}`}
              key={node.id}
            >
              <img src={node.product.image} alt={node.product.name} />
              <div className="nutrition-reminder-editor__copy">
                <span>{node.product.type}</span>
                <b>{node.product.name} · {node.product.dose}</b>
                <small>当前设置：训练第 {minute} 分钟提醒</small>
              </div>
              <label>
                <span>训练第</span>
                <input
                  aria-label={`${node.product.name}提醒分钟`}
                  type="number"
                  min="5"
                  max="240"
                  step="5"
                  value={minute}
                  onChange={(event) =>
                    setTimings((current) => ({
                      ...current,
                      [node.id]: Math.max(
                        5,
                        Math.min(240, Number(event.target.value) || 5),
                      ),
                    }))
                  }
                />
                <span>分钟</span>
              </label>
              <div className="reminder-minute-presets" aria-label={`${node.product.name}快捷时间`}>
                {[20, 30, 45, 60].map((value) => (
                  <button
                    type="button"
                    aria-pressed={minute === value}
                    className={minute === value ? "selected" : ""}
                    onClick={() => setTimings((current) => ({ ...current, [node.id]: value }))}
                    key={value}
                  >
                    {value} 分钟
                  </button>
                ))}
              </div>
            </Card>
          );
        })}
      </div>
      <Card className="channel-card">
        <Bell />
        <div>
          <b>提醒方式保持不变</b>
          <p>到达补给节点时手机与已连接手表各震动一次，用户处理后弹窗关闭。</p>
        </div>
      </Card>
      <Button
        className="page-primary"
        disabled={saving}
        onClick={async () => {
          setSaving(true);
          try {
            await Promise.all(
              reminderNodes.map((node) => {
                const minute = timings[node.id];
                return services.nutrition.updateReminderTiming(
                  plan.planId,
                  node.id,
                  minute * 60,
                  `第 ${minute} 分钟`,
                );
              }),
            );
            const nodes = plan.nodes.map((node) => {
              if (!node.reminder || timings[node.id] == null) return node;
              const minute = timings[node.id];
              return {
                ...node,
                time: `第 ${minute} 分钟`,
                product: {
                  ...node.product,
                  timing: `训练第 ${minute} 分钟`,
                },
                reminder: {
                  ...node.reminder,
                  offsetSecond: minute * 60,
                  demoOffsetSecond:
                    node.reminder.demoOffsetSecond ??
                    Math.max(8, reminderNodes.indexOf(node) * 8 + 8),
                  displayTime: `第 ${minute} 分钟`,
                },
              };
            });
            const updatedPlan = { ...plan, nodes };
            const activeNode = nodes.find(
              (node) => node.id === state.reminder.nodeId,
            );
            patch({
              nutritionPlan: updatedPlan,
              nutritionPlansByDate: {
                ...state.nutritionPlansByDate,
                [plan.planDate]: updatedPlan,
              },
              reminder:
                activeNode?.reminder
                  ? { ...state.reminder, ...reminderFromNode(activeNode) }
                  : state.reminder,
              toast: "补给提醒时间已同步到今日训练补给与设备提醒",
            });
            back();
          } catch {
            patch({ toast: "补给提醒时间保存失败，原设置保持不变" });
          } finally {
            setSaving(false);
          }
        }}
      >
        {saving ? "保存中…" : "保存补给提醒时间"}
      </Button>
    </div>
  );
}

function NutritionDuring() {
  const { state, patch, go } = useApp();
  const plan = state.nutritionPlan;
  const reminderNodes = useMemo(
    () =>
      (plan?.nodes ?? [])
        .filter((node) => node.phase === "训练中" && node.reminder)
        .sort(
          (first, second) =>
            first.reminder!.offsetSecond - second.reminder!.offsetSecond,
        ),
    [plan],
  );
  const [saving, setSaving] = useState(false);
  const reviewActions = plan
    ? (state.nutritionReminderReviewsByDate[plan.planDate] ?? {})
    : {};
  const pendingNodes = reminderNodes.filter(
    (node) => !reviewActions[node.id],
  );
  const current = pendingNodes[0] ?? null;
  const reviewedCount = reminderNodes.length - pendingNodes.length;
  const confirmCurrent = async (enabled: boolean) => {
    if (!plan || !current || saving) return;
    setSaving(true);
    try {
      await services.nutrition.updateReminder(
        plan.planId,
        current.id,
        enabled,
      );
      const nodes = plan.nodes.map((node) =>
        node.id === current.id && node.reminder
          ? {
              ...node,
              reminder: {
                ...node.reminder,
                enabled,
              },
            }
          : node,
      );
      const updatedPlan = { ...plan, nodes };
      const firstPending = nodes
        .filter((node) => node.phase === "训练中" && node.reminder?.enabled)
        .sort(
          (first, second) =>
            first.reminder!.offsetSecond - second.reminder!.offsetSecond,
        )[0];
      patch({
        nutritionPlan: updatedPlan,
        nutritionPlansByDate: {
          ...state.nutritionPlansByDate,
          [plan.planDate]: updatedPlan,
        },
        reminder:
          firstPending?.reminder
            ? { ...state.reminder, ...reminderFromNode(firstPending) }
            : {
                ...state.reminder,
                enabled: false,
                status: "scheduled",
              },
        nutritionReminderReviewsByDate: {
          ...state.nutritionReminderReviewsByDate,
          [plan.planDate]: {
            ...reviewActions,
            [current.id]: enabled ? "confirmed" : "skipped",
          },
        },
        toast: enabled
          ? `${current.product.name}将按${current.reminder!.displayTime}提醒`
          : `${current.product.name}本次训练不提醒`,
      });
    } catch {
      patch({ toast: "提醒状态保存失败，当前项目仍待核对" });
    } finally {
      setSaving(false);
    }
  };
  if (!plan || !reminderNodes.length)
    return (
      <div className="page nutrition-during-page">
        <PageHeader title="训练中补给" />
        <EmptyState
        title="今天跑中不用额外补给"
        detail="短时训练或恢复日，把训练前后补好就够了。"
          action={<Button onClick={() => go("NUT-02")}>返回今日补给</Button>}
        />
      </div>
    );
  if (!current)
    return (
      <div className="page nutrition-during-page">
        <PageHeader title="训练中补给" />
        <Card className="nutrition-queue-complete">
          <CheckCircle2 />
          <span>准备好了</span>
          <h1>训练中补给已经全部确认</h1>
            <p>共 {reminderNodes.length} 项，手表端开始运动后只提醒已确认项目。</p>
          <div className="nutrition-queue-summary">
            {reminderNodes.map((node) => (
              <div key={node.id}>
                <img src={node.product.image} alt="" aria-hidden="true" />
                <span>
                  <b>{node.product.name}</b>
                  <small>{node.product.dose}</small>
                </span>
                <strong className={reviewActions[node.id] === "skipped" ? "is-skipped" : ""}>
                  {reviewActions[node.id] === "skipped"
                    ? "本次不提醒"
                    : node.reminder!.displayTime}
                </strong>
              </div>
            ))}
          </div>
          <Button onClick={() => go("TRAIN-READY")}>前往训练准备</Button>
          <Button
            variant="secondary"
            onClick={() =>
              patch({
                nutritionReminderReviewsByDate: {
                  ...state.nutritionReminderReviewsByDate,
                  [plan.planDate]: {},
                },
              })
            }
          >
            重新检查
          </Button>
        </Card>
      </div>
    );
  return (
    <div className="page nutrition-during-page">
      <PageHeader title="训练中补给" />
      <div className="during-hero">
        <div className="nutrition-queue-progress" aria-label={`第 ${reviewedCount + 1} 项，共 ${reminderNodes.length} 项`}>
          {reminderNodes.map((node, index) => (
            <i
              className={reviewActions[node.id] ? "done" : node.id === current.id ? "current" : ""}
              key={node.id}
            />
          ))}
        </div>
        <span>今日训练中补给 · {reviewedCount + 1}/{reminderNodes.length}</span>
          <h1>再确认一项，就更安心出发</h1>
        <p>{state.planItems.find((item) => item.date === plan.planDate)?.title ?? state.planItems[0].title}</p>
      </div>
      <Card className="next-fuel next-fuel--queue">
        <img src={current.product.image} alt={current.product.name} />
        <div className="next-fuel__copy">
          <span>{current.product.type}</span>
          <b>{current.product.name}</b>
          <p>{current.product.dose}</p>
        </div>
        <StatusBadge tone="orange">待核对</StatusBadge>
        <div className="nutrition-queue-timing">
          <span>当前提醒设置</span>
          <strong>{current.reminder!.displayTime}</strong>
          <small>手机与已连接手表各提醒一次</small>
        </div>
        <button
          type="button"
          className="nutrition-queue-edit-time"
          onClick={() => {
            patch({ selectedNutritionNodeId: current.id });
            go("NUT-09");
          }}
        >
          <Settings2 /> 去调整补给频率 <ChevronRight />
        </button>
      </Card>
      {pendingNodes.length > 1 && (
        <div className="nutrition-queue-next">
          <span>下一项</span>
          <b>{pendingNodes[1].product.name}</b>
          <small>{pendingNodes[1].reminder!.displayTime}</small>
        </div>
      )}
      <div className="bottom-actions">
        <Button
          variant="secondary"
          disabled={saving}
          onClick={() => void confirmCurrent(false)}
        >
          本次不提醒
        </Button>
        <Button disabled={saving} onClick={() => void confirmCurrent(true)}>
          {saving ? "保存中…" : pendingNodes.length === 1 ? "确认并完成" : "按此提醒并看下一项"}
        </Button>
      </div>
      <p className="nutrition-queue-note">这里只核对是否提醒；时间统一在 NUT-09 设置。</p>
    </div>
  );
}

function HydrationLog() {
  const { state, patch } = useApp();
  const [custom, setCustom] = useState(250);
  const selectedHydration = state.hydrationByDate[state.selectedDate] ?? 0;
  const add = (ml: number) => {
    const hydrationMl = selectedHydration + ml;
    patch({
      hydrationByDate: { ...state.hydrationByDate, [state.selectedDate]: hydrationMl },
      ...(state.selectedDate === new Date().toISOString().slice(0, 10) ? { hydrationMl } : {}),
      nutritionCompletion: Math.max(
        state.nutritionCompletion,
        Math.min(100, Math.round(hydrationMl / 22)),
      ),
      toast: `已记录饮水 ${ml} ml，今日营养状态已同步更新`,
    });
  };
  return (
    <div className="page">
      <PageHeader title="饮水记录" />
      <div className="water-hero">
        <Droplets />
        <strong>
          {selectedHydration}
          <small> / 2200 ml</small>
        </strong>
        <span>今日饮水</span>
        <div className="progress-bar">
          <span
            style={{ width: `${Math.min(100, selectedHydration / 22)}%` }}
          />
        </div>
      </div>
      <div className="volume-grid">
        {[150, 250, 350, 500].map((ml) => (
          <button key={ml} onClick={() => add(ml)}>
            <Droplets />
            <b>{ml} ml</b>
          </button>
        ))}
      </div>
      <label className="field">
        <span>自定义容量</span>
        <div className="unit-input">
          <input
            type="number"
            value={custom}
            onChange={(e) => setCustom(Number(e.target.value))}
          />
          <b>ml</b>
        </div>
      </label>
      <Button className="page-primary" onClick={() => add(custom)}>
        添加饮水
      </Button>
    </div>
  );
}

function MealDiary() {
  const { state, patch, go } = useApp();
  const [busy, setBusy] = useState("");
  const [addingMeal, setAddingMeal] = useState(false);
  const dayMeals = state.mealEntries.filter((meal) => meal.date === state.selectedDate);
  const intake = sumMealNutrition(dayMeals);
  const deleteFood = async (mealId: string, foodId: string) => {
    setBusy(foodId);
    try {
      await services.nutrition.deleteMealFood(mealId, foodId);
      patch({
        mealEntries: state.mealEntries.map((meal) => meal.id === mealId ? { ...meal, foods: meal.foods.filter((food) => food.id !== foodId) } : meal),
        toast: "食物已删除，今日营养进度已同步更新",
      });
    } catch { patch({ toast: "删除失败，原记录保持不变" }); }
    finally { setBusy(""); }
  };
  const deleteMeal = async (mealId: string) => {
    setBusy(mealId);
    try {
      await services.nutrition.deleteMeal(mealId);
      patch({ mealEntries: state.mealEntries.filter((meal) => meal.id !== mealId), selectedMealId: "", toast: "餐次已删除，今日营养进度已重新计算" });
    } catch { patch({ toast: "删除餐次失败，原记录保持不变" }); }
    finally { setBusy(""); }
  };
  return (
    <div className="page">
      <PageHeader title="餐次日记" />
      <div className="macro-strip">
        <Metric
          value={intake.caloriesKcal}
          unit="kcal"
          label="已摄入"
        />
        <Metric value={intake.proteinG} unit="g" label="蛋白质" />
        <Metric value={intake.fatG} unit="g" label="脂肪" />
      </div>
      <div className="meal-list">
        {dayMeals.map((meal) => {
          const mealIntake = sumMealNutrition([meal]);
          return <Card key={meal.id} className="meal-row meal-row--editable">
            <div className="meal-row-head"><div><b>{meal.name}</b><span>{mealIntake.caloriesKcal} kcal · 碳水 {mealIntake.carbohydrateG} g · 蛋白 {mealIntake.proteinG} g · 脂肪 {mealIntake.fatG} g</span></div>
              <button className="meal-delete" disabled={busy === meal.id} aria-label={`删除${meal.name}`} onClick={() => deleteMeal(meal.id)}><Trash2 /></button></div>
            <div className="meal-foods">{meal.foods.map((food) => <div key={food.id}><span><b>{food.name}</b><small>{food.amount} · {food.caloriesKcal} kcal</small></span><button disabled={busy === food.id} aria-label={`删除${food.name}`} onClick={() => deleteFood(meal.id, food.id)}><Minus /></button></div>)}</div>
            <button className="meal-add-food" onClick={() => { patch({ selectedMealId: meal.id }); go("NUT-06"); }}><Plus /> 增加食物</button>
          </Card>;
        })}
      {!dayMeals.length && <EmptyState title="这一天还等着第一餐" detail="先选择早餐、午餐、晚餐或加餐，再把吃过的食物记下来。" action={<Button onClick={() => setAddingMeal(true)}>记录第一餐</Button>} />}
      </div>
      <div className="meal-create-panel">
        {addingMeal ? (
          <Card className="meal-type-picker">
            <div><b>选择餐次</b><button aria-label="关闭新增餐次" onClick={() => setAddingMeal(false)}><Minus /></button></div>
            <div className="meal-type-grid">
              {[{ id: "breakfast", name: "早餐" }, { id: "lunch", name: "午餐" }, { id: "dinner", name: "晚餐" }, { id: "snack", name: "加餐" }].map((meal) => {
                const datedMealId = `${meal.id}-${state.selectedDate}`;
                const exists = dayMeals.some((entry) => entry.name === meal.name);
                const targetMealId = dayMeals.find((entry) => entry.name === meal.name)?.id ?? datedMealId;
                return <button key={meal.id} disabled={exists} onClick={() => { patch({ selectedMealId: targetMealId }); go("NUT-06"); }}>
                  <Utensils /><b>{meal.name}</b><small>{exists ? "已存在" : "添加食物"}</small>
                </button>;
              })}
            </div>
          </Card>
        ) : (
          <Button variant="secondary" className="page-primary meal-create-button" onClick={() => setAddingMeal(true)}><Plus /> 新增餐次</Button>
        )}
      </div>
      <Button
        variant="secondary"
        className="page-primary"
        onClick={() => go("NUT-07")}
      >
        <Sparkles /> AI 生成训练食谱
      </Button>
    </div>
  );
}

function FoodSearch() {
  const { state, patch, go } = useApp();
  const [query, setQuery] = useState("香蕉");
  const foods: Array<FoodEntry> = [
    { id: "banana", name: "香蕉", amount: "1 根", caloriesKcal: 105, hydrationMl: 0, carbohydrateG: 27, proteinG: 1, fatG: 0, source: "user" },
    { id: "yogurt", name: "低脂酸奶", amount: "200 g", caloriesKcal: 126, hydrationMl: 0, carbohydrateG: 16, proteinG: 10, fatG: 3, source: "user" },
    { id: "bread", name: "全麦面包", amount: "2 片", caloriesKcal: 168, hydrationMl: 0, carbohydrateG: 30, proteinG: 7, fatG: 3, source: "user" },
  ];
  return (
    <div className="page">
      <PageHeader title="添加食物" />
      <div className="search-field">
        <Search />
        <input
          value={query}
          aria-label="搜索食物"
          onChange={(e) => setQuery(e.target.value)}
        />
      </div>
      <SectionTitle title="搜索结果" />
      <div className="food-list">
        {foods
          .filter((f) => f.name.includes(query) || !query)
          .map((food) => (
            <Card key={food.name} className="food-row">
              <div>
                <b>{food.name}</b>
                <span>{food.amount} · {food.caloriesKcal} kcal</span>
              </div>
              <button
                aria-label={`添加${food.name}`}
                onClick={async () => {
                  const mealId = state.selectedMealId || `dinner-${state.selectedDate}`;
                  const mealType = mealId.split("-")[0];
                  const mealName = ({ breakfast: "早餐", lunch: "午餐", dinner: "晚餐", snack: "加餐", post: "训练后餐" } as Record<string, string>)[mealType] ?? "餐次";
                  const foodWithId = { ...food, id: `${food.id}-${Date.now()}` };
                  try {
                    await services.nutrition.recordMealFood(mealId, foodWithId);
                    const existing = state.mealEntries.find((meal) => meal.id === mealId);
                    patch({
                      mealEntries: existing
                        ? state.mealEntries.map((meal) => meal.id === mealId ? { ...meal, foods: [...meal.foods, foodWithId] } : meal)
                        : [...state.mealEntries, { id: mealId, name: mealName, date: state.selectedDate, foods: [foodWithId] }],
                      selectedMealId: mealId,
                      toast: `${food.name} 已加入${existing?.name ?? mealName}，今日营养进度已更新`,
                    });
                    go("NUT-05");
                  } catch { patch({ toast: "食物保存失败，请重新操作" }); }
                }}
              >
                <Plus />
              </button>
            </Card>
          ))}
      </div>
    </div>
  );
}

function RecipePage() {
  const { state, patch } = useApp();
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [retryNonce, setRetryNonce] = useState(0);
  const [variantByMeal, setVariantByMeal] = useState<
    Partial<Record<AgentMealType, number>>
  >({});
  const [recipePlan, setRecipePlan] = useState<AgentDailyRecipePlan | null>(
    null,
  );
  const [error, setError] = useState(false);
  const dayMeals = state.mealEntries.filter(
    (meal) => meal.date === state.selectedDate,
  );
  const recordedIntake = sumMealNutrition(dayMeals);
  const trainingPlan =
    state.planItems.find((plan) => plan.date === state.selectedDate) ?? null;
  const nutritionPlan =
    state.nutritionPlansByDate[state.selectedDate] ??
    (state.nutritionPlan?.planDate === state.selectedDate
      ? state.nutritionPlan
      : null);
  const targetCalories = nutritionPlan?.dailyTargets.caloriesKcal ?? 2350;
  const nutritionConstraints = [
    state.profile.foodSafetyStatus === "有"
      ? `食物限制：${[
          ...state.profile.foodAllergies,
          state.profile.foodSafetyDetail.trim(),
        ].filter(Boolean).join("、")}`
      : "无已知食物过敏、不耐受或明确忌口",
    state.profile.runningGiFrequency === "从不"
      ? "跑步中无常见胃肠不适"
      : `跑步中${state.profile.runningGiFrequency}出现：${state.profile.runningGiSymptoms.join("、")}`,
    state.profile.fuelingGiHistory === "有过"
      ? `既往补给不耐受：${state.profile.fuelingGiDetail}`
      : "无既往补给相关胃肠不适",
  ];
  const recipeRequest = useMemo(
    () => ({
      date: state.selectedDate,
      planTitle: trainingPlan?.title ?? "恢复日营养",
      planVersion: trainingPlan?.version ?? 0,
      targetCalories,
      consumedCalories: recordedIntake.caloriesKcal,
      existingMealNames: dayMeals.map((meal) => meal.name),
      nutritionConstraints,
      variantByMeal,
    }),
    [
      state.selectedDate,
      trainingPlan?.id,
      trainingPlan?.version,
      targetCalories,
      recordedIntake.caloriesKcal,
      dayMeals.map((meal) => meal.name).sort().join("|"),
      nutritionConstraints.join("|"),
      JSON.stringify(variantByMeal),
      retryNonce,
    ],
  );
  useEffect(() => {
    let active = true;
    setLoading(true);
    setError(false);
    services.agent
      .dailyRecipe(recipeRequest)
      .then((result) => {
        if (active) setRecipePlan(result.data);
      })
      .catch(() => {
        if (active) setError(true);
      })
      .finally(() => {
        if (active) setLoading(false);
      });
    return () => {
      active = false;
    };
  }, [recipeRequest]);
  if (loading)
    return (
      <div className="page">
        <PageHeader title="AI 食谱" />
        <div className="skeleton-stack">
          <div />
          <div />
          <div />
        </div>
        <p className="loading-copy">
          AI 教练正在核对今天的训练、热量目标和已记录餐次，再把剩下的饮食安排补齐…
        </p>
      </div>
    );
  if (error)
    return (
      <div className="page">
        <PageHeader title="AI 食谱" />
        <EmptyState
          title="这次食谱暂时没生成出来"
          detail="已经记录的餐次和旧食谱都没有被覆盖，可以再试一次。"
          action={
            <Button onClick={() => setRetryNonce((nonce) => nonce + 1)}>
              重新生成
            </Button>
          }
        />
      </div>
    );
  if (!recipePlan) return null;
  if (!recipePlan.recipes.length)
    return (
      <div className="page recipe-plan-page">
        <PageHeader title="AI 训练食谱" />
        <EmptyState
          title="今天的主要餐次已经记齐了"
          detail="早餐、午餐、晚餐和加餐都有记录，我不会覆盖它们。想修改时回餐次日记就好。"
          action={
            <Button
              onClick={() =>
                patch({ route: "NUT-05", stack: ["NUT-01"] })
              }
            >
              返回餐次日记
            </Button>
          }
        />
      </div>
    );
  const generatedCalories = recipePlan.recipes.reduce(
    (sum, recipe) => sum + recipe.calories,
    0,
  );
  return (
    <div className="page recipe-plan-page">
      <PageHeader title="AI 训练食谱" />
      <Card className="recipe-plan-context">
        <div className="recipe-plan-context__head">
          <span><Sparkles /> AGENT DAILY PLAN</span>
          <StatusBadge tone="green">{recipePlan.recipes.length} 个餐次待补全</StatusBadge>
        </div>
        <h1>把今天剩下的饮食安排好</h1>
        <p>已保留{recipePlan.existingMealNames.join("、") || "当前记录"}，仅生成其余餐次，不覆盖已记录食物。</p>
        <div className="recipe-plan-targets">
          <div><span>今日训练</span><b>{recipePlan.planTitle}</b></div>
          <div><span>热量目标</span><b>{recipePlan.targetCalories} kcal</b></div>
          <div><span>已摄入</span><b>{recipePlan.consumedCalories} kcal</b></div>
          <div><span>待安排</span><b>{recipePlan.remainingCalories} kcal</b></div>
        </div>
      </Card>
      <div className="recipe-meal-list">
        {recipePlan.recipes.map((recipe) => (
          <Card className="recipe-meal-card" key={recipe.mealType}>
            <div className="recipe-meal-card__head">
              <span className={`recipe-meal-icon recipe-meal-icon--${recipe.mealType}`}><Utensils /></span>
              <div><small>{recipe.mealName}</small><h2>{recipe.title}</h2></div>
              <StatusBadge tone="green">{recipe.calories} kcal</StatusBadge>
            </div>
            <p className="recipe-meal-description">{recipe.description}</p>
            <div className="recipe-meal-macros">
              <span>碳水 <b>{recipe.carbs} g</b></span>
              <span>蛋白 <b>{recipe.protein} g</b></span>
              <span>脂肪 <b>{recipe.fat} g</b></span>
            </div>
            <details className="recipe-meal-details">
              <summary>查看食材与做法</summary>
              <div><b>食材</b><p>{recipe.ingredients}</p></div>
              <div><b>做法</b><p>{recipe.steps}</p></div>
            </details>
            <Button
              variant="secondary"
              className="recipe-swap-button"
              disabled={loading}
              onClick={() =>
                setVariantByMeal((current) => ({
                  ...current,
                  [recipe.mealType]: (current[recipe.mealType] ?? 0) + 1,
                }))
              }
            >
              <RefreshCw /> 换一个{recipe.mealName}方案
            </Button>
          </Card>
        ))}
      </div>
      <Card className="recipe-plan-save-summary">
        <div><span>将加入今日餐次</span><b>{recipePlan.recipes.map((recipe) => recipe.mealName).join("、")}</b></div>
        <strong>{generatedCalories}<small> kcal</small></strong>
      </Card>
      <Button
        className="page-primary"
        disabled={saving}
        onClick={async () => {
          setSaving(true);
          try {
            const generatedMeals: MealEntry[] = recipePlan.recipes.map(
              (recipe) => ({
                id: `${recipe.mealType}-${recipePlan.date}`,
                name: recipe.mealName,
                date: recipePlan.date,
                foods: [
                  {
                    id: `agent-recipe-${recipe.mealType}-${Date.now()}`,
                    name: recipe.title,
                    amount: "1 份",
                    caloriesKcal: recipe.calories,
                    hydrationMl: 0,
                    carbohydrateG: recipe.carbs,
                    proteinG: recipe.protein,
                    fatG: recipe.fat,
                    source: "agent-recipe",
                  },
                ],
              }),
            );
            await Promise.all(
              generatedMeals.map((meal) =>
                services.nutrition.recordMealFood(meal.id, meal.foods[0]),
              ),
            );
            const generatedIds = new Set(generatedMeals.map((meal) => meal.id));
            patch({
              trainingMealCalories: generatedCalories,
              mealEntries: [
                ...state.mealEntries.filter((meal) => !generatedIds.has(meal.id)),
                ...generatedMeals,
              ],
              selectedMealId: generatedMeals[0]?.id ?? "",
              toast: `${generatedMeals.map((meal) => meal.name).join("、")}食谱已保存并加入今日饮食`,
              route: "NUT-05",
              activeTab: "home",
              stack: ["NUT-01"],
            });
          } catch {
            patch({ toast: "AI 食谱保存失败，现有餐次记录保持不变" });
          } finally {
            setSaving(false);
          }
        }}
      >
        {saving ? "正在保存今日食谱…" : "保存并加入今日饮食"}
      </Button>
    </div>
  );
}

function NutritionAlternative() {
  const { state, patch, back } = useApp();
  const node = state.nutritionPlan?.nodes.find(
    (item) => item.id === state.selectedNutritionNodeId,
  );
  const [products, setProducts] = useState<NutritionProduct[]>([]);
  const [selectedId, setSelectedId] = useState(node?.product.id ?? "");
  const [status, setStatus] = useState<"loading" | "ready" | "error">(
    "loading",
  );
  const [saving, setSaving] = useState(false);
  useEffect(() => {
    if (!node || !state.nutritionPlan) {
      setStatus("error");
      return;
    }
    let active = true;
    services.nutrition
      .alternatives(
        state.nutritionPlan.planId,
        node.id,
        node.product.id,
        buildAgentContext(state),
      )
      .then((result) => {
        if (active) {
          setProducts(result.data);
          setStatus("ready");
        }
      })
      .catch(() => {
        if (active) setStatus("error");
      });
    return () => {
      active = false;
    };
  }, [node?.id]);
  if (!node || !state.nutritionPlan)
    return (
      <div className="page">
        <PageHeader title="补给替代" />
        <EmptyState
          title="还没选要替换的补给"
          detail="回到方案页，点一下想换掉的补给品卡片就可以。"
          action={<Button onClick={back}>返回补给方案</Button>}
        />
      </div>
    );
  if (status === "loading")
    return (
      <div className="page">
        <PageHeader title="补给替代" />
        <div className="skeleton-stack" role="status">
          <div />
          <div />
        </div>
        <p className="loading-copy">AI 教练正在核对等效剂量、时机和耐受情况…</p>
      </div>
    );
  if (status === "error")
    return (
      <div className="page">
        <PageHeader title="补给替代" />
        <EmptyState
          title="替代方案暂时没加载出来"
          detail="原补给品没有变化，可以返回后再试。"
          action={<Button onClick={back}>返回原方案</Button>}
        />
      </div>
    );
  const selected =
    products.find((product) => product.id === selectedId) ?? node.product;
  return (
    <div className="page nutrition-alternative-page">
      <PageHeader title="选择等效替代" />
      <div className="page-intro">
        <h1>
          {node.phase} · {node.time}
        </h1>
        <p>
          保持“{node.target}”的补给目标不变，Agent 已按剂量、时机与耐受筛选。
        </p>
      </div>
      <div className="alternative-product-list">
        {products.map((product) => (
          <button
            type="button"
            className={`card product-choice product-choice--visual ${selectedId === product.id ? "selected" : ""}`}
            aria-pressed={selectedId === product.id}
            onClick={() => setSelectedId(product.id)}
            key={product.id}
          >
            <img src={product.image} alt={product.name} />
            <div>
              <b>{product.name}</b>
              <span>{product.dose}</span>
              <small>
                {product.timing} · {product.reason}
              </small>
            </div>
            {selectedId === product.id && <CheckCircle2 />}
          </button>
        ))}
      </div>
      <Card className="scope-note">
        <ShieldCheck />
        <p>替代只调整当前节点；训练目标、总补给目标和其他节点保持不变。</p>
      </Card>
      <Button
        disabled={saving || !selectedId}
        className="page-primary"
        onClick={async () => {
          setSaving(true);
          try {
            await services.nutrition.replaceProduct(
              state.nutritionPlan!.planId,
              node.id,
              selected.id,
            );
            const nodes = state.nutritionPlan!.nodes.map((item) =>
              item.id === node.id
                ? { ...item, product: selected, target: selected.dose }
                : item,
            );
            patch({
              nutritionPlan: { ...state.nutritionPlan!, nodes },
              selectedFuelProduct:
                selected.category === "gel"
                  ? selected.name
                  : state.selectedFuelProduct,
              reminder:
                node.id === "during-gel"
                  ? {
                      ...state.reminder,
                      productName: selected.name,
                      amount: selected.dose,
                    }
                  : state.reminder,
              toast: `${node.phase}补给已替换为${selected.name}`,
            });
            back();
          } catch {
            patch({ toast: "替换失败，原补给品保持不变" });
          } finally {
            setSaving(false);
          }
        }}
      >
        {saving ? (
          <>
            <LoaderCircle className="spin" /> 正在应用
          </>
        ) : (
          "确认替换当前节点"
        )}
      </Button>
    </div>
  );
}

function RecordDay() {
  const { state, go } = useApp();
  const record =
    state.activities.find((item) => item.date === state.selectedDate) ?? null;
  const plan =
    state.planItems.find(
      (item) =>
        item.date === state.selectedDate || item.id === record?.planItemId,
    ) ?? null;
  return (
    <div className="page">
      <PageHeader title="当日详情" />
      <div className="date-title">
        {new Date(state.selectedDate).toLocaleDateString("zh-CN", {
          year: "numeric",
          month: "long",
          day: "numeric",
          weekday: "long",
        })}
      </div>
      <SectionTitle title="原计划" />
      {plan ? (
        <PlanCard item={plan} />
      ) : (
        <EmptyState
          title="自由训练，无关联计划"
          detail="这条设备实绩会独立保留；如果后续与云端计划匹配，可通过 planItemId 建立关联。"
        />
      )}
      <SectionTitle title="设备实绩" />
      {record ? (
        <Card className="record-detail-card" onClick={() => go("RECORD-03")}>
          <div className="record-card__icon">
            <Watch />
          </div>
          <div>
            <b>{record.distance} 公里 · 已完成</b>
            <span>
              {record.duration} · {record.pace}/km
            </span>
            <p>来源：{record.source}</p>
          </div>
          <ChevronRight />
        </Card>
      ) : (
        <EmptyState
          title="尚无设备实绩"
          detail="原计划会保留，设备记录同步后在这里单独显示。"
        />
      )}
      <Card className="layer-note">
        <LayersIcon />
        <p>计划与实绩是两层独立数据，通过 planItemId 关联，不会互相覆盖。</p>
      </Card>
    </div>
  );
}

function SessionLineChart({
  title,
  subtitle,
  data,
  dataKey,
  color,
  valueFormatter,
}: {
  title: string;
  subtitle: string;
  data: Array<Record<string, string | number>>;
  dataKey: string;
  color: string;
  valueFormatter: (value: number) => string;
}) {
  return (
    <section className="record-session-chart" aria-label={`${title}折线图`}>
      <div className="record-section-head"><div><span>{subtitle}</span><h2>{title}</h2></div><small>{valueFormatter(Number(data.at(-1)?.[dataKey] ?? 0))}</small></div>
      <div className="record-session-chart__canvas">
        <ResponsiveContainer width="100%" height="100%">
          <LineChart data={data} margin={{ top: 12, right: 8, bottom: 0, left: -13 }}>
            <CartesianGrid stroke="rgba(24, 61, 42, .08)" strokeDasharray="2 4" vertical={false} />
            <XAxis dataKey="point" tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} minTickGap={22} />
            <YAxis tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} domain={["dataMin - 5", "dataMax + 5"]} tickFormatter={(value) => dataKey === "paceSeconds" ? paceFromSeconds(Number(value)).replace("″", "") : String(value)} />
            <Tooltip contentStyle={{ border: "1px solid rgba(24, 61, 42, .12)", borderRadius: 8, background: "rgba(251, 252, 249, .98)", fontSize: 10 }} formatter={(value) => [valueFormatter(Number(value)), title]} />
            <Line type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2.2} dot={false} activeDot={{ r: 4 }} isAnimationActive={false} />
          </LineChart>
        </ResponsiveContainer>
      </div>
    </section>
  );
}

function SessionZoneDistribution({ label, zones }: { label: string; zones: SessionZone[] }) {
  return (
    <div className="record-zone-distribution">
      <b>{label}</b>
      <div className="record-zone-distribution__bar" aria-label={`${label}分布`}>
        {zones.map((zone) => <i key={zone.label} style={{ width: `${zone.value}%`, background: zone.color }} title={`${zone.label} ${zone.value}%`} />)}
      </div>
      <div className="record-zone-distribution__legend">
        {zones.map((zone) => <span key={zone.label}><i style={{ background: zone.color }} />{zone.label}<b>{zone.value}%</b></span>)}
      </div>
    </div>
  );
}

function SessionElevationChart({
  data,
  gain,
}: {
  data: Array<{ point: string; elevation: number }>;
  gain: number;
}) {
  return (
    <section className="record-session-chart record-elevation-chart" aria-label="本次训练海拔曲线">
      <div className="record-section-head"><div><span>本次路线</span><h2>累计爬升与海拔曲线</h2></div><small>+{gain} m</small></div>
      <div className="record-session-chart__canvas">
        <ResponsiveContainer width="100%" height="100%">
          <AreaChart data={data} margin={{ top: 12, right: 8, bottom: 0, left: -13 }}>
            <defs><linearGradient id="recordElevationFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#d2a227" stopOpacity={0.34} /><stop offset="100%" stopColor="#d2a227" stopOpacity={0.03} /></linearGradient></defs>
            <CartesianGrid stroke="rgba(24, 61, 42, .08)" strokeDasharray="2 4" vertical={false} />
            <XAxis dataKey="point" tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} minTickGap={22} />
            <YAxis tick={{ fontSize: 8, fill: "#7a857e" }} tickLine={false} axisLine={false} domain={["dataMin - 5", "dataMax + 5"]} />
            <Tooltip contentStyle={{ border: "1px solid rgba(24, 61, 42, .12)", borderRadius: 8, background: "rgba(251, 252, 249, .98)", fontSize: 10 }} formatter={(value) => [`${value} m`, "海拔"]} />
            <Area type="monotone" dataKey="elevation" stroke="#bd8c18" strokeWidth={2} fill="url(#recordElevationFill)" isAnimationActive={false} />
          </AreaChart>
        </ResponsiveContainer>
      </div>
    </section>
  );
}

function RecordTrainingSummaryModule({
  record,
  plan,
  evaluation,
  evaluationStatus,
  feedbackState,
  onRetry,
  onTrainingFeedback,
  onNutritionFeedback,
}: {
  record: ActivityRecord;
  plan?: PlanItem | null;
  evaluation: TrainingAnalysisData | null;
  evaluationStatus: "loading" | "ready" | "error";
  feedbackState?: { training: boolean; nutrition: boolean } | null;
  onRetry: () => void;
  onTrainingFeedback: () => void;
  onNutritionFeedback: () => void;
}) {
  const unlocked = Boolean(feedbackState?.training && feedbackState?.nutrition);
  const completedCount =
    Number(Boolean(feedbackState?.training)) + Number(Boolean(feedbackState?.nutrition));
  if (!unlocked) {
    return (
      <article
        className="cycle-agent-summary record-training-evaluation record-training-evaluation--locked"
        data-status="locked"
        aria-labelledby="record-summary-locked-title"
      >
        <div className="cycle-agent-summary__head">
          <div className="cycle-agent-summary__identity">
            <span className="cycle-agent-summary__mark" aria-hidden="true">CPT</span>
            <div><b id="record-summary-locked-title">AI 教练本次机能总结</b><small>设备数据已收到，等待你的两项反馈</small></div>
          </div>
          <span className="cycle-agent-summary__state"><MessageCircleMore aria-hidden="true" />{completedCount}/2 待补充</span>
        </div>
        <div className="record-summary-locked__body">
          <span>总结待解锁 <small>POST-RUN REVIEW</small></span>
          <h3>再告诉我训练感受和补给执行，我就能把身体的声音与这次数据放在一起判断。</h3>
          <p>距离、配速、心率等客观记录已经保存；未补齐反馈前不会提前生成结论。</p>
          <div className="record-summary-locked__steps" aria-label="训练总结解锁进度">
            <button type="button" className={feedbackState?.training ? "is-complete" : ""} disabled={feedbackState?.training} onClick={onTrainingFeedback}>
              {feedbackState?.training ? <CheckCircle2 aria-hidden="true" /> : <MessageCircleMore aria-hidden="true" />}
              <span><b>训练感受</b><small>{feedbackState?.training ? "已完成" : "去补充"}</small></span>
              {!feedbackState?.training && <ChevronRight aria-hidden="true" />}
            </button>
            <button type="button" className={feedbackState?.nutrition ? "is-complete" : ""} disabled={feedbackState?.nutrition} onClick={onNutritionFeedback}>
              {feedbackState?.nutrition ? <CheckCircle2 aria-hidden="true" /> : <Droplets aria-hidden="true" />}
              <span><b>补给执行</b><small>{feedbackState?.nutrition ? "已完成" : "去反馈"}</small></span>
              {!feedbackState?.nutrition && <ChevronRight aria-hidden="true" />}
            </button>
          </div>
        </div>
      </article>
    );
  }
  return (
    <article
      className={`cycle-agent-summary record-training-evaluation cycle-agent-summary--${evaluationStatus}`}
      data-status={evaluationStatus}
      data-context-version={evaluation?.contextVersion ?? "pending"}
      aria-live="polite"
      aria-busy={evaluationStatus === "loading"}
    >
      <div className="cycle-agent-summary__head">
        <div className="cycle-agent-summary__identity">
          <span className="cycle-agent-summary__mark" aria-hidden="true">CPT</span>
          <div><b>AI 教练本次机能总结</b><small>设备实绩、训练感受与补给反馈共同判断</small></div>
        </div>
        <span className="cycle-agent-summary__state">{evaluationStatus === "loading" ? <LoaderCircle aria-hidden="true" /> : <Activity aria-hidden="true" />}{evaluationStatus === "loading" ? "正在总结" : evaluationStatus === "error" ? "总结失败" : "完整分析"}</span>
      </div>
      {evaluationStatus === "loading" ? (
        <div className="cycle-agent-summary__loading" role="status"><span /><span /><span /><small>AI 教练正在整理这次训练的机能变化…</small></div>
      ) : evaluationStatus === "error" ? (
        <div className="cycle-agent-summary__error"><div><AlertTriangle aria-hidden="true" /><span>本次总结暂时没有生成，训练记录已经保留。</span></div><button type="button" onClick={onRetry}><RefreshCw aria-hidden="true" />重新总结</button></div>
      ) : evaluation ? (
        <div className="record-shared-training-report">
          <TrainingPerformanceReportBody activity={record} plan={plan} analysis={evaluation} />
          <small>与首页训练后对话使用同一训练记录和同一分析结果 · 长期变化请前往 TRAIN-HUB</small>
        </div>
      ) : null}
    </article>
  );
}

function RecordDetail() {
  const { state, patch, go, openNutritionFeedback } = useApp();
  const record =
    state.activities.find((item) => item.id === state.lastCompletedActivityId) ??
    state.activities.find((item) => item.date === state.selectedDate) ??
    state.activities[0];
  const plan =
    state.planItems.find((item) => item.id === record?.planItemId) ??
    state.planItems.find((item) => item.date === state.selectedDate) ??
    null;
  const nutritionNodes =
    state.nutritionPlansByDate[plan?.date ?? ""]?.nodes ??
    state.nutritionPlan?.nodes ??
    [];
  const completedNutritionNodes =
    state.completedNutritionNodesByDate[plan?.date ?? ""] ??
    state.completedNutritionNodes;
  const completedNutritionCount = nutritionNodes.filter((node) =>
    completedNutritionNodes.includes(node.id),
  ).length;
  const postTrainingState = record
    ? state.postTrainingFeedbackByActivity[record.id]
    : null;
  const fullAnalysisUnlocked = Boolean(
    postTrainingState?.training && postTrainingState?.nutrition,
  );
  const feedbackContextSuffix = state.feedback.submitted
    ? `feedback-${state.feedback.rpe}-${state.feedback.thirst}-${state.feedback.soreness}-${state.feedback.gi}`
    : "objective-only";
  const analysisContextSuffix = record
    ? `nutrition-${completedNutritionCount}-of-${nutritionNodes.length}-${record.nutritionCompletion}-${feedbackContextSuffix}`
    : "pending";
  const storedEvaluationIsCurrent = Boolean(
    record &&
      plan &&
      state.trainingAnalysis?.planItemId === plan.id &&
      state.trainingAnalysis.activityId === record.id &&
      state.trainingAnalysis.contextVersion.endsWith(analysisContextSuffix),
  );
  const [evaluation, setEvaluation] = useState<TrainingAnalysisData | null>(
    storedEvaluationIsCurrent ? state.trainingAnalysis : null,
  );
  const [evaluationStatus, setEvaluationStatus] = useState<
    "loading" | "ready" | "error"
  >(storedEvaluationIsCurrent ? "ready" : "loading");
  const [evaluationRetry, setEvaluationRetry] = useState(0);
  const sessionPerformance = record
    ? buildSessionPerformance(record, plan)
    : null;

  useEffect(() => {
    if (!record || !fullAnalysisUnlocked) return;
    if (!plan) {
      setEvaluation({
        planItemId: "unplanned",
        activityId: record.id,
        rows: [],
        headline: "本次训练已完成，先建立个人执行基线",
        summary: `本次完成 ${record.distance.toFixed(2)} km，用时 ${record.duration}，平均配速 ${record.pace}/km、平均心率 ${record.heartRate} bpm。由于没有关联计划，这次数据将作为后续训练评价的个人基线。`,
        adjustment: "下一次训练前先关联计划目标，完成后即可获得计划与实绩的逐项评价。",
        evidence: [
          `完成距离 ${record.distance.toFixed(2)} km`,
          `平均配速 ${record.pace}/km`,
          `平均心率 ${record.heartRate} bpm`,
        ],
        contextVersion: `record-detail-${record.id}`,
      });
      setEvaluationStatus("ready");
      return;
    }
    if (storedEvaluationIsCurrent && state.trainingAnalysis) {
      setEvaluation(state.trainingAnalysis);
      setEvaluationStatus("ready");
      return;
    }
    let active = true;
    setEvaluationStatus("loading");
    services.agent
      .analyzeTraining(
        plan,
        record,
        nutritionNodes,
        completedNutritionNodes,
        state.feedback,
      )
      .then((result) => {
        if (!active) return;
        setEvaluation(result.data);
        patch({ trainingAnalysis: result.data });
        setEvaluationStatus("ready");
      })
      .catch(() => {
        if (active) setEvaluationStatus("error");
      });
    return () => {
      active = false;
    };
  }, [
    record?.id,
    plan?.id,
    fullAnalysisUnlocked,
    analysisContextSuffix,
    storedEvaluationIsCurrent,
    state.trainingAnalysis?.contextVersion,
    evaluationRetry,
  ]);
  if (!record)
    return (
      <div className="page">
        <PageHeader title="训练详情" />
        <EmptyState
          title="这里还等着你的第一条训练"
          detail="练完并同步运动设备后，距离、配速和心率都会在这里留下来。"
        />
      </div>
    );
  return (
    <div className="page record-detail-page">
      <PageHeader title="训练详情" />
      <div className="run-map">
        <Route />
        <span>城市公园环线 · 本次运动路线</span>
        <div className="route-line" />
      </div>
      <div className="macro-strip">
        <Metric value={record.distance.toFixed(2)} unit="km" label="距离" />
        <Metric value={record.duration} label="用时" />
        <Metric value={record.pace} unit="/km" label="平均配速" />
      </div>
      {sessionPerformance && (
        <>
          <RecordTrainingSummaryModule
            record={record}
            plan={plan}
            evaluation={evaluation}
            evaluationStatus={evaluationStatus}
            feedbackState={postTrainingState}
            onRetry={() => setEvaluationRetry((value) => value + 1)}
            onTrainingFeedback={() => go("TRAIN-FEEDBACK")}
            onNutritionFeedback={() => openNutritionFeedback("RECORD-03")}
          />

          <section className="record-session-summary" aria-labelledby="record-session-summary-title">
            <div className="record-section-head">
              <div><span>本次结果</span><h2 id="record-session-summary-title">训练质量概览</h2></div>
              <small>{record.source}</small>
            </div>
            <div className="record-session-metrics">
              <article><span>平均心率</span><b>{record.heartRate}<small> bpm</small></b></article>
              <article><span>最快配速</span><b>{sessionPerformance.fastestPace}<small>/km</small></b></article>
              <article><span>训练负荷</span><b>{sessionPerformance.trainingLoad}</b></article>
              <article><span>平均步频</span><b>{sessionPerformance.cadence}<small> spm</small></b></article>
              <article><span>平均步长</span><b>{sessionPerformance.strideLength}<small> m</small></b></article>
              <article><span>本次 VDOT</span><b>{sessionPerformance.vdot}</b></article>
              <article><span>累计爬升</span><b>{sessionPerformance.elevationGain}<small> m</small></b></article>
              <article><span>恢复时间</span><b>{sessionPerformance.recoveryHours}<small> h</small></b></article>
            </div>
            <div className="record-load-range">
              <div><span>训练负荷位置</span><b>建议区间 {sessionPerformance.recommendedLoad[0]}–{sessionPerformance.recommendedLoad[1]}</b></div>
              <div className="record-load-range__track" aria-label={`训练负荷 ${sessionPerformance.trainingLoad}，建议区间 ${sessionPerformance.recommendedLoad[0]} 到 ${sessionPerformance.recommendedLoad[1]}`}>
                <i style={{ left: `${Math.min(92, (sessionPerformance.trainingLoad / (sessionPerformance.recommendedLoad[1] * 1.25)) * 100)}%` }} />
                <span style={{ left: `${(sessionPerformance.recommendedLoad[0] / (sessionPerformance.recommendedLoad[1] * 1.25)) * 100}%`, width: `${((sessionPerformance.recommendedLoad[1] - sessionPerformance.recommendedLoad[0]) / (sessionPerformance.recommendedLoad[1] * 1.25)) * 100}%` }} />
              </div>
            </div>
          </section>

          <SessionLineChart title="配速趋势" subtitle="仅显示本次训练" data={sessionPerformance.paceTrend} dataKey="paceSeconds" color="#2d88b7" valueFormatter={(value) => `${paceFromSeconds(value)}/km`} />
          <SessionLineChart title="心率趋势" subtitle="仅显示本次训练" data={sessionPerformance.heartRateTrend} dataKey="heartRate" color="#d35b45" valueFormatter={(value) => `${Math.round(value)} bpm`} />

          <section className="record-zone-section" aria-labelledby="record-zone-title">
            <div className="record-section-head"><div><span>强度结构</span><h2 id="record-zone-title">本次区间分布</h2></div><small>合计 100%</small></div>
            <SessionZoneDistribution label="配速区间" zones={sessionPerformance.paceZones} />
            <SessionZoneDistribution label="心率区间" zones={sessionPerformance.heartRateZones} />
          </section>

          <section className="record-split-section" aria-labelledby="record-splits-title">
            <div className="record-section-head"><div><span>公里拆分</span><h2 id="record-splits-title">分段数据</h2></div><small>本次训练</small></div>
            <div className="record-split-table" role="table" aria-label="本次训练分段数据">
              <div className="record-split-table__head" role="row"><span>分段</span><span>配速</span><span>心率</span><span>步频</span><span>用时</span></div>
              {sessionPerformance.splits.map((split) => (
                <div className="record-split-table__row" role="row" key={split.segment}>
                  <b>{split.segment}</b><span>{split.pace}</span><span>{split.heartRate}</span><span>{split.cadence}</span><span>{split.duration}</span>
                </div>
              ))}
            </div>
          </section>

          <SessionElevationChart data={sessionPerformance.elevationTrend} gain={sessionPerformance.elevationGain} />
        </>
      )}
    </div>
  );
}

function TrendPage() {
  const { openAnalysis } = useApp();
  const [period, setPeriod] = useState(4);
  const trendCopy =
    period === 4
      ? "近 4 周跑量从 28 km 提升至 34 km，变化平稳。"
      : period === 8
        ? "近 8 周跑量从 24 km 提升至 34 km，中间安排了一个恢复周。"
        : "近 12 周完成基础期到提升期过渡，周跑量增长处于安全区间。";
  return (
    <div className="page">
      <PageHeader title="趋势分析" />
      <div className="period-tabs">
        {[4, 8, 12].map((weeks) => (
          <button
            key={weeks}
            aria-pressed={period === weeks}
            className={period === weeks ? "selected" : ""}
            onClick={() => setPeriod(weeks)}
          >
            {weeks} 周
          </button>
        ))}
      </div>
      <MetricChart
        key={period}
        title={`${period} 周周跑量`}
        unit="km"
        color="green"
      />
      <AiSummary
        summary={trendCopy}
        onClick={() =>
          openAnalysis(`${period} 周跑量趋势`, "RECORD-04", trendCopy)
        }
      />
      <Card className="trend-stat">
        <TrendingUp />
        <div>
          <span>计划完成率</span>
          <b>
            {period === 4 ? 82 : period === 8 ? 79 : 76}%{" "}
            <small>+{period === 4 ? 9 : period === 8 ? 7 : 5}%</small>
          </b>
        </div>
        <div>
          <span>营养执行率</span>
          <b>
            {period === 4 ? 76 : period === 8 ? 72 : 69}%{" "}
            <small>+{period === 4 ? 12 : period === 8 ? 9 : 6}%</small>
          </b>
        </div>
      </Card>
    </div>
  );
}

function ProfilePage() {
  const { state, go } = useApp();
  if (!state.profileComplete)
    return (
      <div className="page hub-page profile-page-v21">
        <PageHeader title="我的" back={false} />
        <p className="hub-subtitle">AI 跑步助手</p>
        <Card className="profile-completion-entry">
          <CircleGauge />
          <div>
            <span>先认识一下彼此</span>
            <h2>让我更懂你的跑步目标</h2>
            <p>
              用大约 3 分钟告诉我目标、近期训练、身体感受与补给习惯，之后的建议才会真正贴合你。
            </p>
          </div>
          <Button onClick={() => go("ONB-01")}>开始建立档案</Button>
        </Card>
      </div>
    );
  if (state.planGenerationStatus !== "ready")
    return (
      <div className="page hub-page profile-page-v21">
        <PageHeader title="我的" back={false} />
        <p className="hub-subtitle">AI 跑步助手</p>
        <Card className="profile-hero profile-hero--incomplete">
          <div className="profile-avatar">
            <img src="/runner-profile-avatar-v21.png" alt="晨跑者头像" />
          </div>
          <div>
            <h1>晨跑者</h1>
            <p>档案已经准备好 · 下一段计划等你来定</p>
            <small>
              {state.profile.gender === "male"
                ? "男"
                : state.profile.gender === "female"
                  ? "女"
                  : "性别未透露"} · {state.profile.heightCm} cm · {state.profile.weightKg} kg
            </small>
            <div className="profile-completion">
              <b>基础档案完整</b>
              <span>
                <i style={{ width: "100%" }} />
              </span>
            </div>
          </div>
        </Card>
        <Card className="profile-completion-entry">
          <Sparkles />
          <div>
            <span>下一步</span>
            <h2>一起把训练计划定下来</h2>
            <p>
              可以先补充档案，也可以回首页直接告诉 AI 教练你的想法。
            </p>
          </div>
          <Button onClick={() => go("ONB-01")}>先补充我的档案</Button>
          <Button variant="secondary" onClick={() => go("HOME-01", "home")}>
            去首页一起规划
          </Button>
        </Card>
        <SectionTitle title="还在等你开始" />
        <Card className="profile-locked-features">
          <div>
            <CalendarCheck />
            <span>
              <b>训练计划</b>
              <small>和 AI 教练聊聊就能开始</small>
            </span>
          </div>
          <div>
            <Droplets />
            <span>
              <b>营养规划</b>
              <small>随训练计划配套生成</small>
            </span>
          </div>
        </Card>
      </div>
    );
  return (
    <div className="page hub-page profile-page-v21">
      <PageHeader
        title="我的"
        back={false}
        action={<span className="assistant-live-dot" aria-label="数据已同步" />}
      />
      <p className="hub-subtitle">AI 跑步助手</p>
      <Card
        className="profile-hero profile-hero--account-entry"
        data-auth-required={state.authMode === "guest" ? "account" : undefined}
        onClick={() => go("ACCOUNT-SECURITY")}
        aria-label="进入账号与安全"
      >
        <div className="profile-avatar">
          <img src="/runner-profile-avatar-v21.png" alt="晨跑者头像" />
        </div>
        <div>
          <h1>晨跑者</h1>
          <p>CPT 跑者账号</p>
          <small>
            {state.profile.gender === "male"
              ? "男"
              : state.profile.gender === "female"
                ? "女"
                : "性别未透露"} · {state.profile.heightCm} cm · {state.profile.weightKg} kg
          </small>
        </div>
        <span className="profile-hero-link" aria-hidden="true">
          <ChevronRight />
        </span>
      </Card>
      <Card className="profile-goal profile-goal--readonly">
        <img src="/race-medal.png" alt="半程马拉松奖牌" />
        <div>
          <small>目标与训练设置</small>
          <b>{state.profile.goal}</b>
          <span>
            <CalendarCheck /> 每周 {state.profile.frequency} 次
            <i aria-hidden="true">·</i> {state.planProgram.phase} 第
            {state.planProgram.currentWeek}/{state.planProgram.totalWeeks}周
          </span>
        </div>
      </Card>
      <SectionTitle title="健康平台与数据" />
      <Card className="device-summary">
        <button
          className="profile-device-main"
          aria-label="管理训练数据与设备"
          onClick={() => go("DEV-01")}
        >
          <img src="/sports-watch.png" alt="训练数据来源" />
          <span>
            <b>训练数据 · 已连接</b>
            <small>
              {state.device.connectedProviders.join("、")} · 优先经健康平台同步
            </small>
          </span>
          <StatusBadge tone={state.device.status === "synced" ? "green" : "gray"}>
            {state.device.status === "synced" ? "数据最新" : "待同步"}
          </StatusBadge>
          <ChevronRight />
        </button>
      </Card>
      <SectionTitle title="我的设定" />
      <Card className="settings-card profile-insights">
        <button
          type="button"
          aria-label="编辑跑步习惯"
          onClick={() => go("ONB-01")}
        >
          <img src="/runner-illustration-unbranded.png" alt="跑者" />
          <span>
            <b>跑步习惯</b>
            <small>
              近阶段周均 {state.profile.weeklyKm} km · 每周 {state.profile.trainingDays.length} 天可训练
            </small>
          </span>
          <ChevronRight />
        </button>
        <button type="button" aria-label="编辑补给习惯" onClick={() => go("ONB-03")}>
          <img
            src="/profile-nutrition-cartoon-v27.png"
            alt="动画风运动营养跑者"
          />
          <span>
            <b>补给习惯</b>
            <small>
              出汗
              {
                ["", "少", "轻微", "明显", "大量", "湿透"][state.profile.sweat]
              }{" "}
              · 每小时碳水 {state.profile.carbsPerHour}
            </small>
          </span>
          <ChevronRight />
        </button>
      </Card>
      <SectionTitle title="权限与隐私" />
      <div className="profile-actions profile-settings-actions">
        <button onClick={() => go("DEV-02")}>
          <LockKeyhole />
          <span>
            <b>权限管理</b>
            <small>定位、训练提醒与数据使用范围</small>
          </span>
          <ChevronRight />
        </button>
      </div>
      <SectionTitle title="账户与服务" />
      <div className="profile-actions profile-service-actions">
        <button onClick={() => go("CONTACT-DEV")}>
          <Headphones />
          <span>
            <b>和开发者聊聊</b>
            <small>提交建议或问题反馈</small>
          </span>
          <ChevronRight />
        </button>
        <button onClick={() => go("LANGUAGE-SETTINGS")}>
          <Languages />
          <span>
            <b>语言设置</b>
            <small>{state.language === "zh-CN" ? "简体中文" : "English"}</small>
          </span>
          <ChevronRight />
        </button>
        <button onClick={() => go("VERSION-INFO")}>
          <FileText />
          <span>
            <b>版本信息</b>
            <small>更新、协议与隐私清单</small>
          </span>
          <ChevronRight />
        </button>
        <button onClick={() => go("ABOUT-CPT")}>
          <Info />
          <span>
            <b>关于 CPT</b>
            <small>产品理念与服务边界</small>
          </span>
          <ChevronRight />
        </button>
      </div>
      <p className="profile-basis">
        <Info /> 建议依据：档案、设备、天气与历史训练
      </p>
    </div>
  );
}

function ConfirmAccountAction({
  action,
  onClose,
}: {
  action: "logout" | "delete";
  onClose: () => void;
}) {
  const { go, patch } = useApp();
  useEffect(() => {
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
    };
    window.addEventListener("keydown", closeOnEscape);
    return () => window.removeEventListener("keydown", closeOnEscape);
  }, [onClose]);
  const deleting = action === "delete";
  const [submitting, setSubmitting] = useState(false);
  return (
    <div
      className="dialog-backdrop"
      onPointerDown={(event) =>
        event.target === event.currentTarget && onClose()
      }
    >
      <div
        className="confirm-dialog"
        role="alertdialog"
        aria-modal="true"
        aria-labelledby="account-action-title"
        aria-describedby="account-action-description"
      >
        <span className="dialog-kicker">
          {deleting ? "不可撤销的操作" : "退出当前账号"}
        </span>
        <h2 id="account-action-title">
          {deleting ? "申请注销 CPT 账号？" : "确认退出登录？"}
        </h2>
        <p id="account-action-description">
          {deleting
            ? "提交后将进入注销审核流程；审核完成前可以继续使用账号。"
            : "退出后，本机缓存会保留，再次登录后将与服务端数据同步。"}
        </p>
        <div className="bottom-actions">
          <Button variant="secondary" autoFocus onClick={onClose}>
            取消
          </Button>
          <Button
            variant="danger"
            disabled={submitting}
            onClick={async () => {
              setSubmitting(true);
              try {
                if (deleting) await services.account.requestDeletion();
                else await services.account.logout();
                if (!deleting) localStorage.removeItem("running-ai-access-token");
                patch({
                  authMode: deleting ? "authenticated" : "guest",
                  toast: deleting
                    ? "注销申请已提交"
                    : "已退出登录，已切换为游客预览",
                });
                go(deleting ? "ACCOUNT-SECURITY" : "PROFILE-01", deleting ? undefined : "profile");
                onClose();
              } catch {
                patch({
                  toast: deleting
                    ? "注销申请提交失败，请重试"
                    : "退出登录失败，请重试",
                });
                setSubmitting(false);
              }
            }}
          >
            {submitting ? "处理中…" : deleting ? "确认申请注销" : "退出登录"}
          </Button>
        </div>
      </div>
    </div>
  );
}

function AccountSecurityPage() {
  const { state, go } = useApp();
  const [confirmAction, setConfirmAction] = useState<
    "logout" | "delete" | null
  >(null);
  const phoneLabel = state.boundPhone
    ? `${state.boundPhone.slice(0, 3)} **** ${state.boundPhone.slice(-4)}`
    : "未绑定";
  return (
    <div className="page utility-page account-security-page">
      <PageHeader title="账号与安全" />
      <div className="utility-hero">
        <ShieldCheck />
        <div>
          <span>账户保护</span>
          <h1>安全状态良好</h1>
          <p>管理登录方式、手机号和账号生命周期。</p>
        </div>
      </div>
      <SectionTitle title="登录与验证" />
      <div className="settings-list">
        <div className="settings-list__row">
          <MessageCircleMore />
          <span>
            <b>微信账号</b>
            <small>CPT_晨跑者 · 已连接</small>
          </span>
          <StatusBadge tone="green">已连接</StatusBadge>
        </div>
        <button
          className="settings-list__row"
          data-auth-required="account"
          onClick={() => go("PHONE-BIND")}
        >
          <Smartphone />
          <span>
            <b>绑定的手机号</b>
            <small>{phoneLabel}</small>
          </span>
          <ChevronRight />
        </button>
      </div>
      <SectionTitle title="账号管理" />
      <div className="settings-list settings-list--danger">
        <button
          className="settings-list__row"
          data-auth-required="account"
          onClick={() => setConfirmAction("delete")}
        >
          <Trash2 />
          <span>
            <b>注销账号</b>
            <small>提交账号与个人数据注销申请</small>
          </span>
          <ChevronRight />
        </button>
      </div>
      <Button
        className="page-primary logout-button"
        variant="secondary"
        data-auth-required="account"
        onClick={() => setConfirmAction("logout")}
      >
        <LogOut />
        退出登录
      </Button>
      <p className="utility-footnote">
        <ShieldCheck />
        账号敏感操作会通过服务端验证和审计。
      </p>
      {confirmAction && (
        <ConfirmAccountAction
          action={confirmAction}
          onClose={() => setConfirmAction(null)}
        />
      )}
    </div>
  );
}

function PhoneBindPage() {
  const { state, patch, back } = useApp();
  const [phone, setPhone] = useState(state.boundPhone);
  const [code, setCode] = useState("");
  const [codeSent, setCodeSent] = useState(false);
  const [developmentCode, setDevelopmentCode] = useState("");
  const [busy, setBusy] = useState(false);
  const digits = phone.replace(/\D/g, "");
  const phoneValid = /^1\d{10}$/.test(digits);
  return (
    <div className="page utility-page">
      <PageHeader title={state.boundPhone ? "更换手机号" : "绑定手机号"} />
      <div className="form-intro">
        <Smartphone />
        <h1>验证常用手机号</h1>
        <p>用于账号验证与重要安全提醒，不会展示给其他用户。</p>
      </div>
      <label className="field">
        <span>手机号</span>
        <div className="phone-field">
          <b>+86</b>
          <input
            type="tel"
            inputMode="numeric"
            autoComplete="tel"
            placeholder="请输入手机号"
            value={phone}
            onChange={(event) => setPhone(event.target.value)}
          />
        </div>
      </label>
      <label className="field">
        <span>验证码</span>
        <div className="verification-field">
          <input
            inputMode="numeric"
            autoComplete="one-time-code"
            placeholder="6 位验证码"
            maxLength={6}
            value={code}
            onChange={(event) => setCode(event.target.value.replace(/\D/g, ""))}
          />
          <button
            data-auth-required="account"
            disabled={!phoneValid}
            onClick={async () => {
              setBusy(true);
              try {
                const result = await services.account.sendPhoneCode(digits);
                setCodeSent(true);
                setDevelopmentCode(result.developmentCode ?? "");
                patch({ toast: "验证码已发送" });
              } catch {
                patch({ toast: "验证码发送失败，请重试" });
              } finally {
                setBusy(false);
              }
            }}
          >
            {busy ? "发送中…" : "获取验证码"}
          </button>
        </div>
      </label>
      {codeSent && (
        <p className="form-hint">
          验证码已发送，有效期 60 秒
          {developmentCode ? ` · 本地开发码 ${developmentCode}` : ""}
        </p>
      )}
      <Button
        className="page-primary"
        data-auth-required="account"
        disabled={!phoneValid || code.length < 4 || busy}
        onClick={async () => {
          setBusy(true);
          try {
            const result = await services.account.bindPhone(digits, code);
            patch({ boundPhone: result.phone, toast: "手机号绑定成功" });
            back();
          } catch {
            patch({ toast: "手机号或验证码不正确" });
          } finally {
            setBusy(false);
          }
        }}
      >
        确认绑定
      </Button>
      <p className="utility-footnote">
        <LockKeyhole />
        手机号仅用于账号安全验证。
      </p>
    </div>
  );
}

function LanguageSettingsPage() {
  const { state, patch, back } = useApp();
  const [language, setLanguage] = useState(state.language);
  return (
    <div className="page utility-page">
      <PageHeader title="语言设置" />
      <SectionTitle title="界面语言" />
      <div className="choice-list">
        <button
          className={language === "zh-CN" ? "selected" : ""}
          onClick={() => setLanguage("zh-CN")}
        >
          <span>
            <b>简体中文</b>
            <small>中国大陆</small>
          </span>
          {language === "zh-CN" && <Check />}
        </button>
        <button
          className={language === "en-US" ? "selected" : ""}
          onClick={() => setLanguage("en-US")}
        >
          <span>
            <b>English</b>
            <small>United States</small>
          </span>
          {language === "en-US" && <Check />}
        </button>
      </div>
      <Button
        className="page-primary"
        onClick={async () => {
          try {
            await services.account.updateLanguage(language);
            patch({ language, toast: "语言偏好已保存" });
            back();
          } catch {
            patch({ toast: "语言设置保存失败，请重试" });
          }
        }}
      >
        保存设置
      </Button>
      <p className="utility-footnote">
        <Languages />
        语言偏好将同步到当前账号的所有设备。
      </p>
    </div>
  );
}

function VersionInfoPage() {
  const { patch, go } = useApp();
  const [version, setVersion] = useState<{
    version: string;
    latest: boolean;
    releaseNotes: string;
  } | null>(null);
  const [checking, setChecking] = useState(false);
  const documents = [
    "用户协议",
    "隐私政策",
    "儿童 / 青少年信息保护规则",
    "个人信息收集清单",
    "第三方信息共享清单",
  ];
  return (
    <div className="page utility-page version-page">
      <PageHeader title="版本信息" />
      <div className="app-mark">
        <div>CPT</div>
        <h1>CPT AI 跑步助手</h1>
        <p>
          Version {version?.version ?? "1.0.0"} ·{" "}
          {version?.latest === false ? "发现新版本" : "当前已是最新版本"}
        </p>
      </div>
      <div className="settings-list">
        <button
          className="settings-list__row"
          onClick={async () => {
            setChecking(true);
            try {
              const result = await services.account.version();
              setVersion(result);
              patch({
                toast: result.latest
                  ? `当前已是最新版本 ${result.version}`
                  : `发现新版本 ${result.version}`,
              });
            } catch {
              patch({ toast: "版本检查失败，请重试" });
            } finally {
              setChecking(false);
            }
          }}
        >
          <RefreshCw />
          <span>
            <b>版本更新</b>
            <small>
              {checking
                ? "检查中…"
                : (version?.releaseNotes ?? "自动检查稳定版本")}
            </small>
          </span>
          <StatusBadge tone="green">最新</StatusBadge>
        </button>
        {documents.map((document) => (
          <button
            key={document}
            className="settings-list__row"
            onClick={() => {
              patch({ selectedLegalDocument: document });
              go("LEGAL-DOCUMENT");
            }}
          >
            <BookOpen />
            <span>
              <b>{document}</b>
            </span>
            <ChevronRight />
          </button>
        ))}
      </div>
    </div>
  );
}

function ContactDeveloperPage() {
  const { patch, back } = useApp();
  const [topic, setTopic] = useState("产品建议");
  const [message, setMessage] = useState("");
  const [submitting, setSubmitting] = useState(false);
  return (
    <div className="page utility-page">
      <PageHeader title="和开发者聊聊" />
      <div className="form-intro form-intro--warm">
        <Headphones />
        <h1>你的每条建议都会被看到</h1>
        <p>告诉我们训练、营养或设备体验中哪里还可以更好。</p>
      </div>
      <label className="field">
        <span>反馈类型</span>
        <select
          value={topic}
          onChange={(event) => setTopic(event.target.value)}
        >
          <option>产品建议</option>
          <option>功能问题</option>
          <option>数据与设备</option>
          <option>账号与安全</option>
        </select>
      </label>
      <label className="field">
        <span>反馈内容</span>
        <textarea
          rows={6}
          maxLength={500}
          placeholder="请尽量描述发生场景和期望结果"
          value={message}
          onChange={(event) => setMessage(event.target.value)}
        />
        <small>{message.length} / 500</small>
      </label>
      <Button
        className="page-primary"
        disabled={message.trim().length < 5 || submitting}
        onClick={async () => {
          setSubmitting(true);
          try {
            await services.account.sendFeedback(topic, message.trim());
            patch({ toast: `${topic}已提交，感谢你的反馈` });
            back();
          } catch {
            patch({ toast: "反馈提交失败，请保留内容后重试" });
          } finally {
            setSubmitting(false);
          }
        }}
      >
        {submitting ? "提交中…" : "提交反馈"}
      </Button>
    </div>
  );
}

function LegalDocumentPage() {
  const { state, patch } = useApp();
  const [document, setDocument] = useState<{
    title: string;
    updatedAt: string;
    sections: Array<{ heading: string; body: string }>;
  } | null>(null);
  const [status, setStatus] = useState<"loading" | "ready" | "error">(
    "loading",
  );
  const load = () => {
    setStatus("loading");
    services.account
      .legalDocument(state.selectedLegalDocument)
      .then((result) => {
        setDocument(result);
        setStatus("ready");
      })
      .catch(() => {
        setStatus("error");
        patch({ toast: "文档加载失败，请重试" });
      });
  };
  useEffect(load, [state.selectedLegalDocument]);
  return (
    <div className="page utility-page">
      <PageHeader title={state.selectedLegalDocument} />
      {status === "loading" && (
        <Card className="incomplete-chat-card" role="status">
          <LoaderCircle className="spin" />
          <p>正在加载最新文本…</p>
        </Card>
      )}
      {status === "error" && (
        <EmptyState
          title="文档暂时无法加载"
          detail="请检查网络连接后重新获取。"
          action={<Button onClick={load}>重新加载</Button>}
        />
      )}
      {status === "ready" && document && (
        <>
          <p className="legal-updated">更新日期：{document.updatedAt}</p>
          <article className="legal-document">
            {document.sections.map((section) => (
              <section key={section.heading}>
                <h2>{section.heading}</h2>
                <p>{section.body}</p>
              </section>
            ))}
          </article>
        </>
      )}
    </div>
  );
}

function AboutCptPage() {
  return (
    <div className="page utility-page about-cpt-page">
      <PageHeader title="关于 CPT" />
      <div className="app-mark app-mark--about">
        <div>CPT</div>
        <h1>让每一次训练更有依据</h1>
        <p>
          CPT AI
          跑步助手结合训练计划、恢复状态、设备数据与运动营养，帮助跑者把建议转化为可执行行动。
        </p>
      </div>
      <Card className="about-principles">
        <div>
          <ShieldCheck />
          <span>
            <b>科学但不越界</b>
            <small>提供训练与营养辅助，不替代医疗诊断。</small>
          </span>
        </div>
        <div>
          <Database />
          <span>
            <b>数据由你掌控</b>
            <small>清晰管理来源、授权与账号生命周期。</small>
          </span>
        </div>
        <div>
          <Sparkles />
          <span>
            <b>持续贴近跑者</b>
            <small>用反馈不断改善计划和补给体验。</small>
          </span>
        </div>
      </Card>
      <p className="utility-footnote">康比特 · CPT AI 跑步助手</p>
    </div>
  );
}

function ReportPage() {
  const { state, report, patch, go } = useApp();
  const [busy, setBusy] = useState(false);
  const [reportError, setReportError] = useState("");
  const reportInputRef = useRef<HTMLInputElement>(null);
  const advance = async (selectedFileName = state.report.fileName) => {
    setBusy(true);
    setReportError("");
    try {
      if (state.report.status === "not_uploaded") {
        if (!selectedFileName) {
          reportInputRef.current?.click();
          return;
        }
        report({ status: "uploading" });
        const fileName = selectedFileName;
        const uploaded = await services.biomarker.upload(fileName);
        report({ status: "parsing" });
        const parsed = await services.biomarker.parse(uploaded.reportId);
        report({
          status: "awaiting_confirmation",
          reportId: uploaded.reportId,
          fileName,
          source: fileName,
          uploadedAt: new Date().toISOString(),
          fields: parsed.fields,
        });
      } else if (state.report.status === "awaiting_confirmation") {
        report({ status: "confirmed", consent: true });
        await services.biomarker.analyze(state.report.reportId);
        report({ status: "insight_ready" });
        patch({
          conversation: [
            ...state.conversation,
            {
              id: crypto.randomUUID(),
              role: "agent",
              text: "你上传并确认的报告已完成运动范围分析。",
              time: "刚刚",
              card: "biomarker",
            },
          ],
        });
        go("HOME-01");
      }
    } catch {
      report({
        status:
          state.report.status === "awaiting_confirmation"
            ? "awaiting_confirmation"
            : "not_uploaded",
      });
      setReportError(
        state.report.status === "awaiting_confirmation"
          ? "报告分析接口暂时无法访问，已保留确认字段和授权选择。"
          : "报告上传或解析失败，未生成任何洞察；请重新上传或检查文件清晰度。",
      );
    } finally {
      setBusy(false);
    }
  };
  return (
    <div className="page">
      <PageHeader title="健康报告" />
      {reportError && (
        <Card className="error-inline" role="alert">
          <AlertTriangle />
          <div>
            <b>报告处理失败</b>
            <p>{reportError}</p>
          </div>
          <button
            onClick={() => {
              setReportError("");
              void advance();
            }}
          >
            重新上传
          </button>
        </Card>
      )}
      {state.report.status === "not_uploaded" ||
      state.report.status === "uploading" ||
      state.report.status === "parsing" ? (
        <>
          <div className="report-upload">
            <UploadCloud />
            <h2>
              {busy
                ? state.report.status === "parsing"
                  ? "正在解析报告…"
                  : "正在上传…"
                : "上传监测报告"}
            </h2>
            <p>上传前不会生成任何指标分析。支持 PDF 或清晰图片。</p>
            {busy && (
              <div className="progress-bar">
                <span
                  style={{
                    width: state.report.status === "parsing" ? "72%" : "36%",
                  }}
                />
              </div>
            )}
          </div>
          <SafetyBanner>
            报告只用于分析对运动训练、生理状态和运动营养补充的可能影响。
          </SafetyBanner>
          <Button
            className="page-primary"
            data-auth-required="healthReport"
            disabled={busy}
            onClick={() => void advance()}
          >
            {busy ? <LoaderCircle className="spin" /> : <UploadCloud />}{" "}
            选择报告文件
          </Button>
          <input
            ref={reportInputRef}
            className="visually-hidden"
            data-auth-required="healthReport"
            type="file"
            accept="application/pdf,image/png,image/jpeg,image/webp"
            aria-label="选择健康报告文件"
            onChange={(event) => {
              const file = event.target.files?.[0];
              if (!file) return;
              void advance(file.name);
              event.target.value = "";
            }}
          />
        </>
      ) : (
        <>
          <Card className="report-meta">
            <div>
              <span>报告日期</span>
              <b>
                {state.report.uploadedAt
                  ? new Date(state.report.uploadedAt).toLocaleDateString(
                      "zh-CN",
                    )
                  : "—"}
              </b>
            </div>
            <div>
              <span>报告来源</span>
              <b>
                {state.report.source || state.report.fileName || "用户上传"}
              </b>
            </div>
            <StatusBadge
              tone={
                state.report.status === "insight_ready" ? "green" : "orange"
              }
            >
              {state.report.status === "insight_ready"
                ? "已生成洞察"
                : "待用户确认"}
            </StatusBadge>
          </Card>
          <SectionTitle title="解析结果" />
          <div className="report-table">
            <div>
              <b>指标</b>
              <b>值</b>
              <b>参考区间</b>
            </div>
            {state.report.fields.map((field) => (
              <div key={field.name}>
                <span>{field.name}</span>
                <strong>{field.value}</strong>
                <span>{field.reference}</span>
              </div>
            ))}
          </div>
          <label className="consent-box">
            <input
              data-auth-required="healthReport"
              type="checkbox"
              checked={state.report.consent}
              onChange={(e) => report({ consent: e.target.checked })}
            />
            <span>仅授权用于运动训练、生理状态与运动营养范围分析</span>
          </label>
          <Button
            className="page-primary"
            data-auth-required="healthReport"
            disabled={
              busy ||
              (!state.report.consent && state.report.status !== "insight_ready")
            }
            onClick={() => void advance()}
          >
            {busy
              ? "Agent 分析中…"
              : state.report.status === "insight_ready"
                ? "已完成分析"
                : "确认并分析"}
          </Button>
          <Button
            variant="danger"
            className="page-primary"
            data-auth-required="healthReport"
            onClick={async () => {
              try {
                if (state.report.reportId)
                  await services.biomarker.revoke(state.report.reportId);
                report({
                  status: "not_uploaded",
                  consent: false,
                  reportId: "",
                  fileName: "",
                  source: "",
                  uploadedAt: "",
                  fields: [],
                });
                patch({ toast: "报告与分析授权已删除" });
                go("PROFILE-01");
              } catch {
                patch({ toast: "报告删除失败，请重试" });
              }
            }}
          >
            <Trash2 size={16} /> 删除报告并撤回授权
          </Button>
        </>
      )}
    </div>
  );
}

function AiDetail() {
  const { state, patch, sendMessage, sendAnalysisToHome, back } = useApp();
  const ctx = state.analysisContext ?? {
    title: "训练建议",
    sourceRoute: "HOME-01" as RouteId,
    summary: "结合当前上下文生成详细解释。",
  };
  const [draft, setDraft] = useState("");
  const conclusion =
    "当前状态支持继续执行计划，但需要把强度控制在目标区间，不追求额外加速。";
  const basis =
    "近 7 日训练负荷、昨夜睡眠、今日天气、主观反馈与当前计划版本。";
  const action =
    "前 2 公里放慢；按补给时间轴完成补液与碳水；训练后记录 RPE 与酸痛。";
  const handoffToHome = () =>
    sendAnalysisToHome(
      `${ctx.title}详细总结`,
      ctx.sourceRoute,
      `${ctx.summary}\n\n结论：${conclusion}\n依据：${basis}\n可执行建议：${action}`,
    );
  return (
    <div className="page ai-detail-page">
      <PageHeader title="跑步 AI 助手建议" />
      <div className="analysis-context">
        <span>正在查看</span>
        <b>{ctx.title}</b>
        <small>来源：{ctx.sourceRoute} · 数据刚刚更新</small>
      </div>
      {state.safetyBlocked ? (
        <SafetyBanner>
          建议暂停训练并寻求医疗建议。本助手已停止普通训练调整与营养品推荐。
        </SafetyBanner>
      ) : (
        <>
          <div className="ai-long-message">
            <CoachAvatar />
            <div>
              <p>{ctx.summary}</p>
              <Card>
                <b>结论</b>
                <p>{conclusion}</p>
              </Card>
              <Card>
                <b>依据</b>
                <p>{basis}</p>
              </Card>
              <Card>
                <b>可执行建议</b>
                <p>{action}</p>
              </Card>
            </div>
          </div>
          <div className="quick-replies">
            <button onClick={() => setDraft("这个建议的置信度是多少？")}>
              建议置信度？
            </button>
            <button onClick={() => setDraft("如果我今天时间不够怎么办？")}>
              时间不够怎么办？
            </button>
          </div>
        </>
      )}
      <div className="ai-detail-actions">
        <Button variant="secondary" onClick={back}>
          返回原分析
        </Button>
        <Button onClick={handoffToHome}>
          去首页查看并追问
        </Button>
      </div>
      <Composer
        value={draft}
        onChange={setDraft}
        onSend={async () => {
          await sendMessage(draft);
          setDraft("");
          patch({ toast: "回答已同步到首页会话历史" });
        }}
      />
    </div>
  );
}

function HistoryPage() {
  const { state, clearConversationHistory, go } = useApp();
  const [confirm, setConfirm] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const dialogRef = useRef<HTMLDivElement>(null);
  const cancelRef = useRef<HTMLButtonElement>(null);
  const closeDialog = () => {
    setConfirm(false);
    window.requestAnimationFrame(() => triggerRef.current?.focus());
  };
  useEffect(() => {
    if (!confirm) return;
    cancelRef.current?.focus();
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape" && state.historyResetStatus !== "processing") {
        event.preventDefault();
        closeDialog();
        return;
      }
      if (event.key !== "Tab" || !dialogRef.current) return;
      const focusable = [
        ...dialogRef.current.querySelectorAll<HTMLElement>(
          'button:not(:disabled), [href], input:not(:disabled), [tabindex]:not([tabindex="-1"])',
        ),
      ];
      if (!focusable.length) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      }
      if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    };
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, [confirm, state.historyResetStatus]);
  const hasHistory =
    state.conversation.length > 0 || state.conversationArchive.length > 0;
  return (
    <div className="page">
      <PageHeader title="历史对话" />
      {hasHistory ? (
        <>
          <div className="history-list">
            {state.conversation.length > 0 && (
              <Card onClick={() => go("HOME-01")}>
                <div>
                  <b>今日训练与恢复</b>
                  <span>今天 · {state.conversation.length} 条消息</span>
                </div>
                <ChevronRight />
              </Card>
            )}
            {[...state.conversationArchive].reverse().map((session) => (
              <Card key={session.id}>
                <div>
                  <b>{session.title}</b>
                  <span>
                    {session.date} · {session.messages.length} 条消息
                  </span>
                </div>
                <ChevronRight />
              </Card>
            ))}
          </div>
          <Button
            ref={triggerRef}
            variant="danger"
            className="page-primary"
            onClick={() => setConfirm(true)}
          >
            <Trash2 size={16} /> 重置全部历史
          </Button>
        </>
      ) : (
        <EmptyState
          title="这里很安静，正好重新开始"
          detail="以往对话已经清空。回到首页，随时可以告诉 AI 教练今天的状态。"
          action={<Button onClick={() => go("HOME-01")}>回首页聊聊</Button>}
        />
      )}
      {confirm && (
        <div
          className="dialog-backdrop"
          onPointerDown={(event) => {
            if (
              event.target === event.currentTarget &&
              state.historyResetStatus !== "processing"
            )
              closeDialog();
          }}
        >
          <div
            ref={dialogRef}
            className="confirm-dialog"
            role="alertdialog"
            aria-modal="true"
            aria-labelledby="history-reset-title"
            aria-describedby="history-reset-description"
            aria-busy={state.historyResetStatus === "processing"}
          >
            <span className="dialog-kicker">不可撤销的操作</span>
            <h2 id="history-reset-title">永久删除全部历史对话？</h2>
            <p id="history-reset-description">
              确认后，全部历史对话将永久删除且无法恢复。训练计划、训练记录和营养记录不会被删除。
            </p>
            {state.historyResetStatus === "error" && (
              <p className="dialog-error" role="alert">
                删除失败，原有对话仍完整保留。请检查网络后重试。
              </p>
            )}
            <div className="bottom-actions">
              <Button
                ref={cancelRef}
                variant="secondary"
                disabled={state.historyResetStatus === "processing"}
                onClick={closeDialog}
              >
                取消
              </Button>
              <Button
                variant="danger"
                disabled={state.historyResetStatus === "processing"}
                onClick={async () => {
                  if (await clearConversationHistory()) closeDialog();
                }}
              >
                {state.historyResetStatus === "processing" ? (
                  <>
                    <LoaderCircle className="spin" /> 删除中
                  </>
                ) : (
                  "确认永久删除"
                )}
              </Button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function AssistantInfo() {
  return (
    <div className="page">
      <PageHeader title="助手说明" />
      <div className="page-intro">
      <h1>我能怎么陪你，也会守住哪些边界</h1>
      </div>
      <Card className="capability-card">
        <Bot />
        <div>
          <b>主要能力</b>
          <p>
          陪你生成与调整训练计划、读懂训练与恢复数据，并把训练补给变成容易执行的行动。
          </p>
        </div>
      </Card>
      <Card className="capability-card">
        <Database />
        <div>
          <b>建议依据</b>
          <p>
            用户确认档案、授权设备数据、天气、历史训练、主观反馈与产品目录。
          </p>
        </div>
      </Card>
      <Card className="capability-card">
        <ShieldAlert />
        <div>
          <b>能力边界</b>
          <p>
            不提供疾病诊断、医疗建议或治疗方案。出现身体不适时只建议暂停训练并寻求医疗建议。
          </p>
        </div>
      </Card>
    </div>
  );
}

function StateLab() {
  const { state, patch, device, go } = useApp();
  const states = [
    {
      title: "网络异常",
      note: "当前无网络，仅显示已保存数据",
      active: state.network === "offline",
      action: () =>
        patch({ network: state.network === "offline" ? "online" : "offline" }),
      icon: Database,
    },
    {
      title: "Agent 失败",
      note: "保留输入并提供重新生成",
      active: state.agentFailure,
      action: () => patch({ agentFailure: !state.agentFailure }),
      icon: Bot,
    },
    {
      title: "数据过期",
      note: "设备超过 72 小时未同步",
      active: state.device.status === "stale",
      action: () =>
        device({
          status: state.device.status === "stale" ? "synced" : "stale",
          lastSyncAt: state.device.status === "stale" ? "刚刚" : "4 天前",
        }),
      icon: RefreshCw,
    },
    {
      title: "权限拒绝",
      note: "说明影响，不伪造 GPS 数据",
      active: !state.device.gpsGranted,
      action: () => device({ gpsGranted: !state.device.gpsGranted }),
      icon: LocateFixed,
    },
    {
      title: "高风险症状",
      note: "中止普通建议并寻求帮助",
      active: state.safetyBlocked,
      action: () => patch({ safetyBlocked: !state.safetyBlocked }),
      icon: ShieldAlert,
    },
  ];
  return (
    <div className="page">
      <PageHeader title="异常与安全状态" />
      <p className="lead">
        打开状态后进入对应页面，可验证专用 UI、恢复动作与明确后果。
      </p>
      {state.network === "offline" && (
        <div className="offline-banner persistent-state" role="status">
          <Database size={16} /> 当前无网络，仅显示已保存数据
        </div>
      )}
      {state.safetyBlocked && (
        <SafetyBanner>
          已阻止普通训练和营养建议。返回首页后无论输入什么，都只显示停止训练并寻求医疗帮助的安全提示。
        </SafetyBanner>
      )}
      <div className="state-list">
        {states.map(({ title, note, active, action, icon: Icon }) => (
          <Card className={`state-row ${active ? "active" : ""}`} key={title}>
            <Icon />
            <div>
              <b>{title}</b>
              <span>{note}</span>
            </div>
            <button
              aria-label={`${title}${active ? "已开启" : "已关闭"}`}
              role="switch"
              aria-checked={active}
              className={`switch ${active ? "on" : ""}`}
              onClick={action}
            >
              <span />
            </button>
          </Card>
        ))}
      </div>
      <Button
        variant="secondary"
        className="page-primary"
        onClick={() => go("PLAN-06")}
      >
        查看同步冲突处理
      </Button>
      <Button className="page-primary" onClick={() => go("HOME-01")}>
        回首页验证状态
      </Button>
    </div>
  );
}

function SummaryRow({
  icon,
  title,
  value,
}: {
  icon: ReactNode;
  title: string;
  value: string;
}) {
  return (
    <div className="summary-row">
      <div>{icon}</div>
      <span>{title}</span>
      <b>{value}</b>
    </div>
  );
}
function StatusRow({
  icon,
  title,
  value,
  ok,
  onClick,
}: {
  icon: ReactNode;
  title: string;
  value: string;
  ok: boolean;
  onClick?: () => void;
}) {
  return (
    <Card className="status-row" onClick={onClick}>
      <div>{icon}</div>
      <span>
        <b>{title}</b>
        <small>{value}</small>
      </span>
      {ok ? (
        <CheckCircle2 className="ok" />
      ) : (
        <AlertTriangle className="warn" />
      )}
      {onClick && <ChevronRight />}
    </Card>
  );
}
function ChoiceField({
  label,
  options,
  value,
  onChange,
}: {
  label: string;
  options: string[];
  value: string;
  onChange: (value: string) => void;
}) {
  return (
    <Card className="choice-field">
      <b>{label}</b>
      <div>
        {options.map((option) => (
          <button
            key={option}
            onClick={() => onChange(option)}
            className={value === option ? "selected" : ""}
          >
            {option}
          </button>
        ))}
      </div>
    </Card>
  );
}
function MetricChart({
  title,
  unit,
  color,
}: {
  title: string;
  unit: string;
  color: "blue" | "red" | "green";
}) {
  return (
    <Card className="metric-chart">
      <div>
        <b>{title}</b>
        <span>近 45 分钟 · {unit}</span>
      </div>
      <svg viewBox="0 0 320 100" role="img" aria-label={`${title}曲线`}>
        <path
          className={`chart-line chart-line--${color}`}
          d="M5 74 C 24 48, 36 58, 52 42 S 86 34, 102 53 S 136 40, 154 26 S 190 36, 208 44 S 242 28, 258 50 S 290 52, 315 22"
        />
        <path className="chart-grid" d="M5 25H315 M5 50H315 M5 75H315" />
      </svg>
    </Card>
  );
}
function LayersIcon() {
  return <Database size={20} />;
}
function formatTime(seconds: number) {
  const m = Math.floor(seconds / 60)
    .toString()
    .padStart(2, "0");
  const s = (seconds % 60).toString().padStart(2, "0");
  return `00:${m}:${s}`;
}
function statusLabel(status: PlanItem["status"]) {
  return (
    {
      draft: "草案",
      proposed: "待确认",
      confirmed: "已确认",
      in_progress: "进行中",
      completed: "已完成",
      adjusted: "已调整",
      skipped: "已跳过",
    } as const
  )[status];
}
