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 {
  AiSummary,
  Button,
  Card,
  Composer,
  DateStrip,
  demoItems,
  DemoDock,
  EmptyState,
  Metric,
  PageHeader,
  PhoneFrame,
  SafetyBanner,
  Score20Slider,
  ScoreSlider,
  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 type {
  AgentDailyRecipePlan,
  AgentMealType,
  FoodEntry,
  MealEntry,
  NutritionIntake,
  NutritionProduct,
  PlanAdjustmentData,
  PlanItem,
  RouteId,
  TrainingAnalysisData,
} from "./types";

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",
  "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>
          当前不会展示与档案不一致的训练计划或营养规划。你可以回到首页直接对 Agent
          说“帮我生成训练计划”，也可以前往“我的”调整画像后生成。
        </p>
        <Button onClick={() => go("HOME-01", "home")}>
          去首页与 Agent 生成
        </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 } = useApp();
  const [authRequest, setAuthRequest] = useState<AuthCapability | null>(null);
  const pendingAuthActionRef = useRef<HTMLElement | null>(null);
  const authTriggerRef = useRef<HTMLElement | null>(null);
  const authGateEnabled = !navigator.webdriver || new URLSearchParams(window.location.search).has("authGate");
  const activeDemo = demoItems[state.demo - 1];
  const showBottomNav =
    state.profileComplete && !noBottomRoutes.includes(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;
  return (
    <div className="prototype-stage">
      <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}
        >
          <RouteView route={state.route} />
        </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>四个一级入口共享统一状态；Demo 可重复操作并一键重置。</p>
        )}
      </aside>
    </div>
  );
}

function RouteView({ route }: { route: RouteId }) {
  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 />;
    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 <PlanAdjust />;
    case "PLAN-06":
      return <PlanConflict />;
    case "TRAIN-READY":
      return <TrainingReady />;
    case "TRAIN-LIVE":
      return <TrainingLive />;
    case "TRAIN-SUMMARY":
      return <TrainingSummary />;
    case "TRAIN-FEEDBACK":
      return <TrainingFeedback />;
    case "TRAIN-ANALYSIS":
      return <TrainingAnalysis />;
    case "NUT-01":
      return <NutritionHome />;
    case "NUT-02":
      return <NutritionPlan />;
    case "NUT-03":
      return <NutritionDuring />;
    case "NUT-04":
      return <HydrationLog />;
    case "NUT-05":
      return <MealDiary />;
    case "NUT-06":
      return <FoodSearch />;
    case "NUT-07":
      return <RecipePage />;
    case "NUT-08":
      return <NutritionAlternative />;
    case "NUT-09":
      return <NutritionReminderSettings />;
    case "RECORD-02":
      return <RecordDay />;
    case "RECORD-03":
      return <RecordDetail />;
    case "RECORD-04":
      return <TrendPage />;
    case "RECORD-06":
      return <PeriodComparison />;
    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>
  );
}

function GoalPage() {
  const { state, profile, go } = useApp();
  return (
    <div className="page onboarding">
      <PageHeader title="训练目标" eyebrow="建立跑步档案" />
      <ProgressDots step={1} />
      <h1>
        你想通过跑步
        <br />
        完成什么？
      </h1>
      <p className="lead">目标将决定计划周期、训练强度和补给策略。</p>
      <label className="field">
        <span>训练目标</span>
        <textarea
          value={state.profile.goal}
          onChange={(e) => profile({ goal: e.target.value })}
        />
      </label>
      <div className="field">
        <span>每周训练次数</span>
        <div className="number-stepper">
          <button
            onClick={() =>
              profile({ frequency: Math.max(1, state.profile.frequency - 1) })
            }
          >
            <Minus />
          </button>
          <b>
            {state.profile.frequency}
            <small>次 / 周</small>
          </b>
          <button
            onClick={() =>
              profile({ frequency: Math.min(7, state.profile.frequency + 1) })
            }
          >
            <Plus />
          </button>
        </div>
      </div>
      <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 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;
  return (
    <div className="page onboarding">
      <PageHeader title="跑步能力" eyebrow="建立跑步档案" />
      <ProgressDots step={2} />
      <h1>
        让计划匹配
        <br />
        你现在的水平
      </h1>
      <fieldset className="field profile-radio-field">
        <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">
        <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">
        <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>
      )}
      <label className="field">
        <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>
      <Card className="hint-card">
        <CircleGauge size={20} />
        <p>
          性别、身高和体重会进入 Agent
          档案，用于个体化训练负荷和每公斤补液、补能需求估算；后续可在“我的”中修改。
        </p>
      </Card>
      <Button
        disabled={!bodyValid}
        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 levels = [
    { n: 1, label: "少汗" },
    { n: 2, label: "轻微" },
    { n: 3, label: "明显" },
    { n: 4, label: "大量" },
    { n: 5, label: "湿透" },
  ];
  return (
    <div className="page onboarding">
      <PageHeader title="营养画像" eyebrow="建立跑步档案" />
      <ProgressDots step={3} />
      <h1>
        一次常见训练中
        <br />
        你通常出多少汗？
      </h1>
      <p className="lead">
        选择最接近的衣物与皮肤状态，Agent 会把它用于补液和训练计划评估。
      </p>
      <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 className="field">
        <span>饮食偏好</span>
        <div className="choice-grid choice-grid--three">
          {["均衡饮食", "素食", "低乳糖"].map((item) => (
            <button
              className={state.profile.diet === item ? "selected" : ""}
              onClick={() => profile({ diet: item })}
              key={item}
            >
              {item}
            </button>
          ))}
        </div>
      </div>
      <SafetyBanner>补给建议会先检查过敏、忌口与胃肠耐受。</SafetyBanner>
      <Button
        disabled={state.planGenerationStatus === "loading"}
        className="page-primary"
        onClick={async () => {
          if (editingExistingProfile) {
            if (await generatePlan()) go("PROFILE-01");
          } else {
            patch({
              profileComplete: true,
              planGenerationStatus: "idle",
              toast: "基础档案已完成，可连接设备或稍后在首页生成计划",
            });
            go("DEV-01");
          }
        }}
      >
        {state.planGenerationStatus === "loading" ? (
          <>
            <LoaderCircle className="spin" /> 正在更新训练计划
          </>
        ) : editingExistingProfile ? (
          "保存并生成方案"
        ) : (
          "完成基础档案并连接设备"
        )}
      </Button>
    </div>
  );
}

const providers = [
  { name: "Apple Health", icon: Apple, note: "活动、心率与睡眠" },
  { name: "Garmin", icon: Watch, note: "训练与健康数据" },
  { name: "高驰", icon: Activity, note: "训练负荷与恢复" },
  { name: "华为健康", icon: HeartPulse, note: "运动与健康数据" },
];

function DevicePage() {
  const { state, patch, connectDevice, syncDevice, go, back } = useApp();
  const origin = state.stack[state.stack.length - 1];
  const onboarding = origin === "ONB-03" || !state.profileComplete;
  return (
    <div className="page">
      <PageHeader title="连接我的设备" />
      <div className="page-intro">
        <h1>让建议基于真实数据</h1>
        <p>连接后可同步训练、心率、睡眠和恢复状态。</p>
      </div>
      <div className="provider-list">
        {providers.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"
                  onClick={() => connectDevice(name)}
                  disabled={Boolean(state.device.authorizingProvider)}
                >
                  {authorizing ? (
                    <>
                      <LoaderCircle className="spin" /> 授权中
                    </>
                  ) : (
                    "连接"
                  )}
                </Button>
              )}
            </Card>
          );
        })}
      </div>
      {state.device.connectedProviders.length > 0 && (
        <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>
      )}
      <Button
        className="page-primary"
        onClick={() => (onboarding ? go("PLAN-02") : back())}
      >
        {onboarding ? "继续生成计划" : "完成设备设置"}
      </Button>
      {onboarding && (
        <button
          className="text-action"
          onClick={() => {
            patch({
              profileComplete: true,
              planGenerationStatus: "idle",
              toast: "已跳过设备和计划生成，可在首页与 Agent 对话生成",
            });
            go("HOME-01", "home");
          }}
        >
          暂时跳过，先进入首页
        </button>
      )}
    </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,
  } = 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 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);
    if (latestMessage?.card !== "training_performance") return;
    window.requestAnimationFrame(() =>
      conversationEndRef.current?.scrollIntoView({ block: "end" }),
    );
  }, [dynamicMessages.at(-1)?.id]);
  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>
                    训练目标、身高体重、跑步能力和出汗情况是首次使用必填信息。
                  </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
                    ? "恢复数据已经同步。今天是计划休息日，优先做好恢复。"
                    : "恢复数据已经同步。今天可以按计划训练。"}
                </div>
              </div>
            </div>
            <div className="message-row message-row--agent home-plan-message">
              <CoachAvatar />
              <div className="message-stack">
                <Card className="home-coach-card">
                  <div className="home-coach-card__head">
                    <h2>{isRestDay ? "今日恢复建议" : "今日训练建议"}</h2>
                    <span>{isRestDay ? "计划休息" : "恢复就绪"}</span>
                    <strong>
                      78<small> / 100</small>
                    </strong>
                  </div>
                  <div
                    className="readiness-track"
                    aria-label="恢复准备度 78 分"
                  >
                    <span style={{ width: "78%" }} />
                    <img
                      src="/runner-illustration-unbranded.png"
                      alt="跑者进度"
                    />
                  </div>
                  <div className="home-health-metrics">
                    <div>
                      <span>睡眠</span>
                      <strong>
                        {state.device.sleepHours ?? "--"}
                        <small>
                          {state.device.sleepHours == null ? " 无数据" : " h"}
                        </small>
                      </strong>
                    </div>
                    <div>
                      <span>HRV</span>
                      <strong>
                        {state.device.hrvMs ?? "--"}
                        <small>
                          {state.device.hrvMs == null ? " 无数据" : " ms"}
                        </small>
                      </strong>
                    </div>
                    <div>
                      <span>静息心率</span>
                      <strong>
                        {state.device.restingHeartRate ?? "--"}
                        <small>
                          {state.device.restingHeartRate == null
                            ? " 无数据"
                            : " bpm"}
                        </small>
                      </strong>
                    </div>
                  </div>
                  <button
                    className="home-device-chip"
                    onClick={() => go("DEV-01")}
                  >
                    <img src="/sports-watch.png" alt="运动手表" />
                    <span>
                      {state.device.syncSources[0] || "COROS"} · 07:08 同步
                    </span>
                  </button>
                  {todayPlan && (
                    <>
                      <div className="home-session-card">
                        <div className="home-session-title">
                          <img src="/running-shoe-unbranded.png" alt="无品牌跑鞋" />
                          <h3>{todayPlan.title}</h3>
                        </div>
                        <div className="home-session-row">
                          <MapPin />
                          <span>目标配速</span>
                          <b>{todayPlan.pace}–6′10″/km</b>
                        </div>
                        <div className="home-session-row">
                          <Timer />
                          <span>预计时长</span>
                          <b>{todayPlan.duration}</b>
                        </div>
                      </div>
                      <div className="home-fuel-note">
                        <img src="/electrolyte-bottle.png" alt="训练饮料" />
                        <span>训练前 30 分钟 · 水 300 ml · 碳水 25 g</span>
                      </div>
                      <div className="home-primary-actions home-primary-actions--single">
                        <Button onClick={() => go("TRAIN-HUB", "training")}>
                          前往训练中心 <ChevronRight size={16} />
                        </Button>
                      </div>
                    </>
                  )}
                </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>
                    生成前不会显示旧计划；Agent
                    将使用你的完整画像和已授权数据生成。
                  </p>
                </div>
                <Button
                  disabled={state.planGenerationStatus === "loading"}
                  onClick={() => send("请根据我的完整档案生成训练计划")}
                >
                  {state.planGenerationStatus === "loading" ? (
                    <>
                      <LoaderCircle className="spin" /> Agent 生成中
                    </>
                  ) : (
                    "让 Agent 生成计划"
                  )}
                </Button>
                <Button
                  variant="secondary"
                  onClick={() => go("PROFILE-01", "profile")}
                >
                  先调整我的画像
                </Button>
              </Card>
            </div>
          </div>
        )}
        {!showInitialConversation && dynamicMessages.length === 0 && (
          <EmptyState
            title="还没有对话记录"
            detail="历史对话已清空。你可以输入问题，或使用下方由 Agent 根据最新数据生成的快捷问题开始新对话。"
          />
        )}
        {dynamicMessages.map((message) => (
          <ConversationMessage key={message.id} message={message} />
        ))}
        <div ref={conversationEndRef} className="conversation-end-anchor" />
        {(state.agentFailure || state.agentRetrying) && (
          <Card className="error-inline">
            <AlertTriangle />
            <div>
              <b>
                {state.agentRetrying
                  ? "正在重新请求 Agent"
                  : "Agent 暂时无法访问"}
              </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>
                  Agent 结合最新档案与数据生成
                  {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 ConversationMessage({
  message,
}: {
  message: ReturnType<typeof useApp>["state"]["conversation"][number];
}) {
  const { state, confirmPlan, go } = useApp();
  const plan = state.planItems[0];
  const performanceActivity =
    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 performanceMatched = performanceAnalysis?.rows.filter(
    (row) => row.status === "met",
  ).length ?? 0;
  return (
    <div className={`message-row message-row--${message.role}`}>
      {message.role === "agent" ? (
        <CoachAvatar />
      ) : (
        <div className="message-avatar">我</div>
      )}
      <div className="message-stack">
        <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={() => {
                  confirmPlan(plan.id);
                  go("TRAIN-READY");
                }}
              >
                接受并准备训练
              </Button>
              <Button variant="secondary" onClick={() => go("PLAN-05")}>
                调整计划
              </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-02")}>
              查看完整方案
            </Button>
          </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>补充 RPE、口渴、酸痛和胃肠感受后，Agent 会结合客观实绩与主观体验给出更完整的建议。</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>
            <div className="training-performance-metrics">
              <div>
                <span>距离</span>
                <strong>{performanceActivity.distance.toFixed(2)}<small> km</small></strong>
              </div>
              <div>
                <span>平均配速</span>
                <strong>{performanceActivity.pace}<small>/km</small></strong>
              </div>
              <div>
                <span>平均心率</span>
                <strong>{performanceActivity.heartRate}<small> bpm</small></strong>
              </div>
            </div>
            <div className="training-performance-match">
              <div>
                <span>计划执行</span>
                <b>{performancePlan?.title ?? "本次训练"}</b>
              </div>
              <strong>{performanceMatched}<small> / {performanceAnalysis?.rows.length ?? 5} 项达标</small></strong>
            </div>
            <p className="training-performance-summary">
              {performanceAnalysis?.summary ?? "训练主体已完成，补充主观感受后可获得更完整的恢复与后续训练建议。"}
            </p>
            <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>
          Agent 将结合最新档案、设备摘要与恢复状态实时生成，不使用固定模板内容。
        </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>计划由 Agent 生成</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" /> Agent 生成中
          </>
        ) : (
          <>
            <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="请返回并让 Agent 根据最新档案生成计划草案。"
          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 TrainingHub() {
  const { state, patch, generatePlan, go, sendAnalysisToHome, confirmPlan, syncDevice } =
    useApp();
  const [scheduleScope, setScheduleScope] = useState<"week" | "cycle">("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 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 weekLoad = weekItems.reduce((sum, item) => sum + item.load, 0);
  const weekDistance = weekItems.reduce((sum, item) => sum + item.distance, 0);
  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.planGenerationStatus === "ready" ? (
      <Card className="training-program-card">
        <div className="training-program-copy">
          <img src="/runner-illustration-unbranded.png" alt="跑者" />
          <div>
            <h2>
              {state.planProgram.title} · {state.planProgram.phase}
            </h2>
            <span>
              第 {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: `${completionPercent}%` }} />
        </div>
        <p>
          本周 {weekDistance.toFixed(1)} km · 计划负荷 {weekLoad} · 已完成{" "}
          {completionPercent}%
        </p>
        <small className="agent-plan-stamp">
          <Bot size={12} /> Agent 根据最新档案生成于 {state.planGeneratedAt}
        </small>
        <button
          className="cycle-overview-entry"
          onClick={() => setScheduleScope("cycle")}
        >
          查看 {state.planProgram.totalWeeks} 周完整周期 <ChevronRight />
        </button>
      </Card>
    ) : state.planGenerationStatus === "loading" ? (
      <Card
        className="training-program-loading"
        role="status"
        aria-live="polite"
      >
        <LoaderCircle className="spin" />
        <div>
          <b>正在按最新档案更新本周计划</b>
          <p>Agent 返回前不会继续展示旧计划。</p>
        </div>
      </Card>
    ) : (
      <EmptyState
        title="档案已更新，计划需要重新生成"
        detail="训练中心不会继续显示与当前档案不一致的旧计划。"
        action={
          <Button onClick={() => go("PLAN-02")}>让 Agent 重新生成</Button>
        }
      />
    );
  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="训练中心" />;
  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" && (
        <>
          <DateStrip
            selected={state.selectedDate}
            onSelect={(selectedDate) => patch({ selectedDate })}
            planItems={state.planItems}
            activityDates={records.map((record) => record.date)}
          />
          {state.selectedDate !== todayIso && (
            <button
              className="training-return-today"
              onClick={() => {
                patch({ selectedDate: todayIso });
                setScheduleScope("week");
              }}
            >
              <LocateFixed />
              <span>回到今天</span>
              <small>{new Date(`${todayIso}T12:00:00`).toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })}</small>
            </button>
          )}
        </>
      )}
      {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>
          </div>
          {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" ? null : (
                <>
                  <SectionTitle
                    title={`${new Date(state.selectedDate + "T12:00:00").toLocaleDateString("zh-CN", { month: "long", day: "numeric", weekday: "short" })} · 当日安排`}
                  />
                  {selectedPlan ? (
                    <Card className="training-today-card">
                      <button
                        className="plan-card training-today-link"
                        onClick={() => {
                          patch({ selectedDate: selectedPlan.date });
                          go("PLAN-04");
                        }}
                      >
                        <div className="training-today-visual">
                          <img
                            src={trainingTypeIcon(selectedPlan).src}
                            alt={trainingTypeIcon(selectedPlan).alt}
                          />
                          <div>
                            <h2>{selectedPlan.title}</h2>
                          </div>
                          <StatusBadge
                            tone={
                              selectedPlan.status === "adjusted"
                                ? "blue"
                                : selectedPlan.status === "completed"
                                  ? "green"
                                  : "gray"
                            }
                          >
                            {statusLabel(selectedPlan.status)}
                          </StatusBadge>
                        </div>
                        <div className="training-today-metrics">
                          <div>
                            <span>距离</span>
                            <strong>
                              {selectedPlan.distance.toFixed(1)}
                              <small> km</small>
                            </strong>
                          </div>
                          <div className="training-pace-metric">
                            <span>配速</span>
                            <strong>
                              <span className="training-pace-range">
                                {selectedPlan.pace}
                              </span>
                              <small>/km</small>
                            </strong>
                          </div>
                          <div>
                            <span>预计用时</span>
                            <strong>
                              {selectedPlan.duration.replace("分钟", "")}
                              <small> 分钟</small>
                            </strong>
                          </div>
                        </div>
                      </button>
                      <div className="training-ai-note">
                        <CoachAvatar />
                        <b>CPT 总结</b>
                        <p>恢复状态支持按计划完成，前 2 公里放慢。</p>
                        <button
                          aria-label="详细总结"
                          onClick={() =>
                            sendAnalysisToHome(
                              `${selectedPlan.title}训练解读`,
                              "PLAN-04",
                              "这次训练用于提升阈值配速，并控制在可恢复负荷内。",
                            )
                          }
                        >
                          去看看 <ChevronRight />
                        </button>
                      </div>
                      <div className="home-primary-actions">
                        <Button
                          onClick={() => {
                            confirmPlan(selectedPlan.id);
                            go("TRAIN-READY");
                          }}
                        >
                          开始训练
                        </Button>
                        <Button
                          variant="secondary"
                          onClick={() => go("PLAN-05")}
                        >
                          调整计划
                        </Button>
                      </div>
                    </Card>
                  ) : (
                    <Card className="training-rest-day">
                      <Moon />
                      <div>
                        <b>今日为恢复日</b>
                        <p>
                          当前日期没有训练任务。可选择本周其他训练日查看具体安排。
                        </p>
                      </div>
                    </Card>
                  )}
                  <SectionTitle
                    title={`第 ${selectedWeek} 周安排 · ${weekItems.length} 次`}
                  />
                  <Card className="training-week-card">
                    {weekItems.map((item) => {
                      const icon = trainingTypeIcon(item);
                      return (
                        <button
                          className={
                            item.id === selectedPlan?.id ? "is-current" : ""
                          }
                          key={item.id}
                          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}
                            </b>
                            <small>
                              {item.distance} km · {item.pace}/km
                            </small>
                          </span>
                          <span
                            className={`week-state ${item.id === selectedPlan?.id ? "week-state--today" : completedIds.has(item.id) ? "week-state--rest" : "week-state--planned"}`}
                          >
                            <i aria-hidden="true" />
                            {item.id === selectedPlan?.id
                              ? "所选"
                              : completedIds.has(item.id)
                                ? "已完成"
                                : statusLabel(item.status)}
                          </span>
                          <ChevronRight />
                        </button>
                      );
                    })}
                  </Card>
                </>
              )}
            </>
          )}
        </>
      ) : (
        <div className="training-history-view">
          {selectedPlan && (
            <Card
              className="record-selected-plan"
              onClick={() => go("PLAN-04")}
            >
              <div className="record-selected-plan__icon">
                <CalendarCheck />
              </div>
              <div>
                <span>
                  {new Date(`${selectedPlan.date}T12:00:00`).toLocaleDateString(
                    "zh-CN",
                    { month: "long", day: "numeric", weekday: "short" },
                  )}
                  · 当前计划
                </span>
                <b>{selectedPlan.title}</b>
                <small>
                  {selectedPlan.distance} km · {selectedPlan.pace}/km · {selectedPlan.duration}
                </small>
              </div>
              <StatusBadge tone={selectedPlan.changed ? "blue" : "gray"}>
                {selectedPlan.changed ? `V${selectedPlan.version} 已调整` : statusLabel(selectedPlan.status)}
              </StatusBadge>
              <ChevronRight />
            </Card>
          )}
          <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>
            <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={() => {
                      patch({ selectedDate: date });
                      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>
          </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 });
                      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>}
            />
          )}
          <SectionTitle title="本月计划变更" />
          <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 })}
                >
                  <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>
              ))}
            {!state.planItems.some(
              (item) => item.date.startsWith(recordMonth) && item.changed,
            ) && (
              <p>这个月暂无计划变更。设备实绩会继续独立保留。</p>
            )}
          </Card>
          <Card className="comparison-entry" onClick={() => go("RECORD-06")}>
            <TrendingUp />
            <div>
              <b>周期水平对比</b>
              <span>完整周期能力</span>
            </div>
            <ChevronRight />
          </Card>
        </div>
      )}
    </div>
  );
}

function TrainingCycleOverview({
  items,
  totalWeeks,
  selectedDate,
  completedIds,
  onSelect,
}: {
  items: PlanItem[];
  totalWeeks: number;
  selectedDate: string;
  completedIds: Set<string>;
  onSelect: (date: string) => void;
}) {
  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>
      {Array.from({ length: totalWeeks }, (_, index) => {
        const week = index + 1;
        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>
        );
      })}
    </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, confirmPlan, go, sendAnalysisToHome } = useApp();
  const plan =
    state.planItems.find((p) => p.date === state.selectedDate) ??
    state.planItems[0];
  return (
    <div className="page">
      <PageHeader title="训练详情" />
      <div className="plan-detail-hero">
        <span>
          {new Date(plan.date).toLocaleDateString("zh-CN", {
            month: "long",
            day: "numeric",
            weekday: "long",
          })}
        </span>
        <h1>{plan.title}</h1>
        <div>
          <Metric value={plan.distance} unit="km" label="距离" />
          <Metric value={plan.pace} unit="/km" label="配速" />
          <Metric value={plan.load} label="负荷" />
        </div>
      </div>
      <SectionTitle title="训练结构" />{" "}
      <Card className="structure-list">
        {plan.structure.map((item, index) => (
          <div key={item}>
            <span>{index + 1}</span>
            <p>{item}</p>
          </div>
        ))}
      </Card>
      <Card className="gear-row">
        <Footprints />
        <div>
          <b>准备提示</b>
          <p>轻量跑鞋 · 心率带 · 训练后放松 8 分钟</p>
        </div>
      </Card>
      <AiSummary
        summary="本次训练用于提高阈值耐力，关键是稳定而非冲刺。"
        onClick={() =>
          sendAnalysisToHome(
            `${plan.title}详细解读`,
            "PLAN-04",
            "基于近期训练与目标赛事，本次安排聚焦阈值耐力和节奏控制。",
          )
        }
      />
      <div className="bottom-actions">
        <Button variant="secondary" onClick={() => go("PLAN-05")}>
          替代 / 顺延
        </Button>
        <Button
          onClick={() => {
            confirmPlan(plan.id);
            go("TRAIN-READY");
          }}
        >
          确认并准备
        </Button>
      </div>
    </div>
  );
}

function PlanAdjust() {
  const { state, applyAdjustment, go, patch } = useApp();
  const [request, setRequest] = useState(
    "周四临时加班，请把节奏跑顺延并降低连续负荷",
  );
  const [preview, setPreview] = useState<PlanAdjustmentData | null>(null);
  const [previewing, setPreviewing] = useState(false);
  const [applying, setApplying] = useState(false);
  return (
    <div className="page">
      <PageHeader title="AI 调整计划" />
      <Card className="context-card">
        <Bot />
        <div>
          <b>当前计划 V{state.planVersion}</b>
          <p>直接在下方告诉 AI 你的变化；生成草案后仍由你确认是否应用。</p>
        </div>
      </Card>
      <label className="field">
        <span>告诉 AI 你遇到的变化</span>
        <textarea
          value={request}
          onChange={(e) => setRequest(e.target.value)}
        />
      </label>
      {!preview ? (
        <Button
          className="page-primary"
          disabled={previewing || !request.trim()}
          onClick={async () => {
            setPreviewing(true);
            try {
              const result = await services.agent.adjustPlan(
                request,
                state.planItems,
                state.selectedDate,
              );
              setPreview(result.data);
            } catch {
              patch({ toast: "变更预览生成失败，原计划保持不变" });
            } finally {
              setPreviewing(false);
            }
          }}
        >
          {previewing ? <LoaderCircle className="spin" /> : <Sparkles size={17} />}
          {previewing ? "正在生成结构化草案" : "生成变更预览"}
        </Button>
      ) : (
        <div className="proposal">
          <div className="proposal__head">
            <span>变更草案</span>
            <StatusBadge tone="orange">待确认</StatusBadge>
          </div>
          <p>{preview.reason}。本次调整 {preview.changes.length} 个训练日，其他计划保持不变。</p>
          {preview.changes.map((change) => {
            const item = state.planItems.find((entry) => entry.id === change.id)!;
            return (
            <Card key={change.id} className="diff-row plan-adjustment-diff">
              <div>
                <b>{item.title}</b>
                <span>
                  {change.before.date} <ArrowRight /> {change.after.date}
                </span>
              </div>
              <div>
                <del>{change.before.pace}</del>
                <ArrowRight />
                <strong>{change.after.pace}</strong>
              </div>
              <small>
                {change.before.duration} / 负荷 {change.before.load}
                <ArrowRight />
                {change.after.duration} / 负荷 {change.after.load}
              </small>
            </Card>
          )})}
          <div className="proposal__scope">
            <CheckCircle2 /> 仅影响以上日期，其他计划保持不变
          </div>
          <div className="bottom-actions">
            <Button
              variant="secondary"
              onClick={() => go("TRAIN-HUB", "training")}
            >
              保持原计划
            </Button>
            <Button
              disabled={applying}
              onClick={async () => {
                setApplying(true);
                const applied = await applyAdjustment(request, preview);
                setApplying(false);
                if (applied) go("TRAIN-HUB", "training");
              }}
            >
              {applying ? (
                <>
                  <LoaderCircle className="spin" /> 应用中
                </>
              ) : (
                "应用变更"
              )}
            </Button>
          </div>
        </div>
      )}
    </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 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 data-auth-required="trainingRecord" disabled={!canStart || starting} onClick={startTraining}>
          {starting ? (
            <>
              <LoaderCircle className="spin" /> 启动中
            </>
          ) : (
            <>
              <Play size={17} /> 开始训练
            </>
          )}
        </Button>
      </div>
      {!canStart && (
        <p className="blocking-note">未授权 GPS，无法启动正式运动记录。</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,
        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 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="详细总结"
        summary="训练完成稳定；后半程配速放缓与补给时机有关。"
        onClick={async () => {
          if (!activity || !plan) 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 soreness = ["无", "轻微", "中等", "明显", "严重"];
  const gi = ["舒适", "轻微不适", "中等不适", "明显不适"];
  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];
  return (
    <div className="page feedback-page">
      <PageHeader title="训练反馈" />
      <p className="form-note">
        <CheckCircle2 /> 本次训练已结束，反馈会帮助 Agent 调整后续计划。
      </p>
      <Card>
        <Score20Slider
          label="RPE · 主观用力感"
          value={state.feedback.rpe}
          onChange={(rpe) => feedback({ rpe })}
          hint="1–6 很轻松 · 7–12 适中 · 13–16 较难 · 17–20 极难"
        />
      </Card>
      <Card>
        <ScoreSlider
          label="口渴感"
          value={state.feedback.thirst}
          max={10}
          onChange={(thirst) => feedback({ thirst })}
          hint="1–3 不口渴 · 4–6 有些口渴 · 7–8 明显口渴 · 9–10 非常口渴"
        />
      </Card>
      <ChoiceField
        label="酸痛程度"
        options={soreness}
        value={state.feedback.soreness}
        onChange={(soreness) => feedback({ soreness })}
      />
      <ChoiceField
        label="胃肠舒适度"
        options={gi}
        value={state.feedback.gi}
        onChange={(value) => feedback({ gi: value })}
      />
      <Button
        className="page-primary"
        disabled={submitting}
        onClick={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 analysisResult = await services.agent.analyzeTraining(
              activePlan,
              completedActivity,
              state.nutritionPlansByDate[activePlan.date]?.nodes ??
                state.nutritionPlan?.nodes ??
                [],
              state.completedNutritionNodesByDate[activePlan.date] ??
                state.completedNutritionNodes,
              submittedFeedback,
            );
            const completed = state.planItems.map((p) =>
              p.id === activePlan.id
                ? { ...p, status: "completed" as const }
                : p,
            );
            patch({
              planItems: completed,
              selectedDate: activePlan.date,
              trainingView: "record",
              feedback: submittedFeedback,
              trainingAnalysis: analysisResult.data,
              live: { ...state.live, status: "completed" },
              toast: "反馈已提交，并同步到训练中心与分析页",
            });
            go("TRAIN-ANALYSIS");
          } catch {
            patch({ toast: "反馈同步失败，请重试；已保存的训练实绩不会丢失" });
          } finally {
            setSubmitting(false);
          }
        }}
      >
        {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">Agent 正在对齐计划、设备实绩与补给记录…</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 Agent 结论</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="继续问 CPT 教练"
          summary="追问训练表现、分析依据，也可以直接向 AI 反馈本次补给方案的完成情况。"
          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>用一句话告诉 CPT 教练哪些补给完成或遗漏</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 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>Agent 目标 · 记录进度</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" /> Agent 正在同步补给节点
          </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>本页只用于查看方案和设置提醒；训练后的实际完成情况请在营养首页或训练分析中向 CPT 教练反馈。</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}、出汗自评与既往胃肠耐受。`,
          )
        }
      />
      <div className="bottom-actions">
        <Button variant="secondary" onClick={() => go("NUT-09")}>
          设置提醒
        </Button>
        <Button onClick={() => go("NUT-03")}>训练中模式</Button>
      </div>
      {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 recipeRequest = useMemo(
    () => ({
      date: state.selectedDate,
      planTitle: trainingPlan?.title ?? "恢复日营养",
      planVersion: trainingPlan?.version ?? 0,
      targetCalories,
      consumedCalories: recordedIntake.caloriesKcal,
      existingMealNames: dayMeals.map((meal) => meal.name),
      diet: state.profile.diet,
      variantByMeal,
    }),
    [
      state.selectedDate,
      trainingPlan?.id,
      trainingPlan?.version,
      targetCalories,
      recordedIntake.caloriesKcal,
      dayMeals.map((meal) => meal.name).sort().join("|"),
      state.profile.diet,
      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">
          Agent 正在核对今日训练、热量目标与已记录餐次，并补全其余饮食方案…
        </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="早餐、午餐、晚餐和加餐均已有记录，Agent 不会覆盖现有餐次。你可以返回餐次日记继续编辑。"
          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: "nutrition",
              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">Agent 正在校验等效剂量与产品可用性…</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 RecordDetail() {
  const { state, sendAnalysisToHome } = useApp();
  const record =
    state.activities.find((item) => item.date === state.selectedDate) ??
    state.activities.find((item) => item.id === state.lastCompletedActivityId);
  if (!record)
    return (
      <div className="page">
        <PageHeader title="训练详情" />
        <EmptyState
          title="没有可查看的训练实绩"
          detail="完成训练或同步运动设备后，实绩会在这里展示。"
        />
      </div>
    );
  return (
    <div className="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>
      <MetricChart title="配速趋势" unit="min/km" color="blue" />
      <MetricChart title="心率区间" unit="bpm" color="red" />
      <AiSummary
        title="本次跑步详细总结"
        summary="后半程配速波动增加，但整体强度仍在目标区间。"
        onClick={() =>
          sendAnalysisToHome(
            "本次跑步详细总结",
            "RECORD-03",
            `本次完成 ${record.distance.toFixed(2)} km，用时 ${record.duration}，平均配速 ${record.pace}/km，平均心率 ${record.heartRate} bpm，补给完成度 ${record.nutritionCompletion}%。`,
          )
        }
      />
    </div>
  );
}

function TrendPage() {
  const { go, 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>
      <Button
        className="page-primary"
        variant="secondary"
        onClick={() => go("RECORD-06")}
      >
        查看周期水平对比
      </Button>
    </div>
  );
}

function PeriodComparison() {
  const { openAnalysis } = useApp();
  const rows = [
    {
      name: "有氧耐力",
      prev: "48.5",
      now: "54.3",
      change: "+12%",
      tone: "up",
      conf: "高",
    },
    {
      name: "阈值配速",
      prev: "4′58″",
      now: "4′32″",
      change: "+8%",
      tone: "up",
      conf: "高",
    },
    {
      name: "速度耐力",
      prev: "92",
      now: "92",
      change: "持平",
      tone: "flat",
      conf: "中",
    },
    {
      name: "补液达成率",
      prev: "68%",
      now: "86%",
      change: "+18%",
      tone: "up",
      conf: "高",
    },
    {
      name: "电解质执行",
      prev: "76%",
      now: "58%",
      change: "-18%",
      tone: "down",
      conf: "高",
    },
  ];
  return (
    <div className="page">
      <PageHeader title="周期水平对比" />
      <div className="comparison-hero">
        <TrendingUp />
        <div>
          <span>整体水平提升</span>
          <strong>8%</strong>
          <p>当前周期 8 周 vs 上一周期 8 周</p>
        </div>
      </div>
      <div className="capability-table">
        <div className="capability-head">
          <span>能力</span>
          <span>上一周期</span>
          <span>当前周期</span>
          <span>变化</span>
        </div>
        {rows.map((row) => (
          <div className="capability-row" key={row.name}>
            <b>{row.name}</b>
            <span>{row.prev}</span>
            <span>{row.now}</span>
            <strong className={row.tone}>{row.change}</strong>
            <small>证据置信度：{row.conf}</small>
          </div>
        ))}
      </div>
      <Card className="action-note action-note--good">
        <CheckCircle2 />
        <div>
          <b>完成良好：继续按计划保持</b>
          <p>有氧与阈值能力稳步提升。</p>
        </div>
      </Card>
      <Card className="action-note action-note--warn">
        <AlertTriangle />
        <div>
          <b>需要注意：电解质补给下降</b>
          <p>下一周期增加提醒与执行复盘。</p>
        </div>
      </Card>
      <AiSummary
        summary="训练能力整体提升，但电解质执行是下一周期重点。"
        onClick={() =>
          openAnalysis(
            "周期水平深度解读",
            "RECORD-06",
            "有氧与阈值能力提升有较高置信度；电解质执行率下降需要重点加强。",
          )
        }
      />
    </div>
  );
}

function ProfilePage() {
  const { state, go, syncDevice } = 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>
              填写训练目标、性别、身高体重、跑步能力与出汗情况后，才能进入首页和使用
              Agent。
            </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>
              你可以继续修改画像并直接生成，也可以回首页通过对话让 Agent 生成。
            </p>
          </div>
          <Button onClick={() => go("ONB-01")}>调整画像并生成</Button>
          <Button variant="secondary" onClick={() => go("HOME-01", "home")}>
            去首页与 Agent 生成
          </Button>
        </Card>
        <SectionTitle title="暂未生成" />
        <Card className="profile-locked-features">
          <div>
            <CalendarCheck />
            <span>
              <b>训练计划</b>
              <small>等待 Agent 生成</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>
            <TrendingUp /> {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("、")} · 今日 07:09 已同步
            </small>
          </span>
          <ChevronRight />
        </button>
        <button className="profile-device-health" onClick={() => go("DEV-02")}>
          <ShieldCheck />
          <span>健康数据与权限 · 4 项已授权</span>
          <ChevronRight />
        </button>
        <button
          className="profile-device-sync"
          aria-label="同步设备数据"
          data-auth-required="cloudSync"
          onClick={syncDevice}
        >
          数据最新
        </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.gender === "male"
                ? "男"
                : state.profile.gender === "female"
                  ? "女"
                  : "性别未透露"} · 跑龄 {state.profile.experience} · 近阶段周均 {state.profile.weeklyKm} km
            </small>
          </span>
          <ChevronRight />
        </button>
        <button onClick={() => go("ONB-03")}>
          <img
            src="/profile-nutrition-cartoon-v27.png"
            alt="动画风运动营养跑者"
          />
          <span>
            <b>营养画像</b>
            <small>
              出汗
              {
                ["", "少", "轻微", "明显", "大量", "湿透"][state.profile.sweat]
              }{" "}
              · {state.profile.diet}
            </small>
          </span>
          <ChevronRight />
        </button>
      </Card>
      <SectionTitle title="设置与授权" />
      <div className="profile-actions profile-settings-actions">
        <button onClick={() => go("REMIND-01")}>
          <Bell />
          通知与补给提醒
          <ChevronRight />
        </button>
        <button onClick={() => go("DEV-02")}>
          <LockKeyhole />
          权限与隐私
          <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="以往对话已被清空，不会再次显示。返回首页可开始新的对话。"
          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];
}
