// calendar.jsx  v2  —  排程与执行日历：月历总览 → 日期执行工作区
// 职责：保留测试计划、周期事件、训练计划镜像与负荷录入的既有数据边界
// 依赖：components.jsx（UI 基础组件）

const {
  useState: calS,
  useEffect: calE,
  useMemo: calM,
} = React;

// ── localStorage ───────────────────────────────────────────────────────
const CAL_LS = 'cal_v1';
const calGet = (k, fb) => { try { const v = localStorage.getItem(CAL_LS + k); return v != null ? JSON.parse(v) : fb; } catch { return fb; } };
const calSet = (k, v) => { try { localStorage.setItem(CAL_LS + k, JSON.stringify(v)); } catch {} };

// ── Date helpers ───────────────────────────────────────────────────────
const CAL_TODAY = toDateStr(new Date());

function toDateStr(d) {
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}
function addDays(ds, n) {
  const d = new Date(ds + 'T00:00:00');
  d.setDate(d.getDate() + n);
  return toDateStr(d);
}

// Returns YYYY-MM-DD strings that fall within [mStart, mEnd] for a schedule
function expandSchedule(sch, year, month) {
  if (!sch.startDate) return [];
  const mNum = String(month + 1).padStart(2, '0');
  const lastDay = new Date(year, month + 1, 0).getDate();
  const mStart = `${year}-${mNum}-01`;
  const mEnd   = `${year}-${mNum}-${String(lastDay).padStart(2, '0')}`;

  const rangeStart = sch.startDate > mStart ? sch.startDate : mStart;
  const rangeEnd   = sch.endDate && sch.endDate < mEnd ? sch.endDate : mEnd;
  if (rangeStart > rangeEnd) return [];

  if (sch.recurrence === 'none') {
    return (sch.startDate >= rangeStart && sch.startDate <= rangeEnd) ? [sch.startDate] : [];
  }

  if (sch.recurrence === 'monthly') {
    const sd = new Date(sch.startDate + 'T00:00:00');
    if (sd.getFullYear() > year || (sd.getFullYear() === year && sd.getMonth() > month)) return [];
    const dom = Math.min(sd.getDate(), lastDay);
    const occ = `${year}-${mNum}-${String(dom).padStart(2, '0')}`;
    return (occ >= rangeStart && occ <= rangeEnd) ? [occ] : [];
  }

  const interval = sch.recurrence === 'weekly' ? 7
    : sch.recurrence === 'biweekly' ? 14
    : Math.max(1, +sch.interval || 7);

  // Fast-forward to first occurrence >= rangeStart
  const schedEpoch = new Date(sch.startDate + 'T00:00:00').getTime();
  const rangeEpoch = new Date(rangeStart + 'T00:00:00').getTime();
  const diffDays = Math.max(0, Math.floor((rangeEpoch - schedEpoch) / 86400000));
  const skipIntervals = Math.floor(diffDays / interval);
  let cur = addDays(sch.startDate, skipIntervals * interval);
  if (cur < rangeStart) cur = addDays(cur, interval);

  const dates = [];
  let guard = 0;
  while (cur <= rangeEnd && guard++ < 60) {
    dates.push(cur);
    cur = addDays(cur, interval);
  }
  return dates;
}

// ── Constants ──────────────────────────────────────────────────────────
const CAL_COLORS = ['#3b82f6', '#a78bfa', '#34d399', '#f59e0b', '#f472b6', '#22d3ee'];
const MONTH_NAMES = ['January','February','March','April','May','June',
  'July','August','September','October','November','December'];
const DAY_NAMES = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const RECUR_OPTIONS = [
  { value: 'none',      label: 'One-time (no recurrence)' },
  { value: 'weekly',    label: 'Weekly (every 7 days)' },
  { value: 'biweekly',  label: 'Every 2 weeks (14 days)' },
  { value: 'monthly',   label: 'Monthly (same day each month)' },
  { value: 'custom',    label: 'Custom interval (set days below)' },
];
const RECUR_LABEL = { none: 'One-time', weekly: 'Weekly', biweekly: 'Biweekly', monthly: 'Monthly', custom: 'Custom' };
const CAL_MONTH_NAMES_ZH = ['1 月','2 月','3 月','4 月','5 月','6 月','7 月','8 月','9 月','10 月','11 月','12 月'];
const CAL_DAY_NAMES_ZH = ['周一','周二','周三','周四','周五','周六','周日'];

// ── Event types (Option B: typed events + per-type routing) ──────────────
// English-first labels with zh译名 for the ZH-mode UI; each type carries a color.
const EVENT_TYPES = [
  { id: 'test',     label: 'Test',     zh: '测试', color: '#3b82f6' },
  { id: 'training', label: 'Training', zh: '训练', color: '#a78bfa' },
  { id: 'match',    label: 'Match',    zh: '比赛', color: '#ef4444' },
  { id: 'rest',     label: 'Rest',     zh: '休息', color: '#10b981' },
  { id: 'note',     label: 'Note',     zh: '备注', color: '#8b94a3' },
];
const EVENT_TYPE_MAP = Object.fromEntries(EVENT_TYPES.map(e => [e.id, e]));

// Resolve a grid/panel item's accent color and display label across the three
// layers the calendar now overlays: recurring schedules, field-test plans, typed events.
function calItemColor(item) {
  if (item.type === 'schedule') return item.schedule.color;
  if (item.type === 'plan')     return '#3b82f6'; // field-test plans render as Test (blue)
  if (item.type === 'training') return '#a78bfa'; // training programs (purple)
  if (item.type === 'event')    return EVENT_TYPE_MAP[item.event.eventType]?.color || '#8b94a3';
  return '#8b94a3';
}
function calItemLabel(item) {
  if (item.type === 'schedule') return item.schedule.name;
  if (item.type === 'plan')     return item.plan.name || 'Test plan';
  if (item.type === 'training') return item.program.name || 'Training';
  if (item.type === 'event')    return item.event.title || item.event.notes || EVENT_TYPE_MAP[item.event.eventType]?.label || 'Event';
  return 'Event';
}
function calItemEventType(item) {
  if (item.type === 'training') return 'training';
  if (item.type === 'plan' || item.type === 'schedule') return 'test';
  if (item.type === 'event') return item.event.eventType || 'note';
  return 'note';
}
function calDateLabel(date) {
  const d = new Date(date + 'T00:00:00');
  return d.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' });
}

const CALENDAR_AXIS_CSS = `
  .calendar-axis-page{min-width:0;max-width:100%;min-height:100%;padding:24px 26px 72px;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
  .cal-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;margin-bottom:17px}
  .cal-kicker{color:var(--muted);font:750 9px var(--font-sans);letter-spacing:.15em;text-transform:uppercase}
  .cal-hero h1{margin:7px 0 4px;font-size:25px;line-height:1.05;letter-spacing:-.035em;text-wrap:balance}
  .cal-hero p{margin:0;color:var(--muted);font-size:11px;line-height:1.6;text-wrap:pretty}
  .cal-actions,.cal-card-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
  .cal-btn{min-height:40px!important;border-radius:9px!important;padding:0 12px!important;font-weight:650!important;transition-property:background-color,border-color,color,transform!important;transition-duration:.14s!important}
  .cal-btn:active,.cal-filter:active,.cal-day-cell:active,.cal-date-row:active{transform:scale(.96)}
  .cal-scope{display:flex;align-items:center;gap:7px;min-height:50px;margin-bottom:14px;padding:7px 8px;border-radius:11px;background:var(--panel-2)}
  .cal-scope strong{padding:0 9px;font-size:10px}.cal-filter{min-height:36px;border:0;border-radius:8px;padding:0 11px;background:transparent;color:var(--muted);font-weight:650;cursor:pointer;transition-property:background-color,color,transform;transition-duration:.14s}
  .cal-filter.on{background:var(--panel);color:var(--text);box-shadow:0 3px 10px rgba(35,43,55,.08)}
  .cal-legend{display:flex;align-items:center;gap:12px;margin-left:auto;padding:0 8px;color:var(--muted);font-size:9.5px}.cal-legend span{display:flex;align-items:center;gap:5px}.cal-dot{width:7px;height:7px;border-radius:3px}
  .cal-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:14px}.cal-stat{min-height:88px;padding:14px 15px;border:1px solid var(--border);border-radius:12px;background:var(--panel)}
  .cal-stat span,.cal-stat small{display:block}.cal-stat span{color:var(--muted);font-size:9.5px}.cal-stat b{display:block;margin:8px 0 3px;font-size:22px;line-height:1;font-variant-numeric:tabular-nums}.cal-stat small{color:var(--muted);font-size:9px}
  .cal-card{min-width:0;border:1px solid var(--border);border-radius:13px;background:var(--panel);box-shadow:0 14px 38px rgba(38,47,63,.07);overflow:hidden}
  .cal-card-head{min-height:62px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 14px;border-bottom:1px solid var(--border)}
  .cal-card-head b,.cal-card-head small{display:block}.cal-card-head b{font-size:13px}.cal-card-head small{margin-top:4px;color:var(--muted);font-size:9.5px}.cal-month-title{font-size:17px!important;font-variant-numeric:tabular-nums}
  .cal-month-grid{max-width:100%;padding:12px;overflow-x:auto;overflow-y:hidden}.cal-weekdays,.cal-month-cells{min-width:700px;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px}.cal-weekdays span{padding:6px;color:var(--muted);font-size:8.5px;font-weight:750;text-align:center;letter-spacing:.08em}
  .cal-day-cell{min-height:112px;border:1px solid var(--border);border-radius:9px;padding:8px;background:var(--panel);color:var(--text);text-align:left;cursor:pointer;position:relative;transition-property:border-color,box-shadow,transform,background-color;transition-duration:.14s}
  .cal-day-cell:hover{border-color:var(--border-strong);box-shadow:0 7px 18px rgba(39,49,66,.08)}.cal-day-cell:focus-visible,.cal-date-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .cal-day-cell.empty{cursor:default;background:var(--bg);box-shadow:none}.cal-day-cell.today{border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}.cal-day-cell.selected{border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 13%,transparent)}
  .cal-day-num{display:inline-grid;place-items:center;min-width:23px;height:23px;font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}.cal-day-cell.today .cal-day-num{border-radius:50%;background:var(--accent);color:#fff}
  .cal-day-events{display:grid;gap:4px;margin-top:7px}.cal-event-pill{min-width:0;display:flex;align-items:center;gap:5px;border-radius:5px;padding:5px 6px;font-size:8.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cal-event-pill:before{content:"";width:5px;height:5px;border-radius:2px;background:currentColor;flex:0 0 auto}
  .cal-more{padding-left:3px;color:var(--muted);font-size:8.5px}
  .cal-command{display:grid;grid-template-columns:250px minmax(0,1fr) 300px;gap:14px;align-items:start}.cal-date-rail{padding:10px}.cal-date-row{width:100%;display:grid;grid-template-columns:42px 1fr auto;gap:8px;align-items:center;min-height:58px;border:0;border-radius:9px;padding:9px;background:transparent;color:var(--text);text-align:left;cursor:pointer;transition-property:background-color,transform;transition-duration:.14s}
  .cal-date-row.on{background:var(--accent-soft)}.cal-date-row strong{font-size:14px;font-variant-numeric:tabular-nums}.cal-date-row b,.cal-date-row small{display:block}.cal-date-row b{font-size:10px}.cal-date-row small,.cal-date-row>span{margin-top:2px;color:var(--muted);font-size:8.5px}
  .cal-command-main{min-height:420px}.cal-command-main>div{height:100%}.cal-day-list{flex:1;padding:12px 14px;display:flex;flex-direction:column;gap:9px}.cal-empty{padding:38px 18px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}
  .cal-event-card{border:1px solid var(--border);border-left-width:3px;border-radius:9px;padding:11px;background:var(--panel)}.cal-event-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.cal-event-card-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.cal-event-card-title strong{font-size:12px}.cal-event-meta{margin-top:5px;color:var(--muted);font-size:10.5px;line-height:1.5}.cal-event-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.cal-mini-btn{min-height:34px;border:1px solid var(--border);border-radius:7px;padding:0 9px;background:var(--panel);color:var(--text-2);font-size:10px;font-weight:650;cursor:pointer}
  .cal-coverage{padding:13px}.cal-coverage-row{margin-bottom:15px}.cal-coverage-row:last-child{margin-bottom:0}.cal-coverage-row span{display:flex;justify-content:space-between;color:var(--text-2);font-size:9.5px}.cal-coverage-row em{color:var(--muted);font-style:normal;font-variant-numeric:tabular-nums}.cal-bar{height:6px;margin-top:7px;border-radius:999px;background:var(--panel-2);overflow:hidden}.cal-bar i{display:block;height:100%;border-radius:inherit;background:var(--accent)}
  .cal-boundary{margin:0 12px 13px;padding:11px;border-radius:9px;background:var(--panel-2);color:var(--muted);font-size:9px;line-height:1.55}.cal-boundary b{color:var(--text-2)}
  .cal-schedule-overlay{position:fixed;inset:0;z-index:2200;display:grid;place-items:center;padding:20px;background:rgba(20,27,38,.28)}.cal-schedule-dialog{width:min(620px,96vw);max-height:86vh;display:flex;flex-direction:column}.cal-schedule-dialog>div{min-height:0}
  @media(max-width:1120px){.cal-command{grid-template-columns:220px minmax(0,1fr)}.cal-command>aside:last-child{grid-column:1/-1}.cal-stats{grid-template-columns:repeat(2,1fr)}}
  @media(max-width:760px){.calendar-axis-page{padding:18px 12px 72px}.cal-hero{align-items:flex-start;flex-direction:column}.cal-actions{width:100%}.cal-scope{overflow:auto}.cal-legend{display:none}.cal-stats{grid-template-columns:repeat(2,1fr)}.cal-command{grid-template-columns:1fr}.cal-command>aside:last-child{grid-column:auto}.cal-card-head{align-items:flex-start;flex-direction:column}.cal-card-tools{width:100%}}
`;

// ── CalendarPage ───────────────────────────────────────────────────────
// P3-5: CalendarPage 接收 onOpenLoadEntry 回调 — 点击日期可触发负荷录入 (DataEntryModal)
// Item 4: onGoToTestEntry / onCreateTestPlan / onOpenTestPlan 把日历接到现有 field-test 模块
//   (测试事件→测试录入、New Schedule→计划建立页、日历上的计划→打开对应计划)
function CalendarPage({ athletes, onOpenLoadEntry, onGoToTestEntry, onCreateTestPlan, onOpenTestPlan, onOpenProgram }) {
  const [schedules, setSchedules] = calS(() => calGet('_schedules', []));
  const [manualEvents, setManualEvents] = calS(() => calGet('_manualEvents', []));
  const [fieldPlans, setFieldPlans] = calS([]); // field-test plans (FieldDataStore) shown on the grid by date
  const [trainingPrograms, setTrainingPrograms] = calS([]); // dated training programs (FieldDataStore)
  const [visibleTypes, setVisibleTypes] = calS(() => ({
    test: true,
    training: calGet('_showTraining', true),
    match: true,
    rest: true,
    note: true,
  }));
  const [viewYear,  setViewYear]  = calS(() => new Date().getFullYear());
  const [viewMonth, setViewMonth] = calS(() => new Date().getMonth());
  const [calendarMode, setCalendarMode] = calS('month'); // month (A) | day (C)
  const [scheduleModal, setScheduleModal] = calS(null); // null | 'new' | scheduleId
  const [dayPanel,      setDayPanel]      = calS(null); // null | dateStr
  const [eventModal,    setEventModal]    = calS(null); // null | { date }
  const [scheduleManagerOpen, setScheduleManagerOpen] = calS(false);

  calE(() => calSet('_schedules',    schedules),    [schedules]);
  calE(() => calSet('_manualEvents', manualEvents), [manualEvents]);
  calE(() => calSet('_showTraining', visibleTypes.training), [visibleTypes.training]);

  // Pull field-test plans + dated training programs so they appear on the calendar by date.
  // Read-only mirror — created/edited inside their modules; re-syncs on remount.
  calE(() => {
    let alive = true;
    (async () => {
      const FS = window.FieldDataStore;
      if (!FS) return;
      try {
        const ok = await FS.healthCheck();
        if (!ok || !alive) return;
        await FS.init('default');
        const list = await FS.listPlans();
        if (alive) setFieldPlans(Array.isArray(list) ? list : []);
        if (FS.listPrograms) {
          const progs = await FS.listPrograms();
          if (alive) setTrainingPrograms((Array.isArray(progs) ? progs : []).filter(p => p.date));
        }
      } catch (e) { console.warn('calendar load failed', e); }
    })();
    return () => { alive = false; };
  }, []);

  // P3-5: ACWR color map — per date, take max ACWR across athletes (shows worst-case risk)
  const acwrByDate = calM(() => {
    const map = {};
    (athletes || []).forEach(a => {
      Object.entries(a.seasons || {}).forEach(([date, vals]) => {
        if (vals.acwr != null) {
          if (map[date] == null || vals.acwr > map[date]) map[date] = vals.acwr;
        }
      });
    });
    return map;
  }, [athletes]);

  const monthEvents = calM(() => {
    const byDate = {};
    schedules.forEach(sch => {
      expandSchedule(sch, viewYear, viewMonth).forEach(date => {
        (byDate[date] = byDate[date] || []).push({ type: 'schedule', schedule: sch, date });
      });
    });
    const mNum = String(viewMonth + 1).padStart(2, '0');
    const mStart = `${viewYear}-${mNum}-01`;
    const mEnd   = `${viewYear}-${mNum}-${String(new Date(viewYear, viewMonth + 1, 0).getDate()).padStart(2, '0')}`;
    manualEvents.forEach(ev => {
      if (ev.date >= mStart && ev.date <= mEnd)
        (byDate[ev.date] = byDate[ev.date] || []).push({ type: 'event', event: ev, date: ev.date });
    });
    fieldPlans.forEach(p => {
      if (p.date && p.date >= mStart && p.date <= mEnd)
        (byDate[p.date] = byDate[p.date] || []).push({ type: 'plan', plan: p, date: p.date });
    });
    if (visibleTypes.training) trainingPrograms.forEach(p => {
      if (p.date && p.date >= mStart && p.date <= mEnd)
        (byDate[p.date] = byDate[p.date] || []).push({ type: 'training', program: p, date: p.date });
    });
    return byDate;
  }, [schedules, manualEvents, fieldPlans, trainingPrograms, visibleTypes.training, viewYear, viewMonth]);

  const visibleMonthEvents = calM(() => {
    const byDate = {};
    Object.entries(monthEvents).forEach(([date, items]) => {
      const visible = items.filter(item => visibleTypes[calItemEventType(item)] !== false);
      if (visible.length) byDate[date] = visible;
    });
    return byDate;
  }, [monthEvents, visibleTypes]);

  const monthFacts = calM(() => {
    const items = Object.values(monthEvents).flat();
    const pending = items.filter(item => {
      if (item.date > CAL_TODAY) return false;
      if (item.type === 'schedule') {
        return !manualEvents.some(event => event.date === item.date && event.scheduleId === item.schedule.id && event.completed);
      }
      if (item.type === 'event') return !item.event.completed;
      return false;
    }).length;
    const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
    const occupiedDays = Object.keys(monthEvents).filter(date => monthEvents[date]?.length).length;
    const todayLoadRecords = (athletes || []).filter(athlete => {
      const row = athlete.seasons?.[CAL_TODAY];
      return row && Object.values(row).some(value => value != null && value !== '');
    }).length;
    return {
      today: (monthEvents[CAL_TODAY] || []).length,
      pending,
      todayLoadRecords,
      occupiedDays,
      daysInMonth,
      itemCount: items.length,
    };
  }, [monthEvents, manualEvents, athletes, viewYear, viewMonth]);

  const prevMonth = () => viewMonth === 0 ? (setViewMonth(11), setViewYear(y => y - 1)) : setViewMonth(m => m - 1);
  const nextMonth = () => viewMonth === 11 ? (setViewMonth(0), setViewYear(y => y + 1)) : setViewMonth(m => m + 1);
  const goToday = () => {
    const today = new Date();
    setViewYear(today.getFullYear());
    setViewMonth(today.getMonth());
    setCalendarMode('month');
    setDayPanel(null);
  };
  const openDay = (date) => {
    const d = new Date(date + 'T00:00:00');
    setViewYear(d.getFullYear());
    setViewMonth(d.getMonth());
    setDayPanel(date);
    setCalendarMode('day');
  };

  const openScheduleModal = (id) => setScheduleModal(id);
  const closeScheduleModal = () => setScheduleModal(null);

  const saveSchedule = (sch) => {
    setSchedules(prev => prev.find(s => s.id === sch.id) ? prev.map(s => s.id === sch.id ? sch : s) : [...prev, sch]);
    closeScheduleModal();
  };

  const markComplete = (date, scheduleId) => {
    setManualEvents(prev => {
      const idx = prev.findIndex(e => e.date === date && e.scheduleId === scheduleId);
      if (idx >= 0) {
        const next = [...prev];
        next[idx] = { ...next[idx], completed: !next[idx].completed };
        return next;
      }
      return [...prev, { id: 'ev_' + Date.now(), scheduleId, date, completed: true, notes: '', athleteIds: [] }];
    });
  };

  return (
    <main className="calendar-axis-page" data-calendar-mode={calendarMode}>
      <style>{CALENDAR_AXIS_CSS}</style>
      <section className="cal-hero">
        <div>
          <div className="cal-kicker">TESTING CALENDAR / WORKSPACE</div>
          <h1>排程与执行日历</h1>
          <p>先看什么时候发生什么，再进入训练、测试与负荷补录；所有来源保留各自真值。</p>
        </div>
        <div className="cal-actions">
          <button onClick={() => setEventModal({ date: dayPanel || CAL_TODAY })} className="btn cal-btn">＋ 添加事件</button>
          <button onClick={() => setScheduleManagerOpen(true)} className="btn cal-btn">循环排程</button>
          <button
            onClick={() => onCreateTestPlan ? onCreateTestPlan() : setScheduleModal('new')}
            className="btn primary cal-btn"
            title="新建测试计划 → 跳转测试计划建立页"
          >＋ 新建测试计划</button>
        </div>
      </section>

      <section className="cal-scope" aria-label="日历事件筛选">
        <strong>显示</strong>
        <button
          className={`cal-filter ${Object.values(visibleTypes).every(Boolean) ? 'on' : ''}`}
          onClick={() => setVisibleTypes({ test: true, training: true, match: true, rest: true, note: true })}
        >全部</button>
        {EVENT_TYPES.map(type => (
          <button
            key={type.id}
            className={`cal-filter ${visibleTypes[type.id] ? 'on' : ''}`}
            onClick={() => setVisibleTypes(current => ({ ...current, [type.id]: !current[type.id] }))}
          >{type.zh}</button>
        ))}
        <div className="cal-legend" aria-hidden="true">
          {EVENT_TYPES.slice(0, 4).map(type => (
            <span key={type.id}><i className="cal-dot" style={{ background: type.color }}/>{type.zh}</span>
          ))}
        </div>
      </section>

      <section className="cal-stats">
        <CalendarStat label="今日安排" value={monthFacts.today} detail="来自当前日历中的真实计划与事件"/>
        <CalendarStat label="待执行" value={monthFacts.pending} detail="仅统计已到期且尚未确认的项目"/>
        <CalendarStat label="今日负荷记录" value={`${monthFacts.todayLoadRecords}/${(athletes || []).length}`} detail="只描述已记录人数，不推断缺失原因"/>
        <CalendarStat label="本月安排覆盖" value={`${monthFacts.occupiedDays}/${monthFacts.daysInMonth}`} detail={`${monthFacts.itemCount} 项安排分布在当月日期中`}/>
      </section>

      {calendarMode === 'month' ? (
        <section className="cal-card">
          <header className="cal-card-head">
            <div>
              <b className="cal-month-title">{viewYear} 年 {CAL_MONTH_NAMES_ZH[viewMonth]}</b>
              <small>月历总览 · 点击具体日期进入当日执行视图</small>
            </div>
            <div className="cal-card-tools">
              <button onClick={prevMonth} className="btn cal-btn" aria-label="上一个月">‹</button>
              <button onClick={goToday} className="btn cal-btn">今天</button>
              <button onClick={nextMonth} className="btn cal-btn" aria-label="下一个月">›</button>
            </div>
          </header>
          <MonthGrid
            year={viewYear} month={viewMonth}
            monthEvents={visibleMonthEvents}
            acwrByDate={acwrByDate}
            selectedDay={dayPanel}
            onDayClick={openDay}
          />
        </section>
      ) : (
        <section className="cal-command">
          <CalendarDateRail date={dayPanel} monthEvents={visibleMonthEvents} onSelect={openDay}/>
          <div className="cal-card cal-command-main">
            <DayDetailPanel
              date={dayPanel}
              events={visibleMonthEvents[dayPanel] || []}
              athletes={athletes}
              manualEvents={manualEvents}
              onClose={() => { setCalendarMode('month'); setDayPanel(null); }}
              onAddEvent={() => setEventModal({ date: dayPanel })}
              onEditSchedule={openScheduleModal}
              onMarkComplete={(scheduleId) => markComplete(dayPanel, scheduleId)}
              onDeleteEvent={(evId) => setManualEvents(prev => prev.filter(e => e.id !== evId))}
              onOpenLoadEntry={onOpenLoadEntry ? () => onOpenLoadEntry(dayPanel) : null}
              onGoToTestEntry={onGoToTestEntry}
              onOpenTestPlan={onOpenTestPlan}
              onOpenProgram={onOpenProgram}
              acwr={acwrByDate[dayPanel] ?? null}
            />
          </div>
          <CalendarCoveragePanel
            date={dayPanel}
            athletes={athletes}
            events={monthEvents[dayPanel] || []}
            onOpenLoadEntry={onOpenLoadEntry ? () => onOpenLoadEntry(dayPanel) : null}
            onManageSchedules={() => setScheduleManagerOpen(true)}
          />
        </section>
      )}

      {/* ── Modals ── */}
      {scheduleManagerOpen && (
        <div className="cal-schedule-overlay" onClick={() => setScheduleManagerOpen(false)}>
          <div className="cal-card cal-schedule-dialog" onClick={event => event.stopPropagation()}>
            <header className="cal-card-head">
              <div><b className="cal-month-title">循环排程</b><small>管理既有周期安排，不改变训练与测试模块中的源数据</small></div>
              <button className="btn cal-btn" onClick={() => setScheduleManagerOpen(false)}>关闭</button>
            </header>
            <ScheduleListPanel
              schedules={schedules}
              athletes={athletes}
              onNew={() => { setScheduleManagerOpen(false); setScheduleModal('new'); }}
              onEdit={(id) => { setScheduleManagerOpen(false); openScheduleModal(id); }}
              onDelete={(id) => setSchedules(prev => prev.filter(s => s.id !== id))}
            />
          </div>
        </div>
      )}
      {scheduleModal && (
        <ScheduleModal
          existing={scheduleModal === 'new' ? null : schedules.find(s => s.id === scheduleModal) || null}
          athletes={athletes}
          onSave={saveSchedule}
          onClose={closeScheduleModal}
        />
      )}
      {eventModal && (
        <EventModal
          date={eventModal.date}
          athletes={athletes}
          fieldPlans={fieldPlans}
          onGoToTestEntry={onGoToTestEntry}
          onSave={(ev) => { setManualEvents(prev => [...prev, ev]); setEventModal(null); }}
          onClose={() => setEventModal(null)}
        />
      )}
    </main>
  );
}

function CalendarStat({ label, value, detail }) {
  return (
    <article className="cal-stat">
      <span>{label}</span>
      <b>{value}</b>
      <small>{detail}</small>
    </article>
  );
}

function CalendarDateRail({ date, monthEvents, onSelect }) {
  const base = new Date(date + 'T00:00:00');
  const dates = [-1, 0, 1, 2].map(offset => {
    const next = new Date(base);
    next.setDate(base.getDate() + offset);
    return toDateStr(next);
  });
  return (
    <aside className="cal-card">
      <header className="cal-card-head"><div><b>近期日期</b><small>围绕所选日期快速切换</small></div></header>
      <div className="cal-date-rail">
        {dates.map(item => {
          const d = new Date(item + 'T00:00:00');
          const count = (monthEvents[item] || []).length;
          const weekday = d.toLocaleDateString('zh-CN', { weekday: 'short' });
          return (
            <button key={item} className={`cal-date-row ${item === date ? 'on' : ''}`} onClick={() => onSelect(item)}>
              <strong>{d.getDate()}</strong>
              <span><b>{item === CAL_TODAY ? `今天 · ${weekday}` : weekday}</b><small>{count ? `${count} 项安排` : '暂无安排'}</small></span>
              <span>{item === date ? '查看中' : '—'}</span>
            </button>
          );
        })}
      </div>
    </aside>
  );
}

function CalendarCoveragePanel({ date, athletes, events, onOpenLoadEntry, onManageSchedules }) {
  const total = Math.max(1, (athletes || []).length);
  const recorded = (athletes || []).filter(athlete => {
    const row = athlete.seasons?.[date];
    return row && Object.values(row).some(value => value != null && value !== '');
  }).length;
  const testCount = events.filter(item => calItemEventType(item) === 'test').length;
  const trainingCount = events.filter(item => calItemEventType(item) === 'training').length;
  return (
    <aside className="cal-card">
      <header className="cal-card-head"><div><b>记录覆盖</b><small>描述性事实，不作风险判断</small></div></header>
      <div className="cal-coverage">
        <CalendarCoverageRow label="负荷记录" value={`${recorded} / ${(athletes || []).length}`} percent={recorded / total * 100}/>
        <CalendarCoverageRow label="测试安排" value={testCount} percent={testCount ? 100 : 0}/>
        <CalendarCoverageRow label="训练安排" value={trainingCount} percent={trainingCount ? 100 : 0}/>
      </div>
      <div style={{ display: 'grid', gap: 7, padding: 12, borderTop: '1px solid var(--border)' }}>
        {onOpenLoadEntry && <button onClick={onOpenLoadEntry} className="btn cal-btn">补录当日训练负荷</button>}
        <button onClick={onManageSchedules} className="btn cal-btn">管理循环排程</button>
      </div>
      <div className="cal-boundary"><b>数据边界。</b>日历负荷录入继续使用现有 Calendar 路径；Training session feedback 仍保持独立。</div>
    </aside>
  );
}

function CalendarCoverageRow({ label, value, percent }) {
  return (
    <div className="cal-coverage-row">
      <span><b>{label}</b><em>{value}</em></span>
      <div className="cal-bar"><i style={{ width: `${Math.max(0, Math.min(100, percent))}%` }}/></div>
    </div>
  );
}

// ── MonthGrid ──────────────────────────────────────────────────────────
function MonthGrid({ year, month, monthEvents, acwrByDate, selectedDay, onDayClick }) {
  const firstDayDow = (new Date(year, month, 1).getDay() + 6) % 7; // 0=Mon
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const mNum = String(month + 1).padStart(2, '0');

  const cellCount = Math.ceil((firstDayDow + daysInMonth) / 7) * 7;
  const cells = Array.from({ length: cellCount }, (_, i) => {
    const d = i - firstDayDow + 1;
    if (d < 1 || d > daysInMonth) return null;
    return `${year}-${mNum}-${String(d).padStart(2, '0')}`;
  });

  return (
    <div className="cal-month-grid">
      <div className="cal-weekdays">
        {CAL_DAY_NAMES_ZH.map(day => <span key={day}>{day}</span>)}
      </div>
      <div className="cal-month-cells">
        {cells.map((date, i) => (
          <DayCell
            key={i}
            date={date}
            isToday={date === CAL_TODAY}
            isSelected={!!date && date === selectedDay}
            events={date ? (monthEvents[date] || []) : []}
            acwr={date ? (acwrByDate?.[date] ?? null) : null}
            onClick={() => date && onDayClick(date)}
          />
        ))}
      </div>
    </div>
  );
}

// ── DayCell ────────────────────────────────────────────────────────────
// P3-5: ACWR color helper
function acwrDotColor(v) {
  if (v == null) return null;
  if (v > 1.5)  return 'var(--neg)';
  if (v > 1.3)  return 'var(--warn)';
  if (v >= 0.8) return 'var(--pos)';
  return 'rgba(148,163,184,.6)'; // undertrain
}

function DayCell({ date, isToday, isSelected, events, acwr, onClick }) {
  const dayNum = date ? +date.slice(8) : null;
  const dotColor = acwrDotColor(acwr);

  return (
    <button
      type="button"
      onClick={onClick}
      disabled={!date}
      className={`cal-day-cell ${!date ? 'empty' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''}`}
      aria-label={date ? `查看 ${calDateLabel(date)}` : undefined}
      style={dotColor ? { borderLeft: `3px solid ${dotColor}` } : undefined}
    >
      {date && (
        <>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <span className="cal-day-num">{dayNum}</span>
            {dotColor && acwr != null && (
              <span style={{ fontSize: 8, fontFamily: 'var(--font-mono)', color: dotColor, fontWeight: 600, opacity: 0.85 }}>{acwr.toFixed(2)}</span>
            )}
          </div>
          <div className="cal-day-events">
            {events.slice(0, 3).map((ev, i) => {
              const color = calItemColor(ev);
              const label = calItemLabel(ev);
              return (
                <span key={i} className="cal-event-pill" style={{ background: color + '18', color }}>{label}</span>
              );
            })}
            {events.length > 3 && (
              <span className="cal-more">另有 {events.length - 3} 项</span>
            )}
          </div>
        </>
      )}
    </button>
  );
}

// ── DayDetailPanel ─────────────────────────────────────────────────────
// P3-5: DayDetailPanel 接收 acwr（当天最高 ACWR）和 onOpenLoadEntry（负荷录入回调）
function DayDetailPanel({ date, events, athletes, manualEvents, onClose, onAddEvent, onEditSchedule, onMarkComplete, onDeleteEvent, onGoToTestEntry, onOpenTestPlan, onOpenProgram, acwr }) {
  const d = new Date(date + 'T00:00:00');
  const dayLabel = d.toLocaleDateString('zh-CN', { weekday: 'long', month: 'long', day: 'numeric' });
  const isPast  = date < CAL_TODAY;
  const isToday = date === CAL_TODAY;
  const dotColor = acwrDotColor(acwr);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <header className="cal-card-head">
        <div>
          <div className="cal-kicker">{isToday ? 'Today / 今天' : isPast ? 'Past / 已过去' : 'Upcoming / 待到来'}</div>
          <div className="cal-month-title" style={{ marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 }}>
            {dayLabel}
            {dotColor && acwr != null && (
              <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: dotColor, fontWeight: 600 }}>ACWR {acwr.toFixed(2)}</span>
            )}
          </div>
          <small>{events.length ? `${events.length} 项安排 · 按现有来源顺序列出` : '当日暂无安排'}</small>
        </div>
        <div className="cal-card-tools">
          <button onClick={onClose} className="btn cal-btn">← 返回月历</button>
          <button onClick={onAddEvent} className="btn cal-btn">＋ 添加事件</button>
        </div>
      </header>

      <div className="cal-day-list">
        {events.length === 0 && (
          <div className="cal-empty">
            当天没有安排。<br/>可返回月历选择其他日期，或直接添加事件。
          </div>
        )}

        {events.map((ev, i) => {
          if (ev.type === 'schedule') {
            const sch = ev.schedule;
            const completion = manualEvents.find(e => e.date === date && e.scheduleId === sch.id);
            const completed = completion?.completed || false;
            const isOverdue = isPast && !completed;
            const names = sch.athleteIds === 'all'
              ? `All athletes (${athletes.length})`
              : athletes.filter(a => (sch.athleteIds || []).includes(a.id)).map(a => a.name).join(', ') || '—';
            return (
              <div key={i} style={{ borderRadius: 5, border: `1px solid ${sch.color}44`, overflow: 'hidden' }}>
                <div style={{ borderLeft: `3px solid ${sch.color}`, padding: '9px 11px', background: sch.color + '0a' }}>
                  <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap', marginBottom: 3 }}>
                        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{sch.name}</span>
                        {completed && <CalBadge color="#10b981" label="Done"/>}
                        {isOverdue && <CalBadge color="#ef4444" label="Overdue"/>}
                      </div>
                      <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 2 }}>{names}</div>
                      {sch.notes && <div style={{ fontSize: 10.5, color: 'var(--muted-2)', fontStyle: 'italic', marginTop: 2 }}>{sch.notes}</div>}
                    </div>
                    <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
                      <button
                        onClick={() => onMarkComplete(sch.id)}
                        title={completed ? 'Mark incomplete' : 'Mark complete'}
                        style={{
                          background: completed ? '#10b98118' : 'var(--panel-hi)',
                          border: `1px solid ${completed ? '#10b98144' : 'var(--border)'}`,
                          borderRadius: 4, padding: '3px 8px', cursor: 'pointer',
                          fontSize: 12, color: completed ? '#10b981' : 'var(--muted)', fontWeight: 600,
                        }}
                      >{completed ? '✓' : '○'}</button>
                      <button
                        onClick={() => onEditSchedule(sch.id)}
                        style={{ background: 'var(--panel-hi)', border: '1px solid var(--border)', borderRadius: 4, padding: '3px 7px', cursor: 'pointer', fontSize: 10.5, color: 'var(--text-2)' }}
                      >Edit</button>
                    </div>
                  </div>
                </div>
              </div>
            );
          } else if (ev.type === 'plan') {
            // Field-test plan (from the Test Entry module) shown on the calendar by date.
            const plan = ev.plan;
            const count = plan.athleteIds === 'all' ? athletes.length : (plan.athleteIds || []).length;
            return (
              <div key={i} style={{ borderRadius: 5, border: '1px solid #3b82f644', overflow: 'hidden' }}>
                <div style={{ borderLeft: '3px solid #3b82f6', padding: '9px 11px', background: '#3b82f60a' }}>
                  <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 3 }}>
                        <CalBadge color="#3b82f6" label="Test plan"/>
                        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{plan.name || 'Test plan'}</span>
                      </div>
                      <div style={{ fontSize: 11, color: 'var(--muted)' }}>{count} athlete{count !== 1 ? 's' : ''} · {(plan.testDefIds || []).length} tests</div>
                    </div>
                    {onOpenTestPlan && (
                      <button onClick={() => onOpenTestPlan(plan.id)} style={{ background: 'var(--panel-hi)', border: '1px solid var(--border)', borderRadius: 4, padding: '3px 8px', cursor: 'pointer', fontSize: 10.5, color: 'var(--text-2)', flexShrink: 0 }}>Open →</button>
                    )}
                  </div>
                </div>
              </div>
            );
          } else if (ev.type === 'training') {
            // Training program (from the Training module) shown on the calendar by date.
            const prog = ev.program;
            const count = prog.mode === 'team' ? (prog.athleteIds || []).length : (prog.athleteId ? 1 : 0);
            const exCount = (prog.sessions || []).reduce((n, s) => n + (s.blocks || []).length, 0);
            return (
              <div key={i} style={{ borderRadius: 5, border: '1px solid #a78bfa44', overflow: 'hidden' }}>
                <div style={{ borderLeft: '3px solid #a78bfa', padding: '9px 11px', background: '#a78bfa0f' }}>
                  <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 3 }}>
                        <CalBadge color="#a78bfa" label="Training"/>
                        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{prog.name || 'Training'}</span>
                      </div>
                      <div style={{ fontSize: 11, color: 'var(--muted)' }}>{prog.mode === 'team' ? `${count} athletes` : 'Individual'} · {exCount} exercises</div>
                    </div>
                    {onOpenProgram && (
                      <button onClick={() => onOpenProgram(prog.id)} style={{ background: 'var(--panel-hi)', border: '1px solid var(--border)', borderRadius: 4, padding: '3px 8px', cursor: 'pointer', fontSize: 10.5, color: 'var(--text-2)', flexShrink: 0 }}>Open →</button>
                    )}
                  </div>
                </div>
              </div>
            );
          } else {
            const event = ev.event;
            const et = EVENT_TYPE_MAP[event.eventType] || { label: 'Event', zh: '', color: '#8b94a3' };
            const names = event.athleteIds === 'all'
              ? `All athletes (${athletes.length})`
              : athletes.filter(a => (event.athleteIds || []).includes(a.id)).map(a => a.name).join(', ') || '—';
            return (
              <div key={i} style={{ borderRadius: 5, border: `1px solid ${et.color}33`, borderLeft: `3px solid ${et.color}`, padding: '9px 11px', background: 'var(--panel)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 2 }}>
                      <CalBadge color={et.color} label={`${et.label} ${et.zh}`}/>
                      <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{event.title || event.notes || et.label}</span>
                    </div>
                    <div style={{ fontSize: 11, color: 'var(--muted)' }}>{names}</div>
                    {event.eventType === 'test' && (onOpenTestPlan || onGoToTestEntry) && (
                      <button
                        onClick={() => event.linkedPlanId && onOpenTestPlan ? onOpenTestPlan(event.linkedPlanId) : (onGoToTestEntry && onGoToTestEntry(date))}
                        style={{ marginTop: 6, background: et.color + '14', border: `1px solid ${et.color}44`, borderRadius: 4, padding: '3px 9px', cursor: 'pointer', fontSize: 10.5, color: et.color, fontWeight: 600 }}
                      >{event.linkedPlanId ? 'Open plan →' : '去测试录入 →'}</button>
                    )}
                  </div>
                  <button onClick={() => onDeleteEvent(event.id)} style={{ background: 'none', border: 0, color: 'var(--muted)', cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: 2, flexShrink: 0 }}>×</button>
                </div>
              </div>
            );
          }
        })}
      </div>

    </div>
  );
}

// ── ScheduleListPanel ──────────────────────────────────────────────────
function ScheduleListPanel({ schedules, athletes, onNew, onEdit, onDelete }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
        <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600, marginBottom: 1 }}>
          Test Schedules
        </div>
        <div style={{ fontSize: 11, color: 'var(--muted)' }}>{schedules.length} configured · click a day to inspect</div>
      </div>

      <div style={{ flex: 1, overflow: 'auto', padding: '10px 10px', display: 'flex', flexDirection: 'column', gap: 7 }}>
        {schedules.length === 0 && (
          <div style={{ textAlign: 'center', padding: '36px 16px', color: 'var(--muted)', fontSize: 12, lineHeight: 1.7 }}>
            No schedules yet.<br/>Create one to populate the calendar.
          </div>
        )}
        {schedules.map(sch => {
          const count = sch.athleteIds === 'all' ? athletes.length : (sch.athleteIds || []).length;
          return (
            <div key={sch.id} style={{ borderRadius: 5, border: '1px solid var(--border)', overflow: 'hidden' }}>
              <div style={{ borderLeft: `3px solid ${sch.color}`, padding: '8px 10px', background: 'var(--panel)' }}>
                <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 6 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 2 }}>{sch.name}</div>
                    <div style={{ fontSize: 10, color: 'var(--muted)' }}>{RECUR_LABEL[sch.recurrence] || 'Custom'} · {count} athlete{count !== 1 ? 's' : ''}</div>
                    <div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 1 }}>{sch.startDate}{sch.endDate ? ` → ${sch.endDate}` : ' → ongoing'}</div>
                  </div>
                  <div style={{ display: 'flex', gap: 3, flexShrink: 0 }}>
                    <button onClick={() => onEdit(sch.id)} style={{ background: 'var(--panel-hi)', border: '1px solid var(--border)', borderRadius: 3, padding: '2px 7px', cursor: 'pointer', fontSize: 10, color: 'var(--text-2)' }}>Edit</button>
                    <button onClick={() => onDelete(sch.id)} style={{ background: 'transparent', border: '1px solid transparent', borderRadius: 3, padding: '2px 6px', cursor: 'pointer', fontSize: 12, color: 'var(--muted)', lineHeight: 1 }}>×</button>
                  </div>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div style={{ padding: '10px 10px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
        <button onClick={onNew} className="btn" style={{ width: '100%', justifyContent: 'center', fontSize: 11 }}>
          + Recurring schedule
        </button>
      </div>
    </div>
  );
}

// ── ScheduleModal ──────────────────────────────────────────────────────
function ScheduleModal({ existing, athletes, onSave, onClose }) {
  const [form, setForm] = calS(() => existing ? { ...existing } : {
    name: '', color: CAL_COLORS[0], recurrence: 'weekly', interval: 14,
    startDate: CAL_TODAY, endDate: '', athleteIds: 'all', notes: '',
  });
  const [athleteMode, setAthleteMode] = calS(() =>
    !existing || existing.athleteIds === 'all' ? 'all' : 'specific'
  );
  const [selectedAthletes, setSelectedAthletes] = calS(() =>
    existing && existing.athleteIds !== 'all' ? (existing.athleteIds || []) : []
  );
  const [error, setError] = calS(null);

  const upd = (k) => (e) => setForm(f => ({ ...f, [k]: e.target.value }));
  const toggleAthlete = (id) => setSelectedAthletes(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);

  const submit = () => {
    if (!form.name.trim())                  { setError('Schedule name is required.'); return; }
    if (!form.startDate)                    { setError('Start date is required.'); return; }
    if (form.endDate && form.endDate < form.startDate) { setError('End date must be on or after start date.'); return; }
    if (form.recurrence === 'custom' && (+form.interval || 0) < 1) { setError('Custom interval must be ≥ 1 day.'); return; }
    if (athleteMode === 'specific' && selectedAthletes.length === 0) { setError('Select at least one athlete.'); return; }
    onSave({
      ...form,
      id: existing?.id || 'sch_' + Date.now(),
      name: form.name.trim(),
      interval: form.recurrence === 'custom' ? Math.max(1, +form.interval) : undefined,
      athleteIds: athleteMode === 'all' ? 'all' : selectedAthletes,
    });
  };

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 2000, background: 'rgba(15,18,15,.45)', backdropFilter: 'blur(6px)', display: 'grid', placeItems: 'center', padding: 20, animation: 'fade .15s ease both' }}>
      <div onClick={e => e.stopPropagation()} style={{ background: 'var(--panel)', border: '1px solid var(--border-strong)', boxShadow: '0 20px 60px rgba(15,18,15,.25)', borderRadius: 6, width: 'min(560px, 96vw)', maxHeight: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column', animation: 'fadeUp .22s ease both' }}>

        {/* Header */}
        <div style={{ padding: '13px 18px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.12em', fontWeight: 600 }}>{existing ? 'Edit Schedule' : 'New Testing Schedule'}</div>
            <h3 style={{ margin: '2px 0 0', fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>{form.name || 'Untitled schedule'}</h3>
          </div>
          <button onClick={onClose} style={{ background: 'none', border: 0, color: 'var(--muted)', fontSize: 20, cursor: 'pointer', padding: 4, lineHeight: 1 }}>×</button>
        </div>

        {/* Body */}
        <div style={{ flex: 1, overflow: 'auto', padding: '15px 18px', display: 'flex', flexDirection: 'column', gap: 13 }}>

          {/* Name + Color */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'end' }}>
            <CalField label="Schedule Name ✱">
              <input value={form.name} onChange={upd('name')} placeholder="e.g. Weekly CMJ Battery" style={calInput}/>
            </CalField>
            <CalField label="Color">
              <div style={{ display: 'flex', gap: 5, paddingBottom: 3 }}>
                {CAL_COLORS.map(c => (
                  <button key={c} onClick={() => setForm(f => ({ ...f, color: c }))} style={{
                    width: 20, height: 20, borderRadius: 999, background: c, cursor: 'pointer',
                    border: '2px solid transparent', outline: 'none',
                    boxShadow: form.color === c ? `0 0 0 2px var(--panel), 0 0 0 4px ${c}` : 'none',
                  }}/>
                ))}
              </div>
            </CalField>
          </div>

          {/* Recurrence */}
          <CalField label="Recurrence">
            <select value={form.recurrence} onChange={upd('recurrence')} style={calInput}>
              {RECUR_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </select>
          </CalField>

          {form.recurrence === 'custom' && (
            <CalField label="Interval (days) ✱">
              <input type="number" min="1" max="365" value={form.interval} onChange={upd('interval')} style={calInput}/>
            </CalField>
          )}

          {/* Date range */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            <CalField label="Start Date ✱">
              <input type="date" value={form.startDate} onChange={upd('startDate')} style={calInput}/>
            </CalField>
            <CalField label="End Date (leave blank = ongoing)">
              <input type="date" value={form.endDate} onChange={upd('endDate')} style={calInput}/>
            </CalField>
          </div>

          {/* Participants */}
          <div>
            <div style={{ fontSize: 9.5, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600, marginBottom: 6 }}>Participants</div>
            <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
              {['all', 'specific'].map(m => (
                <button key={m} onClick={() => setAthleteMode(m)} style={{
                  padding: '4px 10px', borderRadius: 4, cursor: 'pointer', fontSize: 11, fontWeight: 500,
                  background: athleteMode === m ? 'var(--accent)' : 'var(--panel-hi)',
                  color: athleteMode === m ? 'white' : 'var(--text-2)',
                  border: `1px solid ${athleteMode === m ? 'var(--accent)' : 'var(--border)'}`,
                }}>
                  {m === 'all' ? 'All athletes' : 'Specific athletes'}
                </button>
              ))}
            </div>
            {athleteMode === 'specific' && (
              <div style={{ border: '1px solid var(--border)', borderRadius: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--panel-2)' }}>
                {athletes.map((a, i) => (
                  <label key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', cursor: 'pointer', borderBottom: i < athletes.length - 1 ? '1px solid var(--border)' : 'none' }}>
                    <input type="checkbox" checked={selectedAthletes.includes(a.id)} onChange={() => toggleAthlete(a.id)} style={{ accentColor: form.color }}/>
                    <span style={{ fontSize: 12, color: 'var(--text)', flex: 1 }}>{a.name}</span>
                    <span style={{ fontSize: 10, color: 'var(--muted)' }}>{a.position} · {a.sport || ''}</span>
                  </label>
                ))}
              </div>
            )}
          </div>

          {/* Notes */}
          <CalField label="Notes (optional)">
            <textarea value={form.notes} onChange={upd('notes')} placeholder="Protocol, location, equipment, special instructions…" rows={2}
              style={{ ...calInput, resize: 'vertical', lineHeight: 1.5, fontFamily: 'var(--font-sans)' }}/>
          </CalField>

          {error && (
            <div style={{ padding: '7px 10px', borderRadius: 3, background: 'rgba(239,68,68,.08)', border: '1px solid rgba(239,68,68,.3)', color: 'var(--neg)', fontSize: 11 }}>
              {error}
            </div>
          )}
        </div>

        {/* Footer */}
        <div style={{ padding: '11px 18px', borderTop: '1px solid var(--border)', background: 'var(--panel-2)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button onClick={onClose} className="btn">Cancel</button>
          <button onClick={submit} className="btn primary">{existing ? 'Save changes' : 'Create schedule'}</button>
        </div>
      </div>
    </div>
  );
}

// ── EventModal (typed · Option B: per-type smart routing) ────────────────
function EventModal({ date, athletes, fieldPlans = [], onGoToTestEntry, onSave, onClose }) {
  const [eventType, setEventType] = calS('test');
  const [title, setTitle] = calS('');
  const [linkedPlanId, setLinkedPlanId] = calS('');
  const [athleteMode, setAthleteMode] = calS('all');
  const [selectedAthletes, setSelectedAthletes] = calS([]);
  const toggleAthlete = (id) => setSelectedAthletes(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);

  const d = new Date(date + 'T00:00:00');
  const dateLabel = d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
  const type = EVENT_TYPE_MAP[eventType] || EVENT_TYPE_MAP.test;
  // Offer same-day plans first, then the rest, as link targets for Test events.
  const planOptions = [...fieldPlans.filter(p => p.date === date), ...fieldPlans.filter(p => p.date !== date)];

  const buildEvent = () => ({
    id: 'ev_' + Date.now(),
    scheduleId: null,
    date,
    eventType,
    title: title.trim(),
    notes: title.trim(),                       // legacy field kept populated
    linkedPlanId: eventType === 'test' ? (linkedPlanId || null) : null,
    athleteIds: athleteMode === 'all' ? 'all' : selectedAthletes,
    completed: false,
  });

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 2000, background: 'rgba(15,18,15,.45)', backdropFilter: 'blur(6px)', display: 'grid', placeItems: 'center', padding: 20, animation: 'fade .15s ease both' }}>
      <div onClick={e => e.stopPropagation()} style={{ background: 'var(--panel)', border: '1px solid var(--border-strong)', boxShadow: '0 20px 60px rgba(15,18,15,.25)', borderRadius: 6, width: 'min(440px, 96vw)', display: 'flex', flexDirection: 'column', animation: 'fadeUp .22s ease both' }}>
        <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600 }}>Add Event 添加事件</div>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', marginTop: 1 }}>{dateLabel}</div>
          </div>
          <button onClick={onClose} style={{ background: 'none', border: 0, color: 'var(--muted)', fontSize: 20, cursor: 'pointer', padding: 2, lineHeight: 1 }}>×</button>
        </div>

        <div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
          {/* Type chips */}
          <div>
            <div style={{ fontSize: 9.5, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600, marginBottom: 6 }}>Type 类型</div>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {EVENT_TYPES.map(et => {
                const on = eventType === et.id;
                return (
                  <button key={et.id} onClick={() => setEventType(et.id)} style={{
                    padding: '5px 11px', borderRadius: 999, cursor: 'pointer', fontSize: 12, fontWeight: 600,
                    background: on ? et.color : 'var(--panel-hi)',
                    color: on ? '#fff' : 'var(--text-2)',
                    border: `1px solid ${on ? et.color : 'var(--border)'}`,
                    display: 'inline-flex', alignItems: 'center', gap: 6,
                  }}>
                    <span style={{ width: 7, height: 7, borderRadius: 999, background: on ? '#fff' : et.color }}/>
                    {et.label} {et.zh}
                  </button>
                );
              })}
            </div>
          </div>

          <CalField label="Title 标题">
            <input value={title} onChange={e => setTitle(e.target.value)} placeholder={
              eventType === 'test' ? 'e.g. CMJ retest, 30m sprint…'
              : eventType === 'training' ? 'e.g. Lower-body strength…'
              : eventType === 'match' ? 'e.g. vs Rivals (home)…'
              : eventType === 'rest' ? 'e.g. Recovery / off day…'
              : 'e.g. Travel, team meeting…'
            } style={calInput} autoFocus/>
          </CalField>

          {/* Per-type smart routing */}
          {eventType === 'test' && (
            <div style={{ border: `1px solid ${type.color}33`, background: type.color + '0c', borderRadius: 5, padding: '10px 11px', display: 'flex', flexDirection: 'column', gap: 9 }}>
              <CalField label="Link a test plan (optional) 关联测试计划">
                <select value={linkedPlanId} onChange={e => setLinkedPlanId(e.target.value)} style={calInput}>
                  <option value="">— None —</option>
                  {planOptions.map(p => (
                    <option key={p.id} value={p.id}>{p.name || 'Plan'}{p.date ? ` · ${p.date}` : ''}</option>
                  ))}
                </select>
              </CalField>
              {onGoToTestEntry && (
                <button
                  onClick={() => { onSave(buildEvent()); onGoToTestEntry(date); }}
                  className="btn" style={{ justifyContent: 'center', fontSize: 12, borderColor: type.color, color: type.color }}
                >去测试录入 →  Go to Test Entry</button>
              )}
            </div>
          )}
          {eventType === 'training' && (
            <div style={{ border: '1px solid var(--border)', background: 'var(--panel-2)', borderRadius: 5, padding: '10px 11px', fontSize: 11.5, color: 'var(--muted)', lineHeight: 1.6 }}>
              训练计划模块即将上线 — 暂存为占位事件,模块 5 上线后可在此关联训练计划。<br/>
              <span style={{ color: 'var(--muted-2)' }}>Training Plan module coming soon — saved as a placeholder for now.</span>
            </div>
          )}

          {/* Participants */}
          <div>
            <div style={{ fontSize: 9.5, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600, marginBottom: 6 }}>Participants 参与人</div>
            <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
              {['all', 'specific'].map(m => (
                <button key={m} onClick={() => setAthleteMode(m)} style={{
                  padding: '4px 10px', borderRadius: 4, cursor: 'pointer', fontSize: 11, fontWeight: 500,
                  background: athleteMode === m ? 'var(--accent)' : 'var(--panel-hi)',
                  color: athleteMode === m ? 'white' : 'var(--text-2)',
                  border: `1px solid ${athleteMode === m ? 'var(--accent)' : 'var(--border)'}`,
                }}>
                  {m === 'all' ? 'All athletes' : 'Specific athletes'}
                </button>
              ))}
            </div>
            {athleteMode === 'specific' && (
              <div style={{ border: '1px solid var(--border)', borderRadius: 4, maxHeight: 130, overflowY: 'auto', background: 'var(--panel-2)' }}>
                {athletes.map((a, i) => (
                  <label key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', cursor: 'pointer', borderBottom: i < athletes.length - 1 ? '1px solid var(--border)' : 'none' }}>
                    <input type="checkbox" checked={selectedAthletes.includes(a.id)} onChange={() => toggleAthlete(a.id)} style={{ accentColor: type.color }}/>
                    <span style={{ fontSize: 12, color: 'var(--text)', flex: 1 }}>{a.name}</span>
                    <span style={{ fontSize: 10, color: 'var(--muted)' }}>{a.position}</span>
                  </label>
                ))}
              </div>
            )}
          </div>
        </div>

        <div style={{ padding: '10px 16px', borderTop: '1px solid var(--border)', background: 'var(--panel-2)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button onClick={onClose} className="btn">Cancel</button>
          <button onClick={() => onSave(buildEvent())} className="btn primary">Add event</button>
        </div>
      </div>
    </div>
  );
}

// ── Shared helpers ─────────────────────────────────────────────────────
const calInput = {
  width: '100%', padding: '7px 10px',
  background: 'var(--panel)', border: '1px solid var(--border)',
  borderRadius: 3, color: 'var(--text)', font: '13px var(--font-sans)', outline: 'none',
};

function CalField({ label, children }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
      <div style={{ fontSize: 9.5, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600 }}>{label}</div>
      {children}
    </label>
  );
}

function CalBadge({ color, label }) {
  return (
    <span style={{ fontSize: 9.5, background: color + '20', color, border: `1px solid ${color}44`, borderRadius: 3, padding: '1px 5px', fontWeight: 600 }}>
      {label}
    </span>
  );
}

Object.assign(window, { CalendarPage });
