// interactions.jsx  v1  —  命令面板 · 键盘快捷键 · 比较状态栏 · Hash 路由
// 职责：无 UI 布局，纯行为层；useHashRoute / useGlobalShortcuts / CommandPalette / CompareBar
// hash routing, shortcut help. Layered on top of the existing app.jsx state.

const { useState: iuState, useEffect: iuEffect, useRef: iuRef, useMemo: iuMemo, useCallback: iuCb } = React;

// ──────────────────────────────────────────────────────────────────────────
// useHashRoute — read/write the current view+athlete to URL hash so the
// page can be bookmarked / refreshed / shared.
//   #/squad
//   #/cmj
//   #/force-compare  (legacy #/cmj-compare is still accepted)
//   #/a/<athleteId>          (individual)
// ──────────────────────────────────────────────────────────────────────────
function useHashRoute({
  athletes,
  routeTable,
  transitionContext,
  getCurrentContext,
  historyIndexKey,
  getHistoryIndex,
  consumeHistorySuppression,
  getHistorySettling,
  beginHistorySettling,
  endHistorySettling,
}) {
  const athletesRef = iuRef(athletes);
  const ignoreHashRef = iuRef('');
  const initialAppliedRef = iuRef(false);
  iuEffect(() => { athletesRef.current = athletes; }, [athletes]);
  // One history boundary handles both browser traversal and direct hash edits.
  // popstate owns a traversal; its paired hashchange is ignored once.
  iuEffect(() => {
    const apply = (event) => {
      let eventHash = window.location.hash;
      if (event?.type === 'hashchange' && event.newURL) {
        try { eventHash = new URL(event.newURL).hash; } catch (_) {}
      }
      if (event?.type === 'hashchange' && ignoreHashRef.current === eventHash) {
        ignoreHashRef.current = '';
        return;
      }
      if (event?.type === 'popstate') ignoreHashRef.current = window.location.hash;
      const stateIndex = (event?.type === 'popstate' || event?.type === 'initial')
        && Number.isInteger(event.state?.[historyIndexKey])
        ? event.state[historyIndexKey]
        : null;
      if (event?.type === 'popstate' && stateIndex == null) {
        const currentIndex = getHistoryIndex();
        const current = getCurrentContext();
        const canonical = routeTable.hashFromRoute(current.view, current.selectedId, current.season);
        const targetIndex = currentIndex - 1;
        if (!canonical || !beginHistorySettling({
          kind: 'legacy-normalize', targetIndex, releaseOnPop: true,
        })) return;
        // Compatibility cost is explicit: preserve/stamp the legacy target, then push a
        // deterministic current neighbour. pushState intentionally truncates an unknown
        // forward tail; it never overwrites the legacy target with the current route.
        history.replaceState({ ...(history.state || {}), [historyIndexKey]: targetIndex }, '');
        history.pushState({ [historyIndexKey]: currentIndex }, '', canonical);
        requestAnimationFrame(() => {
          const settling = getHistorySettling();
          if (settling?.kind === 'legacy-normalize' && settling.targetIndex === targetIndex) history.back();
        });
        return;
      }
      const settling = getHistorySettling();
      if (event?.type === 'popstate' && stateIndex != null
        && settling?.releaseOnPop === true && settling.targetIndex === stateIndex) {
        endHistorySettling();
      }
      if (stateIndex != null && consumeHistorySuppression(stateIndex)) return;
      const route = routeTable.routeFromHash(window.location.hash, athletesRef.current);
      if (!route) {
        const current = getCurrentContext();
        const canonical = routeTable.hashFromRoute(current.view, current.selectedId, current.season);
        if (canonical && canonical !== window.location.hash) {
          history.replaceState({ ...(history.state || {}), [historyIndexKey]: getHistoryIndex() }, '', canonical);
        }
        return;
      }
      transitionContext({
        view: route.view,
        ...(route.selectedId ? { selectedId: route.selectedId } : {}),
        ...(route.seasonId ? { season: route.seasonId } : {}),
      }, stateIndex == null
        ? { history: 'hash', source: 'history' }
        : { history: 'pop', source: 'history', targetIndex: stateIndex });
    };
    if (!initialAppliedRef.current) {
      initialAppliedRef.current = true;
      apply({ type: 'initial', state: history.state });
    }
    window.addEventListener('popstate', apply);
    window.addEventListener('hashchange', apply);
    return () => {
      window.removeEventListener('popstate', apply);
      window.removeEventListener('hashchange', apply);
    };
  }, [routeTable, transitionContext, getCurrentContext, historyIndexKey, getHistoryIndex, consumeHistorySuppression, getHistorySettling, beginHistorySettling, endHistorySettling]);
}

// ──────────────────────────────────────────────────────────────────────────
// CompareStatusBar — sticky banner shown only when compareMode is on.
// Tells the user how many athletes are selected, max selectable, and offers
// quick actions (view comparison / clear / exit).
// ──────────────────────────────────────────────────────────────────────────
function CompareStatusBar({ compareMode, compareIds, athletes, onClear, onExit, onJump }) {
  if (!compareMode) return null;
  const picks = athletes.filter(a => compareIds.includes(a.id));
  const max = 4;
  return (
    <div style={{
      gridColumn: '1 / -1',
      position: 'sticky', top: 0, zIndex: 50,
      background: 'var(--accent)', color: 'white',
      borderBottom: '1px solid var(--accent-2)',
      padding: '8px 16px',
      display: 'flex', alignItems: 'center', gap: 12,
      font: '500 12px/1 var(--font-sans)',
      animation: 'fadeUp .2s ease both',
    }}>
      <span style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '3px 8px', borderRadius: 3,
        background: 'rgba(255,255,255,.22)', fontSize: 10,
        letterSpacing: '.12em', textTransform: 'uppercase', fontWeight: 700,
      }}>
        <span style={{
          display: 'inline-block', width: 6, height: 6, borderRadius: 999,
          background: 'white', animation: 'pulse 1.6s infinite',
        }}/>
        Compare Mode
      </span>
      <span style={{ opacity: .9 }}>
        <span className="mono" style={{ fontWeight: 600 }}>{picks.length}</span>
        <span style={{ opacity: .65 }}> / {max} selected</span>
      </span>
      <div style={{ display: 'flex', gap: 6, alignItems: 'center', minWidth: 0, flex: 1 }}>
        {picks.map(a => (
          <span key={a.id} style={{
            display: 'inline-flex', alignItems: 'center', gap: 4,
            padding: '3px 7px', borderRadius: 3,
            background: 'rgba(255,255,255,.18)',
            fontSize: 11, whiteSpace: 'nowrap', maxWidth: 140,
            overflow: 'hidden', textOverflow: 'ellipsis',
          }}>
            {a.name}
          </span>
        ))}
        {picks.length === 0 && (
          <span style={{ opacity: .7, fontStyle: 'italic', fontSize: 11 }}>
            Click athletes in the sidebar to add them.
          </span>
        )}
      </div>
      <div style={{ display: 'flex', gap: 6 }}>
        {picks.length >= 2 && (
          <button onClick={onJump} style={cmpBtn()}>
            View comparison →
          </button>
        )}
        <button onClick={onClear} style={cmpBtn(true)}>Clear</button>
        <button onClick={onExit} style={cmpBtn(true)}>Exit</button>
      </div>
    </div>
  );
}
const cmpBtn = (ghost) => ({
  background: ghost ? 'transparent' : 'white',
  color: ghost ? 'white' : 'var(--accent)',
  border: `1px solid ${ghost ? 'rgba(255,255,255,.45)' : 'white'}`,
  borderRadius: 3, padding: '4px 10px',
  font: '600 11px/1 var(--font-sans)',
  letterSpacing: '.04em', textTransform: 'uppercase',
  cursor: 'pointer', transition: 'all .12s',
});

// ──────────────────────────────────────────────────────────────────────────
// CommandPalette — ⌘K / Ctrl+K. Searchable list of athletes + views + actions.
// ──────────────────────────────────────────────────────────────────────────
function CommandPalette({ open, onClose, athletes, onPickAthlete, onPickView, onAction }) {
  const [q, setQ] = iuState('');
  const [active, setActive] = iuState(0);
  const inputRef = iuRef(null);

  iuEffect(() => {
    if (open) {
      setQ(''); setActive(0);
      requestAnimationFrame(() => inputRef.current?.focus());
    }
  }, [open]);

  const items = iuMemo(() => {
    // Build a unified list with kind tags
    const list = [];
    // Views
    list.push({ kind: 'view', id: 'team',       icon: '◧', label: 'Squad Dashboard',    hint: 'team-wide overview' });
    list.push({ kind: 'view', id: 'individual', icon: '◉', label: 'Individual Athlete', hint: 'last selected' });
    // FORCE-WS-1a (2026-07-10): labels reworded to 测力台工作区 semantics; ids are the
    // redirect layer (window.ForceWorkspace derives 类型 + 模式 from the view id).
    list.push({ kind: 'view', id: 'cmj',        icon: '∿', label: '测力台工作区 · CMJ 采集分析', hint: '反向跳 · 力-时间上传' });
    list.push({ kind: 'view', id: 'force-compare',icon: '≋', label: '测力台工作区 · 对比（跨类型）', hint: 'CMJ · SJ · IMTP · 跨运动员' });
    list.push({ kind: 'view', id: 'sj',         icon: '△', label: '测力台工作区 · SJ 采集分析',  hint: '蹲跳 · 纯推进期' });
    list.push({ kind: 'view', id: 'imtp',       icon: '↑', label: '测力台工作区 · IMTP 采集分析', hint: '等长中段拉力 · 力量发展' });
    list.push({ kind: 'view', id: 'dsi',        icon: '⚖', label: '测力台工作区 · 剖析（DSI）',   hint: '动态力量指数 · 跨测试' });
    list.push({ kind: 'view', id: 'calendar',   icon: '▦', label: 'Testing Calendar',   hint: 'schedules · cycles · participants' });
    list.push({ kind: 'view', id: 'report',     icon: '▤', label: 'Individual Report Editor', hint: 'compose athlete-facing report' });
    // Actions
    list.push({ kind: 'action', id: 'compare-toggle', icon: '⇄', label: 'Toggle Compare Mode', hint: 'multi-select athletes' });
    list.push({ kind: 'action', id: 'add-athlete',    icon: '+', label: 'Add Athlete',       hint: 'sport + group + position' });
    list.push({ kind: 'action', id: 'open-settings', icon: '⚙', label: 'Settings: Metrics', hint: 'opens the Settings metrics tab' });
    list.push({ kind: 'action', id: 'open-entry',    icon: '✎', label: 'Quick Data Entry', hint: 'log new measurements' });
    list.push({ kind: 'action', id: 'open-export',   icon: '↓', label: 'Export / Print', hint: 'PDF / PNG output' });
    list.push({ kind: 'action', id: 'toggle-vibe',   icon: '◐', label: 'Cycle Vibe',      hint: 'subtle → mid → bold' });
    list.push({ kind: 'action', id: 'shortcuts',     icon: '?', label: 'Keyboard Shortcuts', hint: 'press ?' });
    // Athletes
    athletes.forEach(a => {
      list.push({
        kind: 'athlete', id: a.id, icon: '#' + String(a.jersey).padStart(2,'0'),
        label: a.name, hint: `${a.position} · ${a.country}`,
        athlete: a,
      });
    });
    return list;
  }, [athletes]);

  const filtered = iuMemo(() => {
    const term = q.trim().toLowerCase();
    if (!term) return items;
    return items.filter(it =>
      it.label.toLowerCase().includes(term) ||
      (it.hint && it.hint.toLowerCase().includes(term))
    );
  }, [q, items]);

  // clamp active
  iuEffect(() => { if (active >= filtered.length) setActive(Math.max(0, filtered.length - 1)); }, [filtered.length]);

  const choose = (it) => {
    if (!it) return;
    if (it.kind === 'athlete') onPickAthlete(it.id);
    else if (it.kind === 'view') onPickView(it.id);
    else if (it.kind === 'action') onAction(it.id);
    onClose();
  };

  const keyDown = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setActive(i => Math.min(i + 1, filtered.length - 1)); }
    else if (e.key === 'ArrowUp')   { e.preventDefault(); setActive(i => Math.max(i - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); choose(filtered[active]); }
    else if (e.key === 'Escape') { e.preventDefault(); onClose(); }
  };

  if (!open) return null;
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 2000,
      background: 'rgba(15,18,15,.45)',
      backdropFilter: 'blur(6px)',
      display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
      paddingTop: '12vh', animation: 'fade .15s ease both',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 'min(640px, 92vw)',
        background: 'var(--panel)',
        border: '1px solid var(--border-strong)',
        boxShadow: '0 20px 60px rgba(15,18,15,.25)',
        borderRadius: 4, overflow: 'hidden',
        animation: 'fadeUp .2s ease both',
      }}>
        {/* input */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', borderBottom: '1px solid var(--border)' }}>
          <span style={{ fontSize: 18, color: 'var(--muted)' }}>⌕</span>
          <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={keyDown}
            placeholder="Jump to athlete, view, or action…"
            style={{
              flex: 1, background: 'transparent', border: 0, outline: 'none',
              font: '15px var(--font-sans)', color: 'var(--text)',
            }}/>
          <kbd style={kbdStyle}>ESC</kbd>
        </div>
        {/* results */}
        <div style={{ maxHeight: '52vh', overflowY: 'auto', padding: '6px 0' }}>
          {filtered.length === 0 && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>
              No matches for &ldquo;{q}&rdquo;
            </div>
          )}
          {(() => {
            // Group by kind, with section labels
            const order = ['view', 'athlete', 'action'];
            const labels = { view: 'Views', athlete: 'Athletes', action: 'Actions' };
            const groups = order.map(k => [k, filtered.filter(it => it.kind === k)]).filter(([, arr]) => arr.length);
            return groups.map(([k, arr]) => (
              <div key={k}>
                <div style={{
                  padding: '8px 16px 4px',
                  fontSize: 10, color: 'var(--muted)',
                  letterSpacing: '.12em', textTransform: 'uppercase', fontWeight: 600,
                }}>{labels[k]}</div>
                {arr.map((it) => {
                  const idx = filtered.indexOf(it);
                  const isActive = idx === active;
                  return (
                    <div key={k + ':' + it.id}
                      data-command-item-kind={it.kind}
                      data-command-item-id={it.id}
                      onMouseEnter={() => setActive(idx)}
                      onClick={() => choose(it)}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 12,
                        padding: '8px 16px',
                        background: isActive ? 'var(--accent-soft)' : 'transparent',
                        borderLeft: `3px solid ${isActive ? 'var(--accent)' : 'transparent'}`,
                        cursor: 'pointer',
                        transition: 'background .06s',
                      }}>
                      <span className="mono" style={{
                        width: 28, textAlign: 'center', fontSize: 13,
                        color: isActive ? 'var(--accent)' : 'var(--muted)',
                      }}>{it.icon}</span>
                      <span style={{ flex: 1, fontSize: 13, color: 'var(--text)' }}>{it.label}</span>
                      {it.hint && (
                        <span style={{ fontSize: 11, color: 'var(--muted)' }}>{it.hint}</span>
                      )}
                    </div>
                  );
                })}
              </div>
            ));
          })()}
        </div>
        <div style={{
          padding: '8px 16px', borderTop: '1px solid var(--border)',
          background: 'var(--panel-2)',
          display: 'flex', gap: 16, fontSize: 10, color: 'var(--muted)',
          letterSpacing: '.06em',
        }}>
          <span><kbd style={kbdStyle}>↑↓</kbd> navigate</span>
          <span><kbd style={kbdStyle}>↵</kbd> select</span>
          <span><kbd style={kbdStyle}>esc</kbd> dismiss</span>
          <span style={{ marginLeft: 'auto' }}>{filtered.length} result{filtered.length === 1 ? '' : 's'}</span>
        </div>
      </div>
    </div>
  );
}

const kbdStyle = {
  display: 'inline-block', padding: '2px 6px',
  background: 'var(--panel-2)', border: '1px solid var(--border)',
  borderRadius: 3, font: '500 10px var(--font-mono)',
  color: 'var(--text-2)', letterSpacing: '.04em',
};

// ──────────────────────────────────────────────────────────────────────────
// ShortcutHelp — modal triggered by ? showing all bindings
// ──────────────────────────────────────────────────────────────────────────
function ShortcutHelp({ open, onClose }) {
  if (!open) return null;
  const rows = [
    ['Navigation', null],
    ['⌘K  /  Ctrl+K', 'Open command palette'],
    ['/',            'Focus athlete search'],
    ['j  /  k',      'Next / previous athlete'],
    ['g s',          'Go to Squad Dashboard'],
    ['g i',          'Go to Individual view'],
    ['g c',          'Go to CMJ Analysis'],
    ['g x',          'Go to CMJ Compare'],
    ['g l',          'Go to Testing Calendar'],
    ['Modes', null],
    ['c',            'Toggle compare mode'],
    ['e',            'Open data entry'],
    ['s',            'Open settings'],
    ['p',            'Open export / print'],
    ['v',            'Cycle vibe (subtle/mid/bold)'],
    ['Misc', null],
    ['?',            'This help'],
    ['Esc',          'Close any overlay'],
  ];
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 2000,
      background: 'rgba(15,18,15,.45)', backdropFilter: 'blur(6px)',
      display: 'grid', placeItems: 'center',
      animation: 'fade .15s ease both',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 'min(480px, 92vw)',
        background: 'var(--panel)',
        border: '1px solid var(--border-strong)',
        boxShadow: '0 20px 60px rgba(15,18,15,.25)',
        borderRadius: 4, overflow: 'hidden',
        animation: 'fadeUp .22s ease both',
      }}>
        <div style={{
          padding: '14px 16px', borderBottom: '1px solid var(--border)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        }}>
          <div>
            <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Keyboard Shortcuts</div>
            <div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 2 }}>Press <kbd style={kbdStyle}>?</kbd> any time</div>
          </div>
          <button onClick={onClose} style={{
            background: 'transparent', border: 0, color: 'var(--muted)',
            fontSize: 18, cursor: 'pointer', padding: 4,
          }}>×</button>
        </div>
        <div style={{ padding: '4px 0 12px' }}>
          {rows.map(([k, v], i) => {
            if (v === null) {
              return <div key={i} style={{
                padding: '10px 16px 4px',
                fontSize: 10, color: 'var(--muted)',
                letterSpacing: '.12em', textTransform: 'uppercase', fontWeight: 600,
              }}>{k}</div>;
            }
            return (
              <div key={i} style={{
                display: 'grid', gridTemplateColumns: '160px 1fr',
                gap: 12, padding: '5px 16px', alignItems: 'center',
              }}>
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                  {k.split(/\s+/).map((part, j) => (
                    <kbd key={j} style={kbdStyle}>{part}</kbd>
                  ))}
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-2)' }}>{v}</div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────────────────
// useGlobalShortcuts — wires up every keybinding above.
// `gMode` implements two-key sequences like "g s".
// ──────────────────────────────────────────────────────────────────────────
function useGlobalShortcuts(handlers) {
  const gModeRef = iuRef(false);
  const gTimerRef = iuRef(0);

  iuEffect(() => {
    const isTyping = (el) => {
      if (!el) return false;
      const t = (el.tagName || '').toLowerCase();
      if (t === 'input' || t === 'textarea' || t === 'select') return true;
      if (el.isContentEditable) return true;
      return false;
    };

    const onKey = (e) => {
      // Always allow ⌘K / Ctrl+K
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        handlers.openCommandPalette();
        return;
      }
      // Don't intercept while typing
      if (isTyping(document.activeElement)) {
        if (e.key === 'Escape') handlers.closeAllOverlays();
        return;
      }
      // ignore modifier-only or with shift+letter (allow plain ?)
      if (e.altKey || e.metaKey || e.ctrlKey) return;

      const k = e.key;

      // Two-key "g <view>" sequences
      if (gModeRef.current) {
        const map = { s: 'team', i: 'individual', c: 'cmj', x: 'force-compare', l: 'calendar' };
        if (map[k]) {
          e.preventDefault();
          handlers.setView(map[k]);
        }
        gModeRef.current = false;
        clearTimeout(gTimerRef.current);
        return;
      }

      if (k === 'g') {
        gModeRef.current = true;
        clearTimeout(gTimerRef.current);
        gTimerRef.current = setTimeout(() => { gModeRef.current = false; }, 1200);
        return;
      }

      switch (k) {
        case '/':  e.preventDefault(); handlers.focusSearch(); break;
        case 'j':  e.preventDefault(); handlers.nextAthlete(); break;
        case 'k':  e.preventDefault(); handlers.prevAthlete(); break;
        case 'c':  e.preventDefault(); handlers.toggleCompare(); break;
        case 'e':  e.preventDefault(); handlers.openEntry(); break;
        case 's':  e.preventDefault(); handlers.openSettings(); break;
        case 'p':  e.preventDefault(); handlers.openExport(); break;
        case 'v':  e.preventDefault(); handlers.cycleVibe(); break;
        case '?':  e.preventDefault(); handlers.openShortcuts(); break;
        case 'Escape': handlers.closeAllOverlays(); break;
        default: break;
      }
    };

    window.addEventListener('keydown', onKey);
    return () => {
      window.removeEventListener('keydown', onKey);
      clearTimeout(gTimerRef.current);
    };
  }, [handlers]);
}

// ──────────────────────────────────────────────────────────────────────────
// SkeletonShim — gentle shimmering box, used while data resolves.
// ──────────────────────────────────────────────────────────────────────────
function Skeleton({ w = '100%', h = 16, r = 3, style }) {
  return (
    <span aria-hidden="true" style={{
      display: 'block', width: w, height: h, borderRadius: r,
      background: 'linear-gradient(90deg, var(--panel-2) 0%, var(--panel-hi) 50%, var(--panel-2) 100%)',
      backgroundSize: '200% 100%',
      animation: 'shimmer 1.5s linear infinite',
      ...style,
    }}/>
  );
}

// ──────────────────────────────────────────────────────────────────────────
// FirstPaintBoot — overlays a brief skeleton on first paint so the user
// sees structure before React mounts charts.
// ──────────────────────────────────────────────────────────────────────────
function FirstPaintBoot({ on }) {
  if (!on) return null;
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 1500,
      background: 'var(--bg)',
      display: 'grid', placeItems: 'center',
      animation: 'fade .25s ease both',
      pointerEvents: 'none',
    }}>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
        <Skeleton w={48} h={48} r={8}/>
        <Skeleton w={180} h={10}/>
        <Skeleton w={120} h={10}/>
      </div>
    </div>
  );
}

Object.assign(window, {
  useHashRoute, CompareStatusBar, CommandPalette,
  ShortcutHelp, useGlobalShortcuts, Skeleton, FirstPaintBoot,
  AddAthleteModal,
});

// ──────────────────────────────────────────────────────────────────────────
// AddAthleteModal — quick athlete creation with sport / group / position
// data captured at creation time. Free-text inputs with datalist auto-suggest
// from existing roster values, so the first time a coach types "Soccer" it
// becomes a future suggestion. No need to manage a separate "Groups" table.
// ──────────────────────────────────────────────────────────────────────────
function AddAthleteModal({ open, onClose, onAdd, athletes, seasons, defaultSport, defaultGroup }) {
  const [form, setForm] = iuState({
    name: '', sport: defaultSport || 'Basketball', group: defaultGroup || '',
    position: '', jersey: '', country: '', height: '', weight: '',
    gender: '', birthDate: '', age: '', dominant: 'Right',
  });
  const [error, setError] = iuState(null);
  const [extraCount, setExtraCount] = iuState(1); // count of "blank rows" added in this batch
  const firstFieldRef = iuRef(null);

  iuEffect(() => {
    if (open) {
      setForm(f => ({ ...f, name: '', position: '', jersey: '', country: '', gender: '', birthDate: '', age: '', height: '', weight: '' }));
      setError(null);
      requestAnimationFrame(() => firstFieldRef.current?.focus());
    }
  }, [open]);

  // Pull existing values for autocomplete suggestions
  const sportOpts    = iuMemo(() => Array.from(new Set(athletes.map(a => a.sport).filter(Boolean))), [athletes]);
  const groupOpts    = iuMemo(() => Array.from(new Set(athletes.map(a => a.group).filter(Boolean))), [athletes]);
  const positionOpts = iuMemo(() => {
    const pool = form.sport
      ? athletes.filter(a => (a.sport || '').toLowerCase() === form.sport.toLowerCase())
      : athletes;
    return Array.from(new Set(pool.map(a => a.position).filter(Boolean)));
  }, [athletes, form.sport]);

  if (!open) return null;

  const update = (k) => (e) => setForm(f => ({ ...f, [k]: e.target.value }));

  const validate = () => {
    const demographic = window.AthleteProfile?.validateRequiredDemographics(form);
    if (!demographic?.ok) return demographic?.message || '姓名、性别以及出生日期或年龄为必填项。';
    if (!form.sport.trim()) return 'Sport is required — choose or type one (Basketball, Soccer, …).';
    if (!form.group.trim()) return 'Group is required — choose or type one (First Team, U21, Recovery, …).';
    if (!form.position.trim()) return 'Position is required.';
    if (form.jersey && isNaN(+form.jersey)) return 'Jersey must be a number.';
    if (form.height && isNaN(+form.height)) return 'Height must be a number.';
    if (form.weight && isNaN(+form.weight)) return 'Weight must be a number.';
    if (form.age && isNaN(+form.age)) return 'Age must be a number.';
    // Duplicate jersey within same group?
    if (form.jersey) {
      const dup = athletes.find(a =>
        +a.jersey === +form.jersey &&
        (a.sport || '').toLowerCase() === form.sport.trim().toLowerCase() &&
        (a.group || '').toLowerCase() === form.group.trim().toLowerCase()
      );
      if (dup) return `Jersey #${form.jersey} already used by ${dup.name} in ${form.group}.`;
    }
    return null;
  };

  const submit = (closeAfter) => {
    const err = validate();
    if (err) { setError(err); return; }
    const demographic = window.AthleteProfile.validateRequiredDemographics(form).profile;
    const id = 'a' + (athletes.reduce((m, a) => {
      const n = parseInt((a.id || '').replace(/^a/, ''), 10);
      return isNaN(n) ? m : Math.max(m, n);
    }, 0) + 1);
    const newAthlete = {
      id,
      name: demographic.name,
      gender: demographic.gender,
      ...(demographic.birthDate ? { birthDate: demographic.birthDate } : {}),
      sport: form.sport.trim(),
      group: form.group.trim(),
      position: form.position.trim(),
      jersey: form.jersey ? +form.jersey : 0,
      country: form.country.trim() || '—',
      age: demographic.age,
      dominant: form.dominant || 'Right',
      height: form.height ? +form.height : null,
      weight: form.weight ? +form.weight : null,
      seasons: Object.fromEntries((seasons || []).map(s => [s, {}])),
    };
    onAdd(newAthlete);
    setExtraCount(c => c + 1);
    if (closeAfter) onClose();
    else {
      // Reset for next entry; preserve sport+group so batch-adding to one team is fast
      setForm(f => ({
        ...f,
        name: '', jersey: '', position: '', country: '', gender: '', birthDate: '', age: '', height: '', weight: '',
      }));
      requestAnimationFrame(() => firstFieldRef.current?.focus());
    }
  };

  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: 4, width: 'min(620px, 96vw)',
        maxHeight: '92vh', overflow: 'hidden',
        display: 'flex', flexDirection: 'column',
        animation: 'fadeUp .22s ease both',
      }}>
        <div style={{
          padding: '14px 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 }}>
              Add Athlete · {extraCount > 1 ? `${extraCount - 1} added this session` : 'Personnel'}
            </div>
            <h3 style={{ margin: '2px 0 0', fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>New athlete</h3>
          </div>
          <button onClick={onClose} style={{
            background: 'transparent', border: 0, color: 'var(--muted)',
            fontSize: 20, cursor: 'pointer', padding: 4, lineHeight: 1,
          }}>×</button>
        </div>

        <div style={{ padding: '16px 18px', overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 14 }}>
          {/* Sport / Group / Position — the three classification axes */}
          <div style={{
            display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10,
            padding: '12px 14px',
            background: 'var(--panel-2)', borderRadius: 4,
            border: '1px dashed var(--border-strong)',
          }}>
            <Field label="Sport ✱" hint="e.g. Basketball, Soccer" >
              <input value={form.sport} onChange={update('sport')} list="aa-sports"
                placeholder="Basketball" style={inputStyle}/>
              <datalist id="aa-sports">{sportOpts.map(s => <option key={s} value={s}/>)}</datalist>
            </Field>
            <Field label="Group / Team ✱" hint="First Team · U21 · Recovery">
              <input value={form.group} onChange={update('group')} list="aa-groups"
                placeholder="First Team" style={inputStyle}/>
              <datalist id="aa-groups">{groupOpts.map(g => <option key={g} value={g}/>)}</datalist>
            </Field>
            <Field label="Position ✱" hint={form.sport ? `${form.sport} positions` : ''}>
              <input value={form.position} onChange={update('position')} list="aa-positions"
                placeholder="Forward" style={inputStyle}/>
              <datalist id="aa-positions">{positionOpts.map(p => <option key={p} value={p}/>)}</datalist>
            </Field>
          </div>

          {/* Identity */}
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: 10 }}>
            <Field label="Name ✱">
              <input ref={firstFieldRef} value={form.name} onChange={update('name')}
                placeholder="Full name" style={inputStyle}/>
            </Field>
            <Field label="Gender / 性别 ✱">
              <select value={form.gender} onChange={update('gender')} style={inputStyle}>
                <option value="">请选择</option>
                <option value="Female">Female / 女</option>
                <option value="Male">Male / 男</option>
                <option value="Other">Other / 其他</option>
                <option value="Unspecified">Prefer not to say / 未说明</option>
              </select>
            </Field>
            <Field label="Jersey #" hint="unique within group">
              <input value={form.jersey} onChange={update('jersey')}
                placeholder="11" inputMode="numeric" style={inputStyle}/>
            </Field>
          </div>

          {/* Anthropometrics */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }}>
            <Field label="Birth date / 出生日期" hint="与年龄二选一">
              <input type="date" value={form.birthDate} max={new Date().toISOString().slice(0, 10)}
                onChange={update('birthDate')} style={inputStyle}/>
            </Field>
            <Field label="Age / 年龄" hint={form.birthDate ? '按出生日期自动计算' : '与出生日期二选一'}>
              <input
                value={form.birthDate ? (window.AthleteProfile?.calculateAge(form.birthDate)?.toFixed(1) || '') : form.age}
                onChange={update('age')}
                placeholder="24.0"
                inputMode="decimal"
                readOnly={!!form.birthDate}
                style={{ ...inputStyle, background: form.birthDate ? 'var(--panel-2)' : 'var(--panel)' }}
              />
            </Field>
            <Field label="Height (cm)">
              <input value={form.height} onChange={update('height')} placeholder="198" style={inputStyle}/>
            </Field>
            <Field label="Weight (kg)">
              <input value={form.weight} onChange={update('weight')} placeholder="96" style={inputStyle}/>
            </Field>
            <Field label="Dominant">
              <select value={form.dominant} onChange={update('dominant')} style={inputStyle}>
                <option>Right</option><option>Left</option><option>Both</option>
              </select>
            </Field>
          </div>

          <Field label="Country / Region">
            <input value={form.country} onChange={update('country')} placeholder="USA" style={inputStyle}/>
          </Field>

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

        <div style={{
          padding: '12px 18px', borderTop: '1px solid var(--border)',
          background: 'var(--panel-2)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
        }}>
          <div style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '.04em' }}>
            Tip: "Save & add another" keeps Sport + Group so you can batch-import a roster.
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={onClose} className="btn">Cancel</button>
            <button onClick={() => submit(false)} className="btn">Save &amp; add another</button>
            <button onClick={() => submit(true)} className="btn primary">Save</button>
          </div>
        </div>
      </div>
    </div>
  );
}

const inputStyle = {
  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',
};

const Field = ({ label, hint, children }) => (
  <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}{hint && <span style={{ marginLeft: 6, fontWeight: 400, letterSpacing: 0, textTransform: 'none', color: 'var(--muted-2)' }}>{hint}</span>}
    </div>
    {children}
  </label>
);
