import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useReducer,
  type ReactNode,
} from "react";
import { services } from "./services";
import { paceSecondsForStep, resolveTrainingProgress } from "./training-engine";
import type {
  AgentContextSnapshot,
  AppState,
  ConversationMessage,
  PlanAdjustmentData,
  RouteId,
  TrainingGoal,
  TrainingAnalysisData,
} from "./types";

const iso = (offset = 0) => {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
};

const timeNow = () =>
  new Date().toLocaleTimeString("zh-CN", {
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  });
const profileSignature = (profile: AppState["profile"]) =>
  JSON.stringify(profile);

const normalizeTrainingGoal = (goal: unknown): TrainingGoal => {
  const text = String(goal ?? "");
  if (/全程|全马|42/.test(text)) return "全马完赛";
  if (/半程|半马|21/.test(text)) return "半马完赛";
  if (/10\s*公里|10\s*km/i.test(text)) return "10公里";
  return "规律跑发展体能";
};

const completedPlanIds = (state: AppState) =>
  new Set([
    ...state.activities.map((activity) => activity.planItemId),
    ...state.planItems
      .filter((item) => item.status === "completed")
      .map((item) => item.id),
  ]);

const archiveCurrentPlan = (
  state: AppState,
  endedAt: string,
  archiveReason: string,
): AppState["planHistory"][number] => {
  const completedIds = completedPlanIds(state);
  const completedItems = state.planItems.filter((item) => completedIds.has(item.id));
  const completedWeeks = new Set(
    completedItems.map((item) => item.weekIndex ?? 1),
  ).size;
  const planIds = new Set(state.planItems.map((item) => item.id));
  return {
    id: `plan-history-${state.planVersion}-${Date.now()}`,
    title: state.planProgram.title,
    phase: state.planProgram.phase,
    version: state.planVersion,
    completedWeeks,
    totalWeeks: state.planProgram.totalWeeks,
    completedSessions: completedItems.length,
    totalSessions: state.planItems.length,
    actualDistance: Number(
      state.activities
        .filter((activity) => planIds.has(activity.planItemId))
        .reduce((sum, activity) => sum + activity.distance, 0)
        .toFixed(1),
    ),
    plannedDistance: Number(
      state.planItems.reduce((sum, item) => sum + item.distance, 0).toFixed(1),
    ),
    startedAt: state.planProgram.startDate ?? state.planItems[0]?.date ?? endedAt,
    endedAt,
    archiveReason,
  };
};

const weekFeaturePattern = /恢复周|建设周|累积周|适应周|减量周/;

const normalizePlanItemTitle = (
  item: AppState["planItems"][number],
): AppState["planItems"][number] => {
  if (!weekFeaturePattern.test(item.title)) return item;
  const structure = item.structure.join(" ");
  const structureName = item.title.split("·")[0]?.trim() || "训练安排";
  const sessionType =
    structureName.includes("耐力长跑") || /长距离|轻松跑 1\d/.test(structure)
      ? "长距离跑"
      : structureName.includes("乳酸阈值") || structure.includes("阈值")
        ? "节奏跑"
        : structureName.includes("速度间歇") || structure.includes("间歇")
          ? "间歇跑"
          : structureName.includes("主动恢复") || structure.includes("恢复跑")
            ? "恢复跑"
            : "轻松跑";
  return { ...item, title: `${structureName} · ${sessionType}` };
};

const buildDemoCycle = (
  weeklyItems: AppState["planItems"],
  totalWeeks: number,
) =>
  Array.from({ length: totalWeeks }, (_, weekOffset) =>
    weeklyItems.map((item, index) => {
      const date = new Date(`${item.date}T12:00:00`);
      date.setDate(date.getDate() + weekOffset * 7);
      const growth = 1 + Math.min(0.16, weekOffset * 0.025);
      const distance = Number((item.distance * growth).toFixed(1));
      const normalizedItem = normalizePlanItemTitle(item);
      return {
        ...normalizedItem,
        id: `${item.id}-w${weekOffset + 1}`,
        date: date.toISOString().slice(0, 10),
        distance,
        duration: `${Math.max(25, Math.round(Number(normalizedItem.duration.match(/\d+/)?.[0] ?? 45) * growth))} 分钟`,
        load: Math.round(normalizedItem.load * growth),
        status: weekOffset === 0 ? normalizedItem.status : ("confirmed" as const),
        weekIndex: weekOffset + 1,
        changed: false,
      };
    }),
  ).flat();

export const buildAgentContext = (state: AppState): AgentContextSnapshot => ({
  generatedAt: new Date().toISOString(),
  profile: { ...state.profile },
  device: {
    provider: state.device.provider,
    connectedProviders: [...state.device.connectedProviders],
    syncSources: [...state.device.syncSources],
    status: state.device.status,
    lastSyncAt: state.device.lastSyncAt,
    weather: state.device.weather ? { ...state.device.weather } : null,
    sleepHours: state.device.sleepHours,
    hrvMs: state.device.hrvMs,
    restingHeartRate: state.device.restingHeartRate,
  },
  recovery: {
    fatigue: state.recoveryFatigue,
    updatedAt: state.recoveryUpdatedAt,
  },
  plan: state.planItems.map(
    ({ id, date, title, distance, pace, duration, status, version }) => ({
      id,
      date,
      title,
      distance,
      pace,
      duration,
      status,
      version,
    }),
  ),
  nutrition: {
    hydrationMl: state.hydrationMl,
    completion: state.nutritionCompletion,
    completedNodes: [...state.completedNutritionNodes],
  },
});

export const createInitialState = (): AppState => ({
  route: "HOME-01",
  stack: [],
  authMode:
    typeof window !== "undefined" &&
    Boolean(window.localStorage.getItem("running-ai-access-token"))
      ? "authenticated"
      : "guest",
  activeTab: "home",
  demo: 2,
  toast: "",
  language: "zh-CN",
  boundPhone: "",
  selectedLegalDocument: "用户协议",
  profileComplete: true,
  profile: {
    goal: "半马完赛",
    frequency: 3,
    trainingDays: [0, 2, 5],
    experience: "3–5 年",
    weeklyKm: 32,
    gender: "male",
    heightCm: 175,
    weightKg: 68,
    sweat: 3,
    longestRunKm: 16,
    weeklyKmConsistency: "3个月以上",
    sessionDuration: "60–90 分钟",
    injuryInterruption: "否",
    currentPain: "无",
    currentPainAreas: [],
    foodSafetyStatus: "无",
    foodAllergies: [],
    foodSafetyDetail: "",
    runningGiFrequency: "偶尔",
    runningGiSymptoms: ["腹胀"],
    fuelStartDuration: "60–90 分钟",
    fuelProducts: ["能量胶", "运动饮料"],
    carbsPerHour: "30–60 g/h",
    fluidPerHour: "300–500 ml/h",
    electrolyteHabit: "天气热或长距离时补",
    fuelingGiHistory: "有过",
    fuelingGiDetail: "能量胶 2 包；跑后段轻微腹胀",
    fuelingBarriers: ["携带不方便"],
  },
  device: {
    provider: "高驰",
    connectedProviders: ["高驰"],
    syncSources: ["高驰 Pace 3"],
    status: "synced",
    lastSyncAt: "今天 08:32",
    gpsGranted: true,
    notificationGranted: true,
    authorizingProvider: "",
    weather: { temperature: 23, humidity: 62, condition: "多云" },
    sleepHours: 7.2,
    hrvMs: 58,
    restingHeartRate: 52,
  },
  planVersion: 3,
  planItems: buildDemoCycle(
    [
      {
        id: "plan-tempo",
        date: iso(-14),
        title: "有氧基础 · 轻松跑",
        distance: 8,
        pace: "5′50″",
        duration: "48 分钟",
        status: "confirmed",
        load: 54,
        version: 3,
        structure: ["动态热身 8 分钟", "轻松跑 8 km", "拉伸 6 分钟"],
      },
      {
        id: "plan-easy",
        date: iso(-12),
        title: "乳酸阈值 · 节奏跑",
        distance: 10,
        pace: "4′45″",
        duration: "52 分钟",
        status: "confirmed",
        load: 86,
        version: 3,
        structure: ["热身 2 km", "阈值跑 6 km", "放松跑 2 km"],
      },
      {
        id: "plan-long",
        date: iso(-10),
        title: "耐力长跑 · 长距离跑",
        distance: 16,
        pace: "5′30″",
        duration: "90 分钟",
        status: "confirmed",
        load: 108,
        version: 3,
        structure: ["轻松跑 12 km", "稳态跑 3 km", "放松 1 km"],
      },
    ],
    8,
  ),
  planProgram: {
    title: "半程马拉松 · 基础建设周期",
    phase: "基础期",
    currentWeek: 3,
    totalWeeks: 8,
    summary: "每周 3 次 · 根据当前档案生成",
    weeklyLoad: 248,
    basis: [
      "目标：半马完赛",
      "频率：每周 3 次",
      "周跑量：32 km",
      "高驰 Pace 3",
    ],
  },
  planGenerationStatus: "ready",
  planProfileSignature: profileSignature({
    goal: "半马完赛",
    frequency: 3,
    trainingDays: [0, 2, 5],
    experience: "3–5 年",
    weeklyKm: 32,
    gender: "male",
    heightCm: 175,
    weightKg: 68,
    sweat: 3,
    longestRunKm: 16,
    weeklyKmConsistency: "3个月以上",
    sessionDuration: "60–90 分钟",
    injuryInterruption: "否",
    currentPain: "无",
    currentPainAreas: [],
    foodSafetyStatus: "无",
    foodAllergies: [],
    foodSafetyDetail: "",
    runningGiFrequency: "偶尔",
    runningGiSymptoms: ["腹胀"],
    fuelStartDuration: "60–90 分钟",
    fuelProducts: ["能量胶", "运动饮料"],
    carbsPerHour: "30–60 g/h",
    fluidPerHour: "300–500 ml/h",
    electrolyteHabit: "天气热或长距离时补",
    fuelingGiHistory: "有过",
    fuelingGiDetail: "能量胶 2 包；跑后段轻微腹胀",
    fuelingBarriers: ["携带不方便"],
  }),
  planGeneratedAt: "今天 07:08",
  planAgentFlow: {
    stage: "idle",
    sourceRoute: null,
    planId: "",
    request: "",
    preview: null,
  },
  planSyncReceipt: null,
  planRevision: {
    version: 3,
    previousVersion: 2,
    kind: "tuning",
    effectiveDate: iso(0),
    appliedAt: "今天 07:08",
    reason: "根据近期完成情况微调后续训练刺激",
    inheritedCompletedCount: 6,
    previousWeekSessions: 3,
    currentWeekSessions: 3,
  },
  planHistory: [
    {
      id: "history-plan-10k-speed",
      title: "10 公里 · 速度耐力周期",
      phase: "专项期",
      version: 2,
      completedWeeks: 6,
      totalWeeks: 6,
      completedSessions: 17,
      totalSessions: 18,
      actualDistance: 96.5,
      plannedDistance: 101.2,
      startedAt: iso(-70),
      endedAt: iso(-29),
      archiveReason: "阶段目标已完成，切换至半程马拉松目标",
    },
  ],
  activities: [
    ...[
      ["plan-tempo-w1", -14, 8.1, "46:54", "5′47″", 144],
      ["plan-easy-w1", -12, 9.8, "48:12", "4′55″", 153],
      ["plan-long-w1", -10, 15.7, "1:27:18", "5′34″", 147],
      ["plan-tempo-w2", -7, 8.3, "47:02", "5′40″", 145],
      ["plan-easy-w2", -5, 10.1, "49:04", "4′52″", 154],
      ["plan-long-w2", -3, 16.2, "1:29:36", "5′32″", 148],
    ].map(([planItemId, dayOffset, distance, duration, pace, heartRate], index) => ({
      id: `activity-cycle-${index + 1}`,
      planItemId: String(planItemId),
      date: iso(Number(dayOffset)),
      distance: Number(distance),
      duration: String(duration),
      pace: String(pace),
      heartRate: Number(heartRate),
      source: "高驰 Pace 3",
      nutritionCompletion: 85,
    })),
    {
      id: "activity-last",
      planItemId: "plan-last",
      date: iso(-2),
      distance: 6.12,
      duration: "34:28",
      pace: "5′38″",
      heartRate: 142,
      source: "高驰 Pace 3",
      nutritionCompletion: 75,
    },
    {
      id: "activity-history-tempo",
      planItemId: "history-tempo",
      date: iso(-8),
      distance: 8.02,
      duration: "45:36",
      pace: "5′41″",
      heartRate: 148,
      source: "高驰 Pace 3",
      nutritionCompletion: 100,
    },
    {
      id: "activity-history-long",
      planItemId: "history-long",
      date: iso(-16),
      distance: 13.18,
      duration: "1:16:42",
      pace: "5′49″",
      heartRate: 145,
      source: "高驰 Pace 3",
      nutritionCompletion: 80,
    },
    {
      id: "activity-history-previous-month",
      planItemId: "history-easy",
      date: iso(-32),
      distance: 7.36,
      duration: "41:25",
      pace: "5′38″",
      heartRate: 139,
      source: "高驰 Pace 3",
      nutritionCompletion: 90,
    },
  ],
  trainingView: "plan",
  selectedDate: iso(0),
  trainingStartReminder: {
    planId: "",
    displayTime: "",
    scheduledAt: "",
    status: "idle",
  },
  trainingAnalysis: null,
  postTrainingFeedbackByActivity: {},
  conversation: [
    {
      id: "m1",
      role: "agent",
        text: "早上好。昨晚的恢复和今天的安排我都看过了，先陪你把今天怎么练理清楚。",
      time: "07:10",
    },
    {
      id: "m2",
      role: "agent",
        text: "恢复数据更新于今天 08:32，身体给出的信号不错，可以安心按计划训练。",
      time: "07:10",
      card: "recovery",
    },
    {
      id: "m3",
      role: "agent",
        text: "今天的主角是有氧基础训练。先看看身体给出的信号，再决定按原计划走，还是一起调一调。",
      time: "07:11",
      card: "training",
    },
    {
      id: "m4",
      role: "agent",
        text: "补给也替你准备好了，已经考虑天气、出汗和训练时长；出发前看一眼就行。",
      time: "07:12",
      card: "nutrition",
    },
  ],
  homeSessionDate: iso(0),
  conversationArchive: [],
  conversationResetAt: null,
  historyResetStatus: "idle",
  suggestedQuestions: [],
  suggestedQuestionsStatus: "idle",
  suggestedQuestionsUpdatedAt: "",
  composerDraft: "",
  hydrationMl: 1200,
  hydrationByDate: { [iso(0)]: 1200 },
  mealCalories: 1200,
  trainingMealCalories: 0,
  mealEntries: [
    {
      id: "breakfast",
      name: "早餐",
      date: iso(0),
      foods: [
        { id: "oats", name: "燕麦", amount: "50 g", caloriesKcal: 190, hydrationMl: 0, carbohydrateG: 34, proteinG: 7, fatG: 4, source: "user" },
        { id: "egg", name: "鸡蛋", amount: "2 个", caloriesKcal: 140, hydrationMl: 0, carbohydrateG: 1, proteinG: 13, fatG: 10, source: "user" },
        { id: "banana", name: "香蕉", amount: "1 根", caloriesKcal: 105, hydrationMl: 0, carbohydrateG: 27, proteinG: 1, fatG: 0, source: "user" },
      ],
    },
    {
      id: "lunch",
      name: "午餐",
      date: iso(0),
      foods: [
        { id: "rice", name: "米饭", amount: "200 g", caloriesKcal: 232, hydrationMl: 0, carbohydrateG: 52, proteinG: 5, fatG: 1, source: "user" },
        { id: "chicken", name: "鸡胸肉", amount: "150 g", caloriesKcal: 248, hydrationMl: 0, carbohydrateG: 0, proteinG: 46, fatG: 5, source: "user" },
        { id: "broccoli", name: "西兰花", amount: "150 g", caloriesKcal: 51, hydrationMl: 0, carbohydrateG: 10, proteinG: 4, fatG: 1, source: "user" },
      ],
    },
  ],
  selectedMealId: "",
  recoveryFatigue: 6,
  recoveryUpdatedAt: "今天 08:32",
  nutritionCompletion: 55,
  completedNutritionNodes: ["pre-water"],
  completedNutritionNodesByDate: { [iso(0)]: ["pre-water"] },
  nutritionFeedbackPending: {
    active: false,
    sourceRoute: null,
    planDate: "",
    activityId: "",
  },
  selectedFuelProduct: "康比特能量胶",
  nutritionPlan: null,
  nutritionPlansByDate: {},
  nutritionPlanStatus: "idle",
  selectedNutritionNodeId: "",
  nutritionReminderReviewsByDate: {},
  live: {
    sessionId: "",
    status: "ready",
    elapsed: 0,
    distance: 0,
    heartRate: 128,
    energy: 0,
    currentStepIndex: 0,
    stepStartedAt: 0,
    stepStartedDistance: 0,
  },
  reminder: {
    id: "fuel-45",
    nodeId: "during-electrolyte",
    enabled: true,
    triggerSecond: 8,
    triggerLabel: "第 30 分钟",
    type: "补液",
    amount: "电解质饮料 150 ml",
    productName: "康比特电解质饮料",
    status: "scheduled",
    vibrationCount: 0,
  },
  feedback: {
    rpe: 13,
    thirst: 5,
    soreness: "轻微",
    gi: "舒适",
    submitted: false,
  },
  lastCompletedActivityId: "",
  analysisContext: null,
  report: {
    status: "not_uploaded",
    consent: false,
    reportId: "",
    fileName: "",
    source: "",
    uploadedAt: "",
    fields: [],
  },
  network: "online",
  agentFailure: false,
  agentRetrying: false,
  lastFailedPrompt: "",
  safetyBlocked: false,
});

type Action =
  | { type: "NAVIGATE"; route: RouteId; tab?: AppState["activeTab"] }
  | {
      type: "OPEN_PLAN_AGENT";
      sourceRoute: RouteId;
      planId: string;
      planTitle: string;
    }
  | { type: "BACK" }
  | { type: "PATCH"; patch: Partial<AppState> }
  | { type: "PROFILE"; patch: Partial<AppState["profile"]> }
  | {
      type: "PLAN_GENERATED";
      items: AppState["planItems"];
      program: AppState["planProgram"];
      nutritionPlansByDate: AppState["nutritionPlansByDate"];
      signature: string;
      generatedAt: string;
    }
  | { type: "DEVICE"; patch: Partial<AppState["device"]> }
  | { type: "DEVICE_CONNECTED"; provider: string }
  | { type: "DEVICE_SYNCED"; at: string; activities: AppState["activities"] }
  | { type: "MESSAGE"; message: ConversationMessage }
  | { type: "START_DAILY_SESSION"; date: string }
  | { type: "CLEAR_HISTORY"; clearedAt: string }
  | { type: "CONFIRM_PLAN"; id: string; at: string }
  | {
      type: "APPLY_ADJUSTMENT";
      adjustment: PlanAdjustmentData;
      nutritionPlansByDate: AppState["nutritionPlansByDate"];
      at: string;
      effectiveDate: string;
    }
  | { type: "TICK" }
  | { type: "REMINDER_TRIGGER" }
  | { type: "REMINDER_ACTION"; status: "acknowledged" | "delayed" | "skipped" }
  | { type: "FEEDBACK"; patch: Partial<AppState["feedback"]> }
  | { type: "REPORT"; patch: Partial<AppState["report"]> }
  | { type: "RESET" };

const tabForRoute = (route: RouteId): AppState["activeTab"] | undefined => {
  if (route === "HOME-01") return "home";
  if (route === "TRAIN-HUB") return "training";
  if (route === "PROFILE-01") return "profile";
  return undefined;
};

function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "NAVIGATE": {
      const tab = action.tab ?? tabForRoute(action.route) ?? state.activeTab;
      const push =
        action.route !== state.route &&
        !["HOME-01", "TRAIN-HUB", "PROFILE-01"].includes(
          action.route,
        );
      return {
        ...state,
        route: action.route,
        activeTab: tab,
        stack: push ? [...state.stack, state.route] : state.stack,
      };
    }
    case "OPEN_PLAN_AGENT": {
      if (
        state.route === "HOME-01" &&
        state.planAgentFlow.planId === action.planId &&
        !["idle", "synced"].includes(state.planAgentFlow.stage)
      )
        return state;
      const draft = `请帮我把「${action.planTitle}」顺延一天，并重新平衡后续训练负荷`;
      return {
        ...state,
        route: "HOME-01",
        activeTab: "home",
        composerDraft: draft,
        planAgentFlow: {
          stage: "collecting",
          sourceRoute: action.sourceRoute,
          planId: action.planId,
          request: "",
          preview: null,
        },
        conversation: [
          ...state.conversation,
          {
            id: crypto.randomUUID(),
            role: "user",
            text: `调整「${action.planTitle}」：替代或顺延`,
            time: timeNow(),
          },
          {
            id: crypto.randomUUID(),
            role: "agent",
            text: "已带入当前训练与整个周期。告诉我你希望如何替代或顺延，我会先生成影响范围清晰的草案；你确认后才会同步到训练界面。",
            time: timeNow(),
            card: "proposal",
            sources: ["当前训练计划", "周期负荷", "恢复状态"],
            contextVersion: `plan-agent-${action.planId}`,
          },
        ],
      };
    }
    case "BACK": {
      const stack = [...state.stack];
      const route =
        stack.pop() ??
        (
          {
            home: "HOME-01",
            training: "TRAIN-HUB",
            profile: "PROFILE-01",
          } as const
        )[state.activeTab];
      return { ...state, route, stack };
    }
    case "PATCH":
      return { ...state, ...action.patch };
    case "PROFILE": {
      const profile = { ...state.profile, ...action.patch };
      const unchanged =
        profileSignature(profile) === state.planProfileSignature;
      return {
        ...state,
        profile,
        planGenerationStatus: unchanged ? state.planGenerationStatus : "idle",
      };
    }
    case "PLAN_GENERATED": {
      const replacesCurrentPlan =
        state.planGenerationStatus !== "loading" ||
        (Boolean(state.planProfileSignature) &&
          action.signature !== state.planProfileSignature);
      const archivedPlan = replacesCurrentPlan
        ? archiveCurrentPlan(
            state,
            iso(0),
            "个人档案或训练目标更新，后续训练启用新周期",
          )
        : null;
      const nextVersion = action.items[0]?.version ?? state.planVersion + 1;
      return {
        ...state,
        planItems: action.items.map(normalizePlanItemTitle),
        planProgram: action.program,
        planVersion: nextVersion,
        selectedDate: action.items[0]?.date ?? state.selectedDate,
        planGenerationStatus: "ready",
        planProfileSignature: action.signature,
        planGeneratedAt: action.generatedAt,
        nutritionPlan:
          action.nutritionPlansByDate[action.items[0]?.date ?? ""] ?? null,
        nutritionPlansByDate: action.nutritionPlansByDate,
        nutritionPlanStatus: "ready",
        completedNutritionNodes: [],
        completedNutritionNodesByDate: {},
        planRevision: replacesCurrentPlan
          ? {
              version: nextVersion,
              previousVersion: state.planVersion,
              kind: "new_cycle",
              effectiveDate: iso(0),
              appliedAt: action.generatedAt,
              reason: "个人档案或训练目标更新，已启用新的训练周期",
              inheritedCompletedCount: 0,
              previousWeekSessions: state.planItems.filter(
                (item) =>
                  (item.weekIndex ?? 1) === state.planProgram.currentWeek,
              ).length,
              currentWeekSessions: action.items.filter(
                (item) => (item.weekIndex ?? 1) === action.program.currentWeek,
              ).length,
            }
          : null,
        planHistory: archivedPlan
          ? [archivedPlan, ...state.planHistory]
          : state.planHistory,
        toast: "Agent 已生成训练计划与配套营养规划",
      };
    }
    case "DEVICE":
      return { ...state, device: { ...state.device, ...action.patch } };
    case "DEVICE_CONNECTED": {
      const connectedProviders = Array.from(
        new Set([...state.device.connectedProviders, action.provider]),
      );
      return {
        ...state,
        device: {
          ...state.device,
          provider: action.provider,
          connectedProviders,
          status: "connected",
          authorizingProvider: "",
          lastSyncAt: "尚未同步",
          weather: null,
          sleepHours: null,
          hrvMs: null,
          restingHeartRate: null,
        },
        toast: `${action.provider} 已连接，可继续连接其他设备或立即同步`,
      };
    }
    case "DEVICE_SYNCED": {
      const syncSources = state.device.connectedProviders.map((provider) =>
        provider === "高驰" ? "高驰 Pace 3" : provider,
      );
      return {
        ...state,
        activities: [
          ...state.activities.filter(
            (existing) =>
              !action.activities.some(
                (incoming) =>
                  incoming.id === existing.id ||
                  (incoming.planItemId === existing.planItemId &&
                    incoming.source === existing.source),
              ),
          ),
          ...action.activities,
        ],
        planItems: state.planItems.map((item) =>
          action.activities.some((activity) => activity.planItemId === item.id)
            ? { ...item, status: "completed" as const }
            : item,
        ),
        device: {
          ...state.device,
          status: "synced",
          lastSyncAt: action.at,
          syncSources,
          weather: { temperature: 23, humidity: 62, condition: "多云" },
          sleepHours: 7.2,
          hrvMs: 58,
          restingHeartRate: 52,
        },
        recoveryUpdatedAt: action.at,
        toast: action.activities.length
          ? `设备数据已同步到首页、训练记录页与我的 · 新增 ${action.activities.length} 条训练记录`
          : "设备数据已同步；所选日期没有发现训练记录",
      };
    }
    case "MESSAGE":
      return {
        ...state,
        conversation: [...state.conversation, action.message],
        composerDraft: "",
      };
    case "START_DAILY_SESSION": {
      if (state.homeSessionDate === action.date) return state;
      const archive = state.conversation.length
        ? [
            ...state.conversationArchive,
            {
              id: `session-${state.homeSessionDate}`,
              date: state.homeSessionDate,
              title: "训练与恢复对话",
              messages: state.conversation,
            },
          ]
        : state.conversationArchive;
      return {
        ...state,
        homeSessionDate: action.date,
        conversationArchive: archive,
        conversation: createInitialState().conversation,
        composerDraft: "",
        analysisContext: null,
        conversationResetAt: null,
        suggestedQuestionsStatus: "idle",
        suggestedQuestions: [],
      };
    }
    case "CLEAR_HISTORY":
      return {
        ...state,
        conversation: [],
        conversationArchive: [],
        composerDraft: "",
        analysisContext: null,
        conversationResetAt: action.clearedAt,
        historyResetStatus: "idle",
        suggestedQuestions: [],
        suggestedQuestionsStatus: "idle",
        suggestedQuestionsUpdatedAt: "",
        toast: "全部历史对话已永久删除",
      };
    case "CONFIRM_PLAN": {
      const confirmedPlan = state.planItems.find((plan) => plan.id === action.id);
      return {
        ...state,
        planItems: state.planItems.map((p) =>
          p.id === action.id ? { ...p, status: "confirmed" } : p,
        ),
        planSyncReceipt: {
          planId: action.id,
          kind: "confirmed",
          summary: `${confirmedPlan?.title ?? "训练计划"}已确认并下发设备课表`,
          version: confirmedPlan?.version ?? state.planVersion,
          updatedAt: action.at,
        },
        toast: "已确认该训练，并同步到训练中心与首页",
      };
    }
    case "APPLY_ADJUSTMENT": {
      const version = state.planVersion + 1;
      const changedIds = new Set(action.adjustment.affected);
      const completedIds = completedPlanIds(state);
      const currentById = new Map(state.planItems.map((item) => [item.id, item]));
      const adjustedItems = action.adjustment.items
        .map((item) => {
          const current = currentById.get(item.id);
          if (current && (completedIds.has(item.id) || current.date < action.effectiveDate)) {
            return current;
          }
          return changedIds.has(item.id)
            ? {
                ...item,
                status: "adjusted" as const,
                changed: true,
                version,
              }
            : { ...item, version };
        })
        .sort((a, b) => a.date.localeCompare(b.date));
      const selectedChanged = adjustedItems.find((item) =>
        changedIds.has(item.id) &&
        state.planItems.some(
          (current) =>
            current.id === item.id && current.date === state.selectedDate,
        ),
      );
      const firstWeek = adjustedItems.filter(
        (item) => (item.weekIndex ?? 1) === state.planProgram.currentWeek,
      );
      const effectiveWeek =
        adjustedItems.find((item) => item.date >= action.effectiveDate)?.weekIndex ??
        state.planProgram.currentWeek;
      const previousWeekSessions = state.planItems.filter(
        (item) => (item.weekIndex ?? 1) === effectiveWeek,
      ).length;
      const currentWeekSessions = adjustedItems.filter(
        (item) => (item.weekIndex ?? 1) === effectiveWeek,
      ).length;
      const affectedDates = new Set(
        action.adjustment.changes.flatMap((change) => [
          change.before.date,
          change.after.date,
        ]),
      );
      const completedNutritionNodesByDate = Object.fromEntries(
        Object.entries(state.completedNutritionNodesByDate).filter(
          ([date]) => !affectedDates.has(date),
        ),
      );
      return {
        ...state,
        planVersion: version,
        planItems: adjustedItems,
        selectedDate: selectedChanged?.date ?? state.selectedDate,
        trainingView: "plan",
        planProgram: {
          ...state.planProgram,
          summary: `每周 ${currentWeekSessions} 次 · 调整生效日前的训练按原版本保留`,
          weeklyLoad: firstWeek.reduce((sum, item) => sum + item.load, 0),
          basis: [
            ...state.planProgram.basis.filter(
              (item) => !item.startsWith("最近调整："),
            ),
            `最近调整：${action.adjustment.reason}`,
          ],
        },
        reminder: {
          ...state.reminder,
          triggerLabel: "第 40 分钟",
          amount: "能量胶 1 支 · 饮水 150 ml",
        },
        nutritionPlan:
          action.nutritionPlansByDate[
            selectedChanged?.date ?? state.selectedDate
          ] ?? null,
        nutritionPlansByDate: action.nutritionPlansByDate,
        nutritionPlanStatus: "ready",
        completedNutritionNodes:
          completedNutritionNodesByDate[iso(0)] ?? [],
        completedNutritionNodesByDate,
        planAgentFlow: {
          ...state.planAgentFlow,
          stage: "synced",
          preview: null,
        },
        planSyncReceipt: {
          planId: action.adjustment.affected[0] ?? state.planAgentFlow.planId,
          kind: "adjusted",
          summary: action.adjustment.reason,
          version,
          updatedAt: action.at,
        },
        planRevision: {
          version,
          previousVersion: state.planVersion,
          kind:
            adjustedItems.length === state.planItems.length
              ? "tuning"
              : "structure",
          effectiveDate: action.effectiveDate,
          appliedAt: action.at,
          reason: action.adjustment.reason,
          inheritedCompletedCount: state.planItems.filter((item) =>
            completedIds.has(item.id),
          ).length,
          previousWeekSessions,
          currentWeekSessions,
        },
      toast: `计划 V${version} 已同步到训练界面与已连接设备`,
      };
    }
    case "TICK": {
      if (state.live.status !== "recording") return state;
      const elapsed = state.live.elapsed + 1;
      const activePlan =
        state.planItems.find((item) => item.status === "in_progress") ??
        state.planItems.find((item) => item.date === state.selectedDate) ??
        state.planItems[0];
      const current = resolveTrainingProgress(activePlan, state.live);
      const paceSeconds = paceSecondsForStep(current.step, activePlan.pace);
      const distanceIncrement =
        current.step.type === "stretch" ? 0 : 1 / paceSeconds;
      const distance = Number(
        (state.live.distance + distanceIncrement).toFixed(3),
      );
      const reachedTarget =
        current.step.target.metric === "time"
          ? elapsed - state.live.stepStartedAt >= current.step.target.value * 60
          : distance - state.live.stepStartedDistance >=
            current.step.target.value;
      const nextIndex =
        reachedTarget && current.index < current.steps.length - 1
          ? current.index + 1
          : current.index;
      const stepChanged = nextIndex !== current.index;
      const nextStep = current.steps[nextIndex];
      const targetHeartRate =
        nextStep.type === "interval"
          ? 158
          : nextStep.type === "tempo"
            ? 150
            : nextStep.type === "warmup" || nextStep.type === "cooldown"
              ? 136
              : 144;
      const heartRate =
        state.live.heartRate === targetHeartRate
          ? targetHeartRate
          : state.live.heartRate +
            (state.live.heartRate < targetHeartRate ? 1 : -1);
      return {
        ...state,
        live: {
          ...state.live,
          elapsed,
          distance,
          heartRate,
          energy: Number(
            (
              state.live.energy + (nextStep.type === "interval" ? 0.22 : 0.16)
            ).toFixed(1),
          ),
          currentStepIndex: nextIndex,
          stepStartedAt: stepChanged ? elapsed : state.live.stepStartedAt,
          stepStartedDistance: stepChanged
            ? state.live.distance
            : state.live.stepStartedDistance,
        },
        toast: stepChanged
          ? `进入${nextStep.title}${current.steps[nextIndex + 1] ? ` · 下一步 ${current.steps[nextIndex + 1].title}` : " · 最后一步"}`
          : state.toast,
      };
    }
    case "REMINDER_TRIGGER":
      return state.reminder.enabled
        ? {
            ...state,
            reminder: {
              ...state.reminder,
              status: "triggered",
              vibrationCount: 1,
            },
          }
        : state;
    case "REMINDER_ACTION": {
      const planDate = state.nutritionPlan?.planDate ?? state.selectedDate;
      const completedForDate =
        state.completedNutritionNodesByDate[planDate] ??
        (planDate === iso(0) ? state.completedNutritionNodes : []);
      const completedNutritionNodes =
        action.status === "acknowledged"
          ? Array.from(
              new Set([
                ...completedForDate,
                state.reminder.nodeId,
              ]),
            )
          : completedForDate;
      const nutritionPlan =
        action.status === "acknowledged" && state.nutritionPlan
          ? {
              ...state.nutritionPlan,
              nodes: state.nutritionPlan.nodes.map((node) =>
                node.id === state.reminder.nodeId
                  ? { ...node, completedAt: new Date().toISOString() }
                  : node,
              ),
            }
          : state.nutritionPlan;
      const totalNodes = nutritionPlan?.nodes.length ?? 0;
      const nutritionCompletion = totalNodes
        ? Math.round((completedNutritionNodes.length / totalNodes) * 100)
        : state.nutritionCompletion;
      return {
        ...state,
        reminder:
          action.status === "delayed"
            ? {
                ...state.reminder,
                status: "scheduled",
                lastAction: "delayed",
                triggerSecond: state.live.elapsed + 600,
                triggerLabel: "延后 10 分钟",
              }
            : {
                ...state.reminder,
                status: action.status,
                lastAction: action.status,
              },
        nutritionPlan,
        nutritionPlansByDate:
          nutritionPlan && action.status === "acknowledged"
            ? { ...state.nutritionPlansByDate, [planDate]: nutritionPlan }
            : state.nutritionPlansByDate,
        nutritionCompletion,
        completedNutritionNodes:
          planDate === iso(0)
            ? completedNutritionNodes
            : state.completedNutritionNodes,
        completedNutritionNodesByDate:
          action.status === "acknowledged"
            ? {
                ...state.completedNutritionNodesByDate,
                [planDate]: completedNutritionNodes,
              }
            : state.completedNutritionNodesByDate,
        toast:
          action.status === "acknowledged"
            ? "补给已记录，营养页与训练记录已同步"
            : action.status === "delayed"
              ? "已延后 10 分钟，不会重复震动"
              : "本次已跳过，训练后可补记",
      };
    }
    case "FEEDBACK":
      return { ...state, feedback: { ...state.feedback, ...action.patch } };
    case "REPORT":
      return { ...state, report: { ...state.report, ...action.patch } };
    case "RESET":
      return createInitialState();
    default:
      return state;
  }
}

interface AppContextValue {
  state: AppState;
  go: (route: RouteId, tab?: AppState["activeTab"]) => void;
  back: () => void;
  patch: (patch: Partial<AppState>) => void;
  profile: (patch: Partial<AppState["profile"]>) => void;
  device: (patch: Partial<AppState["device"]>) => void;
  confirmPlan: (id: string) => void;
  confirmReadyPlan: (id: string, title: string) => void;
  openPlanAgent: (
    sourceRoute: RouteId,
    planId: string,
    planTitle: string,
  ) => void;
  applyAdjustment: (
    request: string,
    adjustment?: PlanAdjustmentData,
  ) => Promise<boolean>;
  sendMessage: (text: string, displayText?: string) => Promise<void>;
  retryLastMessage: () => Promise<void>;
  generatePlan: () => Promise<boolean>;
  refreshSuggestedQuestions: () => Promise<void>;
  clearConversationHistory: () => Promise<boolean>;
  connectDevice: (provider: string) => Promise<boolean>;
  syncDevice: () => Promise<void>;
  tick: () => void;
  triggerReminder: () => void;
  reminderAction: (status: "acknowledged" | "delayed" | "skipped") => void;
  feedback: (patch: Partial<AppState["feedback"]>) => void;
  report: (patch: Partial<AppState["report"]>) => void;
  reset: () => void;
  openAnalysis: (title: string, sourceRoute: RouteId, summary: string) => void;
  sendAnalysisToHome: (
    title: string,
    sourceRoute: RouteId,
    summary: string,
    analysis?: TrainingAnalysisData,
  ) => void;
  publishTrainingAnalysisUpdate: (
    analysis: TrainingAnalysisData,
    previousCompletion: number,
    nextCompletion: number,
  ) => void;
  openNutritionFeedback: (sourceRoute: RouteId) => void;
  startDemo: (demo: number) => void;
}

const AppContext = createContext<AppContextValue | null>(null);

export function AppProvider({ children }: { children: ReactNode }) {
  const saved = (() => {
    try {
      return JSON.parse(
        localStorage.getItem("running-ai-prd-v1") ?? "null",
      ) as AppState | null;
    } catch {
      return null;
    }
  })();
  const defaults = createInitialState();
  const savedPlanItems = saved?.planItems ?? [];
  const migratedPlanItems =
    saved &&
    saved.planGenerationStatus === "ready" &&
    savedPlanItems.length > 0 &&
    savedPlanItems.length <= 7
      ? buildDemoCycle(
          savedPlanItems.map((item, index) => ({
            ...item,
            id: item.id.replace(/-w\d+$/, "") || `plan-${index + 1}`,
            weekIndex: 1,
          })),
          saved.planProgram?.totalWeeks ?? 8,
        )
      : savedPlanItems;
  const migratedProfile = Object.fromEntries(
    Object.entries(saved?.profile ?? {}).filter(([key]) => key !== "diet"),
  ) as Partial<AppState["profile"]>;
  const hydrated = saved
    ? {
        ...defaults,
        ...saved,
        activeTab:
          String(saved.activeTab) === "nutrition"
            ? "home"
            : saved.activeTab ?? defaults.activeTab,
        authMode: localStorage.getItem("running-ai-access-token")
          ? ("authenticated" as const)
          : ("guest" as const),
        planItems: migratedPlanItems.length
          ? migratedPlanItems.map(normalizePlanItemTitle)
          : defaults.planItems,
        planProgram: {
          ...defaults.planProgram,
          ...saved.planProgram,
          startDate:
            saved.planProgram?.startDate ??
            migratedPlanItems[0]?.date ??
            defaults.planProgram.startDate,
          endDate:
            saved.planProgram?.endDate ??
            migratedPlanItems.at(-1)?.date ??
            defaults.planProgram.endDate,
        },
        profile: {
          ...defaults.profile,
          ...migratedProfile,
          goal: normalizeTrainingGoal(saved.profile?.goal),
        },
        device: {
          ...defaults.device,
          ...saved.device,
          connectedProviders:
            saved.device.connectedProviders ??
            (saved.device.provider ? [saved.device.provider] : []),
          syncSources:
            saved.device.syncSources ??
            (saved.device.provider ? [saved.device.provider] : []),
        },
        feedback: {
          ...defaults.feedback,
          ...saved.feedback,
          thirst: saved.feedback.thirst ?? 5,
        },
        mealEntries: saved.mealEntries ?? defaults.mealEntries,
        hydrationByDate:
          saved.hydrationByDate ?? {
            [iso(0)]: saved.hydrationMl ?? defaults.hydrationMl,
          },
        selectedMealId: saved.selectedMealId ?? "",
        nutritionPlansByDate: saved.nutritionPlansByDate ?? {},
        nutritionPlanStatus:
          saved.nutritionPlanStatus === "loading"
            ? "idle"
            : saved.nutritionPlanStatus ?? defaults.nutritionPlanStatus,
        completedNutritionNodesByDate:
          saved.completedNutritionNodesByDate ?? {
            [iso(0)]: saved.completedNutritionNodes ?? defaults.completedNutritionNodes,
          },
      }
    : defaults;
  const [state, dispatch] = useReducer(reducer, hydrated);

  useEffect(() => {
    localStorage.setItem("running-ai-prd-v1", JSON.stringify(state));
  }, [state]);
  useEffect(() => {
    dispatch({ type: "START_DAILY_SESSION", date: iso(0) });
  }, []);
  useEffect(() => {
    if (
      !state.profileComplete ||
      state.planGenerationStatus !== "ready" ||
      state.nutritionPlanStatus !== "idle" ||
      !state.planItems.length
    ) {
      return;
    }
    dispatch({
      type: "PATCH",
      patch: { nutritionPlanStatus: "loading" },
    });
    const context = buildAgentContext(state);
    void Promise.all(
      state.planItems.map(async (item) => {
        const nutrition = await services.nutrition.buildPlan(
          item.id,
          context,
          item.date,
        );
        return [item.date, nutrition.data] as const;
      }),
    )
      .then((entries) => {
        const nutritionPlansByDate = Object.fromEntries(entries);
        dispatch({
          type: "PATCH",
          patch: {
            nutritionPlansByDate,
            nutritionPlan:
              nutritionPlansByDate[state.selectedDate] ?? entries[0]?.[1] ?? null,
            nutritionPlanStatus: "ready",
          },
        });
      })
      .catch(() => {
        dispatch({
          type: "PATCH",
          patch: {
            nutritionPlanStatus: "error",
          toast: "暂时没能读到这份补给规划。训练计划不受影响，可以稍后再试。",
          },
        });
      });
  }, [
    state.nutritionPlanStatus,
    state.planGenerationStatus,
    state.planProfileSignature,
    state.planVersion,
    state.profileComplete,
  ]);
  useEffect(() => {
    if (!state.toast) return;
    const id = window.setTimeout(
      () => dispatch({ type: "PATCH", patch: { toast: "" } }),
      2800,
    );
    return () => window.clearTimeout(id);
  }, [state.toast]);

  const runPlanGeneration = async () => {
    if (!state.profileComplete) {
        dispatch({ type: "PATCH", patch: { toast: "先让我了解你的基础情况，再一起制定训练计划。" } });
      return null;
    }
    dispatch({
      type: "PATCH",
      patch: { planGenerationStatus: "loading", agentFailure: false },
    });
    try {
      const context = buildAgentContext(state);
      const result = await services.agent.generatePlan(context);
      const expectedFrequency = Math.max(
        1,
        Math.min(7, state.profile.frequency),
      );
      const expectedCycleItems =
        expectedFrequency * result.data.program.totalWeeks;
      if (result.data.items.length !== expectedCycleItems) {
        throw new Error(
          `Agent 返回 ${result.data.items.length} 次训练，与 ${result.data.program.totalWeeks} 周 × 每周 ${expectedFrequency} 次不一致`,
        );
      }
      const nutritionContext = {
        ...context,
        plan: result.data.items,
      };
      const nutritionEntries = await Promise.all(
        result.data.items.map(async (item) => {
          const nutrition = await services.nutrition.buildPlan(
            item.id,
            nutritionContext,
            item.date,
          );
          return [item.date, nutrition.data] as const;
        }),
      );
      dispatch({
        type: "PLAN_GENERATED",
        items: result.data.items,
        program: result.data.program,
        nutritionPlansByDate: Object.fromEntries(nutritionEntries),
        signature: profileSignature(state.profile),
        generatedAt: `今天 ${timeNow()}`,
      });
      return result.data;
    } catch (error) {
      dispatch({
        type: "PATCH",
        patch: {
          planGenerationStatus: "error",
          agentFailure: true,
          toast:
            error instanceof Error
              ? error.message
            : "暂时没能生成计划。请检查网络后再试，现有内容不会被覆盖。",
        },
      });
      return null;
    }
  };

  const applyPlanAdjustment = async (
    request: string,
    adjustment?: PlanAdjustmentData,
  ) => {
    dispatch({
      type: "PATCH",
      patch: {
            toast: "AI 教练正在把你的想法整理成可执行调整…",
        planAgentFlow: {
          ...state.planAgentFlow,
          stage: "applying",
          request,
        },
      },
    });
    try {
      const result = adjustment
        ? { data: adjustment }
        : await services.agent.adjustPlan(
            request,
            state.planItems,
            state.selectedDate,
          );
      const currentIds = new Set(state.planItems.map((item) => item.id));
      const resultIds = new Set(result.data.items.map((item) => item.id));
      const protectedIds = completedPlanIds(state);
      const effectiveDate = iso(0);
      if (
        resultIds.size !== result.data.items.length ||
        result.data.items.length === 0 ||
        [...protectedIds].some((id) => currentIds.has(id) && !resultIds.has(id)) ||
        result.data.affected.some((id) => !resultIds.has(id)) ||
        result.data.items.some(
          (item) => !currentIds.has(item.id) && item.date < effectiveDate,
        )
      ) {
        throw new Error("Agent 返回的计划会改写已完成训练，已自动拦截");
      }
      const nutritionContext = {
        ...buildAgentContext(state),
        plan: result.data.items,
      };
      const affectedItems = result.data.items.filter((item) =>
        result.data.affected.includes(item.id),
      );
      const regeneratedNutrition = await Promise.all(
        affectedItems.map(async (item) => {
          const nutrition = await services.nutrition.buildPlan(
            item.id,
            nutritionContext,
            item.date,
          );
          return [item.date, nutrition.data] as const;
        }),
      );
      const nutritionPlansByDate = { ...state.nutritionPlansByDate };
      result.data.changes.forEach((change) => {
        if (change.before.date !== change.after.date) {
          delete nutritionPlansByDate[change.before.date];
        }
      });
      regeneratedNutrition.forEach(([date, plan]) => {
        nutritionPlansByDate[date] = plan;
      });
      dispatch({
        type: "APPLY_ADJUSTMENT",
        adjustment: result.data,
        nutritionPlansByDate,
        at: `今天 ${timeNow()}`,
        effectiveDate,
      });
      if (state.planAgentFlow.stage !== "idle") {
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "agent",
        text: `调整好了。计划 V${state.planVersion + 1} 已同步到训练界面与已连接平台，训练和补给节点也按新日期重新排好了。`,
            time: timeNow(),
            card: "training",
            sources: ["用户确认", "Agent 变更草案", "周期训练计划"],
            contextVersion: `plan-sync-v${state.planVersion + 1}`,
          },
        });
      }
      return true;
    } catch {
      dispatch({
        type: "PATCH",
        patch: {
          agentFailure: true,
          planAgentFlow: {
            ...state.planAgentFlow,
            stage: adjustment ? "preview" : "collecting",
          },
          toast: "这次调整暂时没能应用，原计划已经替你保留，可以重新生成。",
        },
      });
      return false;
    }
  };

  const value = useMemo<AppContextValue>(
    () => ({
      state,
      go: (route, tab) => dispatch({ type: "NAVIGATE", route, tab }),
      back: () => dispatch({ type: "BACK" }),
      patch: (patch) => dispatch({ type: "PATCH", patch }),
      profile: (patch) => dispatch({ type: "PROFILE", patch }),
      device: (patch) => dispatch({ type: "DEVICE", patch }),
      confirmPlan: (id) =>
        dispatch({ type: "CONFIRM_PLAN", id, at: `今天 ${timeNow()}` }),
      confirmReadyPlan: (id, title) => {
        dispatch({ type: "CONFIRM_PLAN", id, at: `今天 ${timeNow()}` });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "user",
            text: `确认「${title}」并同步设备`,
            time: timeNow(),
          },
        });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "agent",
        text: `计划已经确认并同步。去训练界面可以查看状态，也可以在已连接的运动设备上调用「${title}」；练完回来，我会在首页陪你复盘。`,
            time: timeNow(),
            card: "training",
        sources: ["用户确认", "训练计划", "健康平台与设备课表"],
            contextVersion: `ready-sync-${id}`,
          },
        });
        dispatch({ type: "NAVIGATE", route: "HOME-01", tab: "home" });
      },
      openPlanAgent: (sourceRoute, planId, planTitle) =>
        dispatch({
          type: "OPEN_PLAN_AGENT",
          sourceRoute,
          planId,
          planTitle,
        }),
      applyAdjustment: applyPlanAdjustment,
      sendMessage: async (text, displayText) => {
        if (!text.trim()) return;
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "user",
            text: displayText?.trim() || text,
            time: timeNow(),
          },
        });
        dispatch({ type: "PATCH", patch: { agentFailure: false } });
        if (state.safetyBlocked) {
          dispatch({
            type: "MESSAGE",
            message: {
              id: crypto.randomUUID(),
              role: "agent",
              text: "检测到高风险症状状态。请停止训练并尽快寻求医疗帮助；在解除安全状态前不再提供普通训练或补给建议。",
              time: timeNow(),
              card: "safety",
            },
          });
          return;
        }
        if (!["idle", "synced"].includes(state.planAgentFlow.stage)) {
          const normalized = text.replace(/\s+/g, "");
          if (
            state.planAgentFlow.stage === "preview" &&
            /(保持原计划|取消调整|不改了|不要调整)/.test(normalized)
          ) {
            dispatch({
              type: "PATCH",
              patch: {
                composerDraft: "",
                planAgentFlow: {
                  stage: "idle",
                  sourceRoute: null,
                  planId: "",
                  request: "",
                  preview: null,
                },
            toast: "好的，原计划已经保留，训练界面没有变化。",
              },
            });
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
        text: "好的，我们先按原计划走。训练界面和已连接设备课表都没有变化，想再调整时随时告诉我。",
                time: timeNow(),
              },
            });
            return;
          }
          if (
            state.planAgentFlow.stage === "preview" &&
            /(应用|确认|同意|就这样)/.test(normalized) &&
            state.planAgentFlow.preview
          ) {
            await applyPlanAdjustment(
              state.planAgentFlow.request,
              state.planAgentFlow.preview,
            );
            return;
          }
          dispatch({
            type: "PATCH",
            patch: {
              composerDraft: "",
              planAgentFlow: {
                ...state.planAgentFlow,
                stage: "generating",
                request: text,
              },
            toast: "AI 教练正在核对整个训练周期，确保前后负荷接得住…",
            },
          });
          try {
            const result = await services.agent.adjustPlan(
              text,
              state.planItems,
              state.selectedDate,
            );
            const addedCount = result.data.items.filter(
              (item) => !state.planItems.some((current) => current.id === item.id),
            ).length;
            const changedDates = result.data.changes
              .map((change) =>
                change.before.date === change.after.date
                  ? change.before.date
                  : `${change.before.date} → ${change.after.date}`,
              )
              .join("、");
            const impactSummary = [
              changedDates,
              addedCount ? `新增 ${addedCount} 次后续训练` : "",
            ]
              .filter(Boolean)
              .join("；");
            dispatch({
              type: "PATCH",
              patch: {
                planAgentFlow: {
                  ...state.planAgentFlow,
                  stage: "preview",
                  request: text,
                  preview: result.data,
                },
              },
            });
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
              text: `我把调整草案整理好了：${result.data.reason}。${impactSummary || "将调整当前训练日"}；已经完成的训练和生效日前进度都不会动。你可以继续和我商量，也可以确认并同步。`,
                time: timeNow(),
                card: "proposal",
                sources: ["用户调整要求", "周期训练计划", "相邻训练负荷"],
                contextVersion: `plan-preview-${state.planVersion + 1}`,
              },
            });
          } catch {
            dispatch({
              type: "PATCH",
              patch: {
                agentFailure: true,
                planAgentFlow: {
                  ...state.planAgentFlow,
                  stage: "collecting",
                  request: text,
                },
                composerDraft: text,
            toast: "这次没能整理出调整草案，原计划保持不变，可以再告诉我一次。",
              },
            });
          }
          return;
        }
        if (state.nutritionFeedbackPending.active) {
          const pending = state.nutritionFeedbackPending;
          const activity =
            state.activities.find((item) => item.id === pending.activityId) ??
            state.activities.find((item) => item.date === pending.planDate) ??
            null;
          const plan =
            state.planItems.find((item) => item.id === activity?.planItemId) ??
            state.planItems.find((item) => item.date === pending.planDate) ??
            null;
          const nutritionPlan =
            state.nutritionPlansByDate[pending.planDate] ?? state.nutritionPlan;
          const nodes = nutritionPlan?.nodes ?? [];
          const normalized = text.replace(/\s+/g, "");
          let nextCompletion: number | null = null;
          let completedNodeIds: string[] | null = null;
          const percentMatch = normalized.match(/(\d{1,3})(?:%|％|成)/);
          const countMatch = normalized.match(/(?:完成(?:了)?|补了)(\d+)项/);
          if (/(全部没|都没|没有完成|一项都没|完全没)/.test(normalized)) {
            nextCompletion = 0;
            completedNodeIds = [];
          } else if (/(全部完成|都完成|全都完成|按计划完成|都补了)/.test(normalized)) {
            nextCompletion = 100;
            completedNodeIds = nodes.map((node) => node.id);
          } else if (countMatch && nodes.length) {
            const count = Math.min(nodes.length, Number(countMatch[1]));
            completedNodeIds = nodes.slice(0, count).map((node) => node.id);
            nextCompletion = Math.round((count / nodes.length) * 100);
          } else if (percentMatch) {
            const raw = Number(percentMatch[1]);
            nextCompletion = normalized.includes("成") && raw <= 10 ? raw * 10 : Math.min(100, raw);
            const count = nodes.length ? Math.round((nextCompletion / 100) * nodes.length) : 0;
            completedNodeIds = nodes.slice(0, count).map((node) => node.id);
          } else if (nodes.length && /(训练前|训练中|训练后)/.test(normalized)) {
            const missedPhases = ["训练前", "训练中", "训练后"].filter(
              (phase) => new RegExp(`${phase}.{0,4}(没|未|忘|漏|跳过)`).test(normalized),
            );
            const positivePhases = ["训练前", "训练中", "训练后"].filter(
              (phase) => normalized.includes(phase) && !missedPhases.includes(phase),
            );
            completedNodeIds = missedPhases.length
              ? nodes.filter((node) => !missedPhases.includes(node.phase)).map((node) => node.id)
              : nodes.filter((node) => positivePhases.includes(node.phase)).map((node) => node.id);
            nextCompletion = Math.round((completedNodeIds.length / nodes.length) * 100);
          }
          if (nextCompletion === null || completedNodeIds === null || !activity || !plan || !nutritionPlan) {
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
              text: "我还没听准你的完成度。可以直接说“全部完成”“完成了 3 项”“约 75%”，或告诉我训练前、中、后漏了哪一段。",
                time: timeNow(),
                contextVersion: `nutrition-feedback-clarify-${pending.activityId || pending.planDate}`,
              },
            });
            return;
          }
          const previousCompletion = activity.nutritionCompletion;
          const updatedActivity = { ...activity, nutritionCompletion: nextCompletion };
          const postTrainingState = state.postTrainingFeedbackByActivity[activity.id] ?? {
            training: false,
            nutrition: false,
            analysisReady: false,
            detectedAt: new Date().toISOString(),
          };
          const canGenerateFullAnalysis =
            postTrainingState.training && state.feedback.submitted;
          const updatedNodes = nodes.map((node) => ({
            ...node,
            completedAt: completedNodeIds.includes(node.id) ? node.completedAt || new Date().toISOString() : undefined,
          }));
          const updatedPlan = { ...nutritionPlan, nodes: updatedNodes };
          dispatch({
            type: "PATCH",
            patch: {
              activities: state.activities.map((item) => item.id === activity.id ? updatedActivity : item),
              nutritionPlan: state.nutritionPlan?.planDate === pending.planDate ? updatedPlan : state.nutritionPlan,
              nutritionPlansByDate: { ...state.nutritionPlansByDate, [pending.planDate]: updatedPlan },
              completedNutritionNodes: pending.planDate === iso(0) ? completedNodeIds : state.completedNutritionNodes,
              completedNutritionNodesByDate: { ...state.completedNutritionNodesByDate, [pending.planDate]: completedNodeIds },
              nutritionCompletion: pending.planDate === iso(0) ? nextCompletion : state.nutritionCompletion,
              nutritionFeedbackPending: { ...pending, active: false },
              postTrainingFeedbackByActivity: {
                ...state.postTrainingFeedbackByActivity,
                [activity.id]: {
                  ...postTrainingState,
                  nutrition: true,
                  analysisReady: canGenerateFullAnalysis,
                },
              },
            },
          });
          if (!canGenerateFullAnalysis) {
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
                text: `补给反馈收到，完成度已记录为 ${nextCompletion}%。再补充一下这次训练的体感，完整训练分析就能生成。`,
                time: timeNow(),
                card: "post_training_checkin",
                sources: ["用户补给反馈", "设备训练实绩", "训练后反馈状态"],
                contextVersion: `post-training-${activity.id}-nutrition-ready`,
                activityId: activity.id,
              },
            });
            return;
          }
          try {
            const analysisResult = await services.agent.analyzeTraining(
              plan,
              updatedActivity,
              updatedNodes,
              completedNodeIds,
              state.feedback,
            );
            dispatch({ type: "PATCH", patch: { trainingAnalysis: analysisResult.data } });
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
              text: `两项反馈都收到了。补给完成度已从 ${previousCompletion}% 更新为 ${nextCompletion}%，下面是这次训练的机能情况与完整总结。${nextCompletion < 100 ? "没完成的部分不用自责，我会把它纳入恢复和下一次补给建议。" : "这次执行得很完整，接下来继续观察身体耐受和恢复就好。"}`,
                time: timeNow(),
                card: "training_performance",
                sources: ["用户对话反馈", "本次训练补给方案", "设备训练实绩"],
                contextVersion: analysisResult.data.contextVersion,
                analysis: analysisResult.data,
                activityId: activity.id,
              },
            });
          } catch {
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
                text: `补给完成度已保存为 ${nextCompletion}%，但训练分析暂时更新失败。原训练实绩和本次反馈都已保留，可以稍后让我重新分析。`,
                time: timeNow(),
              },
            });
          }
          return;
        }
        const asksForPlan =
          /(生成|制定|创建|安排|做).{0,8}(训练)?计划|训练计划.{0,8}(生成|制定|创建|安排|做)/.test(
            text,
          );
        if (asksForPlan && state.planGenerationStatus !== "ready") {
          const generated = await runPlanGeneration();
          if (generated)
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
              text: `你的新计划「${generated.program.title}」已经排好了，包含每周 ${generated.items.length} 次训练和对应补给。可以去训练中心确认，也可以留在这里继续和我打磨。`,
                time: timeNow(),
                card: "training",
                sources: generated.program.basis,
                contextVersion: generated.contextVersion,
              },
            });
          return;
        }
        try {
          const context = buildAgentContext(state);
          const result = await services.agent.chat(text, context);
          dispatch({
            type: "MESSAGE",
            message: {
              id: crypto.randomUUID(),
              role: "agent",
              text: result.data.answer,
              time: timeNow(),
              card: result.data.safety ? "safety" : undefined,
              sources: result.sourceData,
              contextVersion: result.data.contextVersion,
            },
          });
          if (result.data.safety)
            dispatch({ type: "PATCH", patch: { safetyBlocked: true } });
        } catch {
          dispatch({
            type: "PATCH",
            patch: {
              agentFailure: true,
              lastFailedPrompt: text,
          toast: "AI 教练暂时没有连上，你的输入已经替你保留。",
            },
          });
        }
      },
      retryLastMessage: async () => {
        if (!state.lastFailedPrompt || state.agentRetrying) return;
        dispatch({
          type: "PATCH",
          patch: { agentRetrying: true, agentFailure: false },
        });
        try {
          const result = await services.agent.chat(
            state.lastFailedPrompt,
            buildAgentContext(state),
          );
          dispatch({
            type: "MESSAGE",
            message: {
              id: crypto.randomUUID(),
              role: "agent",
              text: result.data.answer,
              time: timeNow(),
              card: result.data.safety ? "safety" : undefined,
              sources: result.sourceData,
              contextVersion: result.data.contextVersion,
            },
          });
          dispatch({
            type: "PATCH",
            patch: {
              agentRetrying: false,
              lastFailedPrompt: "",
            toast: "已经重新连上，回答也替你补回来了。",
            },
          });
        } catch {
          dispatch({
            type: "PATCH",
            patch: {
              agentRetrying: false,
              agentFailure: true,
            toast: "这次还是没能连上。你的问题还在，可以稍后再试。",
            },
          });
        }
      },
      generatePlan: async () => {
        return Boolean(await runPlanGeneration());
      },
      refreshSuggestedQuestions: async () => {
        dispatch({
          type: "PATCH",
          patch: { suggestedQuestionsStatus: "loading" },
        });
        try {
          const result = await services.agent.suggestedQuestions(
            buildAgentContext(state),
          );
          dispatch({
            type: "PATCH",
            patch: {
              suggestedQuestions: result.data.questions,
              suggestedQuestionsStatus: "ready",
              suggestedQuestionsUpdatedAt: timeNow(),
            },
          });
        } catch {
          dispatch({
            type: "PATCH",
            patch: { suggestedQuestionsStatus: "error" },
          });
        }
      },
      clearConversationHistory: async () => {
        dispatch({
          type: "PATCH",
          patch: { historyResetStatus: "processing" },
        });
        try {
          const result = await services.agent.clearConversationHistory();
          dispatch({ type: "CLEAR_HISTORY", clearedAt: result.clearedAt });
          return true;
        } catch {
          dispatch({
            type: "PATCH",
            patch: {
              historyResetStatus: "error",
              toast: "历史对话删除失败，原记录已保留",
            },
          });
          return false;
        }
      },
      connectDevice: async (provider) => {
        dispatch({
          type: "DEVICE",
          patch: { status: "authorizing", authorizingProvider: provider },
        });
        try {
          await services.device.connect(provider);
          dispatch({ type: "DEVICE_CONNECTED", provider });
          return true;
        } catch {
          dispatch({
            type: "DEVICE",
            patch: { status: "failed", authorizingProvider: "" },
          });
          dispatch({
            type: "PATCH",
        patch: { toast: `${provider} 暂时没能连接，设备列表没有变化，请再试一次。` },
          });
          return false;
        }
      },
      syncDevice: async () => {
        if (state.device.status === "disconnected") {
          dispatch({
            type: "PATCH",
        patch: { toast: "还没有连接设备。连接后，我才能帮你读取训练与恢复数据。" },
          });
          return;
        }
        dispatch({ type: "DEVICE", patch: { status: "syncing" } });
        try {
          const result = await services.device.sync({
            provider: state.device.provider || state.device.connectedProviders[0] || "设备",
            selectedDate: state.selectedDate,
            plans: state.planItems,
          });
          dispatch({
            type: "DEVICE_SYNCED",
            at: `今天 ${timeNow()}`,
            activities: result.activities,
          });
          const detectedActivity = result.activities.find(
            (activity) => !state.postTrainingFeedbackByActivity[activity.id],
          );
          if (detectedActivity) {
            dispatch({
              type: "PATCH",
              patch: {
                lastCompletedActivityId: detectedActivity.id,
                selectedDate: detectedActivity.date,
                trainingAnalysis: null,
                feedback: { ...state.feedback, submitted: false },
                postTrainingFeedbackByActivity: {
                  ...state.postTrainingFeedbackByActivity,
                  [detectedActivity.id]: {
                    training: false,
                    nutrition: false,
                    analysisReady: false,
                    detectedAt: new Date().toISOString(),
                  },
                },
              },
            });
            dispatch({
              type: "MESSAGE",
              message: {
                id: crypto.randomUUID(),
                role: "agent",
                text: `我发现设备刚刚回传了这次 ${detectedActivity.distance.toFixed(2)} 公里的训练记录。训练感受和补给执行各补充一次，我就能把客观数据与身体感受放在一起，生成完整训练分析。`,
                time: timeNow(),
                card: "post_training_checkin",
                sources: [detectedActivity.source, "本次训练计划", "训练后反馈状态"],
                contextVersion: `post-training-${detectedActivity.id}`,
                activityId: detectedActivity.id,
              },
            });
          }
        } catch {
          dispatch({ type: "DEVICE", patch: { status: "failed" } });
          dispatch({
            type: "PATCH",
          patch: { toast: "设备暂时没同步上，上次数据已经保留，并标记为非最新。" },
          });
        }
      },
      tick: () => dispatch({ type: "TICK" }),
      triggerReminder: () => dispatch({ type: "REMINDER_TRIGGER" }),
      reminderAction: (status) => dispatch({ type: "REMINDER_ACTION", status }),
      feedback: (patch) => dispatch({ type: "FEEDBACK", patch }),
      report: (patch) => dispatch({ type: "REPORT", patch }),
      reset: () => {
        localStorage.removeItem("running-ai-prd-v1");
        dispatch({ type: "RESET" });
      },
      openAnalysis: (title, sourceRoute, summary) => {
        dispatch({
          type: "PATCH",
          patch: { analysisContext: { title, sourceRoute, summary } },
        });
        dispatch({ type: "NAVIGATE", route: "AI-02" });
      },
      sendAnalysisToHome: (title, sourceRoute, summary, analysis) => {
        const isTrainingSummary = sourceRoute === "TRAIN-SUMMARY";
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "user",
            text: `查看${title}`,
            time: timeNow(),
          },
        });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "agent",
            text: isTrainingSummary
              ? "本次训练的计划与设备实绩已经对齐，关键结果整理如下。"
              : `${title}（来源：${sourceRoute}）\n${summary}\n\n建议依据：个人档案、设备数据、天气、历史训练与当前计划版本。你可以在下方继续追问。`,
            time: timeNow(),
            card: isTrainingSummary ? "training_performance" : undefined,
            contextVersion: isTrainingSummary
              ? analysis?.contextVersion ??
                `training-feedback-${state.lastCompletedActivityId || state.selectedDate}`
              : undefined,
            analysis: isTrainingSummary ? analysis : undefined,
          },
        });
        if (isTrainingSummary && !state.feedback.submitted) {
          dispatch({
            type: "MESSAGE",
            message: {
              id: crypto.randomUUID(),
              role: "agent",
        text: "再告诉我一点这次的体感，我会把恢复、补给和下一次训练一起调得更贴合你。",
              time: timeNow(),
              card: "training_feedback_prompt",
              contextVersion: `training-feedback-${state.lastCompletedActivityId || state.selectedDate}`,
            },
          });
        }
        dispatch({ type: "NAVIGATE", route: "HOME-01", tab: "home" });
      },
      openNutritionFeedback: (sourceRoute) => {
        const activity =
          state.activities.find((item) => item.id === state.lastCompletedActivityId) ??
          state.activities.find((item) => item.date === state.selectedDate) ??
          state.activities.at(0) ??
          null;
        const planDate = activity?.date ?? state.selectedDate;
        dispatch({
          type: "PATCH",
          patch: {
            nutritionFeedbackPending: {
              active: true,
              sourceRoute,
              planDate,
              activityId: activity?.id ?? "",
            },
            composerDraft: "",
          },
        });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "user",
            text: "向 CPT 教练反馈补给完成情况",
            time: timeNow(),
          },
        });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "agent",
        text: "今天这堂练完了，辛苦了。也和我说说补给完成得怎么样：可以说“全部完成”“完成了 3 项”“约 75%”，或直接告诉我漏了哪一段。",
            time: timeNow(),
            sources: ["本次训练补给方案", "训练设备实绩", "用户自然语言反馈"],
            contextVersion: `nutrition-feedback-${activity?.id ?? planDate}`,
          },
        });
        dispatch({ type: "NAVIGATE", route: "HOME-01", tab: "home" });
      },
      publishTrainingAnalysisUpdate: (
        analysis,
        previousCompletion,
        nextCompletion,
      ) => {
        dispatch({
          type: "PATCH",
          patch: { trainingAnalysis: analysis },
        });
        dispatch({
          type: "MESSAGE",
          message: {
            id: crypto.randomUUID(),
            role: "agent",
            text: `补给情况已更新：本次训练补给完成度由 ${previousCompletion}% 更新为 ${nextCompletion}%。我已结合最新记录重新生成训练分析。`,
            time: timeNow(),
            card: "training_performance",
            sources: ["NUT-02 补给完成记录", "本次训练设备实绩", "训练计划"],
            contextVersion: analysis.contextVersion,
            analysis,
          },
        });
      },
      startDemo: (demo) => {
        const routeMap: Record<number, RouteId> = {
          1: "AUTH-01",
          2: "HOME-01",
          3: "TRAIN-HUB",
          4: "PROFILE-01",
          5: "TRAIN-READY",
          6: "NUT-01",
          7: "HOME-01",
          8: "STATE-LAB",
        };
        dispatch({ type: "RESET" });
        dispatch({
          type: "PATCH",
          patch:
            demo === 1
              ? { demo, profileComplete: false, planGenerationStatus: "idle" }
              : demo === 3
                ? {
                    demo,
                    trainingView: "plan" as const,
                    selectedDate: iso(0),
                  }
              : demo === 4
                ? {
                    demo,
                    device: {
                      ...createInitialState().device,
                      provider: "",
                      connectedProviders: [],
                      syncSources: [],
                      status: "disconnected",
                      lastSyncAt: "尚未同步",
                      gpsGranted: false,
                      notificationGranted: false,
                      authorizingProvider: "",
                      weather: null,
                      sleepHours: null,
                      hrvMs: null,
                      restingHeartRate: null,
                    },
                  }
              : { demo },
        });
        dispatch({ type: "NAVIGATE", route: routeMap[demo] ?? "HOME-01" });
      },
    }),
    [state],
  );

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

export function useApp() {
  const ctx = useContext(AppContext);
  if (!ctx) throw new Error("useApp must be used inside AppProvider");
  return ctx;
}
