/* global React, Card, Button, Segmented, Pill, Eyebrow */
// ============================================================================
// parents.jsx — the Family tab, parents only (_spec-app-v2.md "Parents (admin)").
//   board   — three-day release board per boy + "Needs you" (step-ups, flags, pain)
//   week    — family week: both boys side by side, who has to be where and when
//   signals — four weeks of check-in trends, load vs limit, the monthly four-signal check
// Release model (_spec-season1-app.md): day +2 and beyond is draft, tomorrow releases
// at 18:00 unless flagged, today is locked. Safety rules never wait for approval.
// Data only through window.PlanAPI; every write reloads.
// ============================================================================
(() => {
  const { useState, useEffect, useCallback } = React;
  const API = () => window.PlanAPI;

  const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  const DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  const hm = (t) => (t ? String(t).slice(0, 5) : '');
  const span = (a, b) => (a ? (b ? `${hm(a)}–${hm(b)}` : hm(a)) : '');
  const dateObj = (s) => new Date(s + 'T12:00:00');
  const wd = (s) => (dateObj(s).getDay() + 6) % 7;
  const dayLabel = (s) => { const d = dateObj(s); return `${DAYS[wd(s)]} ${d.getDate()} ${d.toLocaleString('en-GB', { month: 'short' })}`; };
  const shortDate = (s) => { const d = dateObj(s); return `${DAYS_SHORT[wd(s)]} ${d.getDate()} ${d.toLocaleString('en-GB', { month: 'short' })}`; };
  const minutes = (t) => { if (!t) return null; const [h, m] = String(t).split(':').map(Number); return h * 60 + (m || 0); };
  const fmt1 = (n) => (n == null ? '–' : (Math.round(n * 10) / 10).toString());
  const errMsg = (e) => String((e && e.message) || e || 'Unknown error');

  const wrap = { maxWidth: 900, margin: '0 auto', padding: '16px 16px 120px', fontFamily: 'var(--font-sans)', boxSizing: 'border-box' };
  const linkBtn = { background: 'none', border: 'none', color: 'var(--signal)', cursor: 'pointer', fontSize: 13, padding: 0, fontFamily: 'var(--font-sans)' };
  const muted = { color: 'var(--fg-muted)' };

  // ------------------------------------------------------------------ shared bits
  function ErrorNote({ children }) {
    if (!children) return null;
    return (
      <div style={{ border: '1px solid var(--danger)', background: 'var(--danger-tint)', color: 'var(--fg)', borderRadius: 8,
                    padding: '8px 12px', fontSize: 13, margin: '8px 0', overflowWrap: 'anywhere' }}>{children}</div>
    );
  }
  function Loading() { return <div style={{ ...muted, padding: 24, textAlign: 'center', fontSize: 14 }}>Loading…</div>; }
  function H({ children, sub }) {
    return (
      <div style={{ margin: '22px 0 10px' }}>
        <div style={{ fontSize: 17, fontWeight: 500, letterSpacing: '-0.01em' }}>{children}</div>
        {sub && <div style={{ fontSize: 13, ...muted, marginTop: 2 }}>{sub}</div>}
      </div>
    );
  }
  const grid = (min) => ({ display: 'grid', gridTemplateColumns: `repeat(auto-fit, minmax(${min}px, 1fr))`, gap: 10 });

  // ==================================================================== screen
  function ParentsScreen({ isAdmin, go, sub }) {
    if (!isAdmin) {
      return <div style={{ ...wrap, textAlign: 'center', paddingTop: 48, ...muted }}>Parents only.</div>;
    }
    const view = ['board', 'week', 'signals'].includes(sub) ? sub : 'board';
    return (
      <div style={wrap}>
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 6 }}>
          <Segmented value={view} onChange={(x) => go('family', x)}
            options={[{ value: 'board', label: 'Board' }, { value: 'week', label: 'Family week' }, { value: 'signals', label: 'Signals' }]} />
        </div>
        {view === 'board' && <Board />}
        {view === 'week' && <FamilyWeek />}
        {view === 'signals' && <Signals />}
      </div>
    );
  }

  // ==================================================================== BOARD
  // The planner's evening brief (supabase/functions/planner): written at 17:00, an hour before release.
  function PlannerBrief() {
    const [note, setNote] = useState(null);
    useEffect(() => { API().getPlannerNote().then(setNote).catch(() => {}); }, []);
    if (!note) return null;
    const when = new Date(note.created_at).toLocaleString('en-GB', { weekday: 'short', hour: '2-digit', minute: '2-digit' });
    return (
      <Card pad={14} style={{ marginTop: 8 }}>
        <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--fg-subtle)' }}>
          Planner · {when}
        </div>
        <div style={{ fontSize: 15, fontWeight: 500, marginTop: 6 }}>{note.title}</div>
        <div style={{ fontSize: 14, marginTop: 6, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{note.body}</div>
      </Card>
    );
  }

  // Parents-only notes and anything left undone, per boy (026)
  function BoardNotes() {
    const [rows, setRows] = useState(null);
    useEffect(() => { API().getBoardNotes().then(setRows).catch(() => setRows([])); }, []);
    if (!rows || !rows.some(r => r.private.length || r.overdue.length)) return null;
    const NL = window.NoteList;
    return (
      <Card pad={14} style={{ marginTop: 8 }}>
        <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--fg-subtle)' }}>Notes</div>
        {rows.filter(r => r.private.length || r.overdue.length).map(r => (
          <div key={r.kid.db_id} style={{ marginTop: 8 }}>
            <div style={{ fontSize: 14, fontWeight: 500 }}>{r.kid.name}</div>
            {r.private.length > 0 && <div style={{ fontSize: 12, ...muted, marginTop: 4 }}>🔒 Parents only</div>}
            {NL && <NL notes={r.private.map(n => ({ ...n, text: `${shortDate(n.day)}: ${n.text}` }))} canWrite={false} />}
            {r.overdue.length > 0 && <div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 4 }}>Not done yet</div>}
            {NL && <NL notes={r.overdue.map(n => ({ ...n, carriedFrom: n.day }))} canWrite={false} />}
          </div>
        ))}
      </Card>
    );
  }

  function Board() {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);
    const [busy, setBusy] = useState(false);

    const load = useCallback(async () => {
      try { setData(await API().getBoard(3)); setError(null); }
      catch (e) { setError(errMsg(e)); }
    }, []);
    useEffect(() => { load(); }, [load]);

    // Every write goes through here: one at a time, error shown, then reload.
    const act = useCallback(async (fn) => {
      if (busy) return false;
      setBusy(true);
      try { await fn(); setError(null); return true; }
      catch (e) { setError(errMsg(e)); return false; }
      finally { setBusy(false); await load(); }
    }, [busy, load]);

    if (!data) return <>{error ? <ErrorNote>Could not load the board: {error}</ErrorNote> : <Loading />}</>;
    const today = API().today();

    return (
      <>
        <div style={{ fontSize: 13, ...muted, textAlign: 'center', margin: '8px 0 4px' }}>
          Tomorrow releases at 18:00 unless flagged. Pain and load rules act immediately.
        </div>
        <ErrorNote>{error}</ErrorNote>
        <PlannerBrief />
        <BoardNotes />
        <NeedsYou data={data} act={act} busy={busy} />
        {data.kids.length === 0 && <Card pad={16} style={{ ...muted, textAlign: 'center', marginTop: 12 }}>No boys found in this family.</Card>}
        {data.kids.map(k => (
          <div key={k.db_id}>
            <H>{k.name}</H>
            <div style={grid(250)}>
              {k.days.map((d, n) => <DayCard key={d.day} d={d} n={n} today={today} act={act} busy={busy} />)}
            </div>
          </div>
        ))}
      </>
    );
  }

  function NeedsYou({ data, act, busy }) {
    const [propErr, setPropErr] = useState({});
    const [notes, setNotes] = useState({});
    const flagged = [];
    data.kids.forEach(k => k.days.forEach(d => { if (d.planDay && d.planDay.flagged) flagged.push({ k, d }); }));
    const pains = [];
    data.kids.forEach(k => (k.checkins || []).forEach(c => { if (c.pain) pains.push({ k, c }); }));
    const count = data.proposals.length + flagged.length + pains.length;

    async function decide(p, approve) {
      let msg = null;
      await act(async () => {
        try { await API().decideStep(p.id, approve, (notes[p.id] || '').trim() || null); }
        catch (e) { msg = errMsg(e).replace(/^decide step:\s*/, ''); }
      });
      setPropErr(s => ({ ...s, [p.id]: msg }));
    }

    return (
      <>
        <H sub={count === 0 ? 'Nothing waiting. The plan runs on its own.' : null}>Needs you{count ? ` · ${count}` : ''}</H>
        {count > 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {pains.map(({ k, c }) => (
              <Card key={'p' + c.id} pad={12} style={{ borderLeft: '3px solid var(--danger)' }}>
                <Eyebrow color="var(--danger)">Pain · {k.name} · {shortDate(c.day)}</Eyebrow>
                <div style={{ fontSize: 14, marginTop: 4 }}>Hurts: {c.pain_where || 'not said where'}. Physical work is paused by rule — check in with him.</div>
                <div style={{ fontSize: 12, ...muted, marginTop: 2 }}>Pain-free tomorrow? Clear the flag on that day and the paused items come back.</div>
                <div style={{ marginTop: 8 }}>
                  <Button size="sm" kind="secondary" disabled={busy} onClick={() => act(() => API().markSeen(c.id))}>Seen, talked to him</Button>
                </div>
              </Card>
            ))}
            {data.proposals.map(p => (
              <Card key={'s' + p.id} pad={12} style={{ borderLeft: '3px solid var(--signal)' }}>
                <Eyebrow>Step-up · {p.kid?.name || 'Unknown'}</Eyebrow>
                <div style={{ fontSize: 15, fontWeight: 500, marginTop: 4 }}>
                  {p.ladder?.name || p.progression_id}: step {p.from_step} → {p.to_step}
                  {p.ladder?.stepName ? <span style={{ fontWeight: 400, ...muted }}> ({p.ladder.stepName})</span> : null}
                </div>
                <div style={{ fontSize: 13, ...muted, marginTop: 2 }}>
                  {p.evidence_value != null ? `Evidence: ${p.evidence_value}` : 'No result logged'}
                  {p.ladder?.passResult ? ` · pass line for the new step: ${p.ladder.passResult}` : ''}
                </div>
                {p.note && <div style={{ fontSize: 13, marginTop: 4 }}>{p.note}</div>}
                <input value={notes[p.id] || ''} onChange={e => setNotes(s => ({ ...s, [p.id]: e.target.value }))}
                  placeholder="Note (optional): why, or what to fix first" style={{ ...inputStyle, width: '100%', marginTop: 10 }} />
                <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
                  <Button size="sm" disabled={busy} onClick={() => decide(p, true)}>Approve</Button>
                  <Button size="sm" kind="secondary" disabled={busy} onClick={() => decide(p, false)}>Not yet</Button>
                </div>
                <ErrorNote>{propErr[p.id]}</ErrorNote>
              </Card>
            ))}
            {flagged.map(({ k, d }) => (
              <Card key={'f' + d.planDay.id} pad={12} style={{ borderLeft: '3px solid var(--warning)' }}>
                <Eyebrow color="var(--warning)">Flagged · {k.name} · {dayLabel(d.day)}</Eyebrow>
                <div style={{ fontSize: 14, marginTop: 4 }}>{d.planDay.flag_reason || 'Flagged for a look'}</div>
                <div style={{ fontSize: 12, ...muted, marginTop: 2 }}>A flagged day does not auto-release. Adjust below, then clear the flag; items the pain rule paused come back.</div>
                <div style={{ marginTop: 8 }}>
                  <Button size="sm" kind="secondary" disabled={busy}
                    onClick={() => act(() => API().clearFlag(d.planDay.id))}>Clear flag</Button>
                </div>
              </Card>
            ))}
          </div>
        )}
      </>
    );
  }

  const inputStyle = { padding: '6px 8px', border: '1px solid var(--border-strong)', borderRadius: 6, fontSize: 14,
    background: 'var(--bg-raised)', color: 'var(--fg)', fontFamily: 'var(--font-sans)', boxSizing: 'border-box', minWidth: 0 };

  const STATUS_STYLE = {
    draft: { color: 'var(--fg-muted)', bg: 'var(--ink-50)' },
    released: { color: 'var(--success)', bg: 'var(--success-tint)' },
    locked: { color: 'var(--fg)', bg: 'var(--ink-100)' },
    none: { color: 'var(--fg-subtle)', bg: 'var(--ink-25)' },
  };

  function DayCard({ d, n, today, act, busy }) {
    const pd = d.planDay;
    // Today is locked by the model even if the row still says released.
    const status = !pd ? 'none' : (d.day === today && pd.status !== 'draft') ? 'locked' : pd.status;
    const st = STATUS_STYLE[status] || STATUS_STYLE.none;
    const when = ['Today', 'Tomorrow', 'Day after'][n] || '';
    return (
      <Card pad={12} style={{ minWidth: 0, borderColor: pd && pd.flagged ? 'var(--warning)' : 'var(--border)' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
          <div style={{ minWidth: 0 }}>
            <Eyebrow>{when}</Eyebrow>
            <div style={{ fontSize: 15, fontWeight: 500, marginTop: 2 }}>{dayLabel(d.day)}</div>
          </div>
          <Pill color={st.color} bg={st.bg}>{status === 'none' ? 'no plan' : status}</Pill>
        </div>
        {pd && pd.flagged && (
          <div style={{ fontSize: 13, marginTop: 8, padding: '6px 8px', background: 'var(--warning-tint)', borderRadius: 6 }}>
            {pd.flag_reason || 'Flagged'}
            <div><button style={{ ...linkBtn, fontSize: 12, marginTop: 4 }} disabled={busy}
              onClick={() => act(() => API().clearFlag(pd.id))}>Clear flag</button></div>
          </div>
        )}
        {pd && pd.status === 'draft' && (
          <div style={{ marginTop: 8 }}>
            <Button size="sm" kind="secondary" disabled={busy} onClick={() => act(() => API().releaseDay(pd))}>Release now</Button>
          </div>
        )}
        <div style={{ marginTop: 8 }}>
          {d.items.length === 0 && <div style={{ fontSize: 13, ...muted, padding: '6px 0' }}>{pd ? 'No items.' : 'Nothing planned yet.'}</div>}
          {d.items.map(i => <ItemRow key={i.id} i={i} dayDraft={pd && pd.status === 'draft'} act={act} busy={busy} />)}
        </div>
      </Card>
    );
  }

  const ITEM_STATUS = {
    draft: { label: 'draft', color: 'var(--fg-subtle)' },
    released: { label: 'on', color: 'var(--fg-muted)' },
    done: { label: 'done', color: 'var(--success)' },
    skipped: { label: 'skipped', color: 'var(--danger)' },
    changed: { label: 'changed', color: 'var(--warning)' },
  };
  const SOURCE = { template: '', rule: 'rule', planner: 'Claude', parent: 'parent' };

  function ItemRow({ i, dayDraft, act, busy }) {
    const [open, setOpen] = useState(false);
    const [note, setNote] = useState(i.rule_note || '');
    useEffect(() => { setNote(i.rule_note || ''); }, [i.rule_note]);
    const s = ITEM_STATUS[i.status] || { label: i.status, color: 'var(--fg-muted)' };
    const struck = i.status === 'skipped';
    const tag = i.ladder ? `${i.ladder.name} · step ${i.ladder.step}${i.ladder.of ? ' of ' + i.ladder.of : ''}` : null;
    const restoreTo = dayDraft ? 'draft' : 'released';
    const run = async (p) => { if (await act(() => API().setItem(i.id, p))) setOpen(false); };
    const [moveDay, setMoveDay] = useState(i.day);
    const [moveTime, setMoveTime] = useState(hm(i.start_time));
    const open2 = i.status === 'draft' || i.status === 'released';

    return (
      <div style={{ borderTop: '1px solid var(--border)', padding: '8px 0' }}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
          <div style={{ width: 40, flexShrink: 0, fontFamily: 'var(--font-mono)', fontSize: 11, ...muted, paddingTop: 2 }}>{hm(i.start_time)}</div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 14, fontWeight: 500, textDecoration: struck ? 'line-through' : 'none',
                          color: struck ? 'var(--fg-muted)' : 'var(--fg)', overflowWrap: 'anywhere' }}>{i.title}</div>
            {i.dose && <div style={{ fontSize: 12, ...muted }}>{i.dose}</div>}
            {tag && <div style={{ fontSize: 11, color: 'var(--signal)', marginTop: 2 }}>{tag}</div>}
            {i.rule_note && <div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 2, overflowWrap: 'anywhere' }}>{i.rule_note}</div>}
            <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.06em', marginTop: 3, color: s.color }}>
              {s.label}{SOURCE[i.source] ? <span style={{ color: 'var(--fg-subtle)' }}> · {SOURCE[i.source]}</span> : null}
            </div>
          </div>
          <button aria-label="Actions" onClick={() => setOpen(!open)} style={{
            flexShrink: 0, width: 32, height: 28, borderRadius: 6, border: '1px solid var(--border)', cursor: 'pointer',
            background: open ? 'var(--ink-50)' : 'var(--bg-raised)', color: 'var(--fg-muted)', fontSize: 16, lineHeight: 1,
          }}>⋯</button>
        </div>
        {open && (
          <div style={{ padding: '8px 0 2px 48px' }}>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {open2 && <Button size="sm" kind="secondary" disabled={busy}
                onClick={() => run({ rule_note: 'Lighter: half the sets, easy effort' })}>Lighter</Button>}
              {open2 && <Button size="sm" kind="secondary" disabled={busy} onClick={() => run({ status: 'skipped', rule_note: 'Skipped by a parent' })}>Skip</Button>}
              {(i.status === 'changed' || i.status === 'skipped' || (i.rule_note && open2)) && (
                <Button size="sm" kind="ghost" disabled={busy}
                  onClick={async () => { if (await act(() => API().restoreItem(i, restoreTo))) setOpen(false); }}>Restore</Button>
              )}
            </div>
            {open2 && (
              <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap', alignItems: 'center' }}>
                <span style={{ fontSize: 12, ...muted }}>Move to</span>
                <input type="date" value={moveDay} onChange={e => setMoveDay(e.target.value)} style={{ ...inputStyle, width: 150 }} />
                <input type="time" value={moveTime} onChange={e => setMoveTime(e.target.value)} style={{ ...inputStyle, width: 100 }} />
                <Button size="sm" kind="secondary" disabled={busy || !moveDay || (moveDay === i.day && moveTime === hm(i.start_time))}
                  onClick={async () => { if (await act(() => API().moveItem(i, moveDay, moveTime ? moveTime + ':00' : null))) setOpen(false); }}>Move</Button>
              </div>
            )}
            <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
              <input value={note} onChange={e => setNote(e.target.value)} placeholder="Note for this item"
                style={{ flex: 1, minWidth: 0, padding: '6px 8px', border: '1px solid var(--border-strong)', borderRadius: 6,
                         fontSize: 14, background: 'var(--bg-raised)', color: 'var(--fg)', fontFamily: 'var(--font-sans)' }} />
              <Button size="sm" disabled={busy || note === (i.rule_note || '')}
                onClick={() => run({ rule_note: note.trim() || null })}>Save</Button>
            </div>
          </div>
        )}
      </div>
    );
  }

  // ==================================================================== FAMILY WEEK
  const TEAM_KINDS = ['team', 'games', 'travel'];

  function FamilyWeek() {
    const [anchor, setAnchor] = useState(() => API().today());
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
      let live = true;
      setData(null);
      API().getFamilyWeek(anchor)
        .then(r => { if (live) { setData(r); setError(null); } })
        .catch(e => { if (live) setError(errMsg(e)); });
      return () => { live = false; };
    }, [anchor]);

    const today = API().today();
    const first = data && data.kids[0] && data.kids[0].week;
    const navBtn = { width: 36, height: 36, borderRadius: 18, border: '1px solid var(--border)', background: 'var(--bg-raised)', fontSize: 18, cursor: 'pointer', color: 'var(--fg)' };

    return (
      <>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '12px 0 4px' }}>
          <button style={navBtn} onClick={() => setAnchor(API().addDays(anchor, -7))} aria-label="Previous week">‹</button>
          <div style={{ flex: 1, textAlign: 'center' }}>
            <Eyebrow>{first && first.week > 0 ? `Season week ${first.week}` : 'Family week'}</Eyebrow>
            <div style={{ fontSize: 16, fontWeight: 500, marginTop: 2 }}>
              {first ? `${shortDate(first.from)} – ${shortDate(first.to)}` : '…'}
            </div>
          </div>
          <button style={navBtn} onClick={() => setAnchor(API().addDays(anchor, 7))} aria-label="Next week">›</button>
        </div>
        {first && (first.from > today || first.to < today) && (
          <div style={{ textAlign: 'center', marginBottom: 6 }}><button style={linkBtn} onClick={() => setAnchor(today)}>Back to this week</button></div>
        )}
        <div style={{ fontSize: 13, ...muted, textAlign: 'center', marginBottom: 10 }}>Who has to be where, and when.</div>
        <ErrorNote>{error && `Could not load the week: ${error}`}</ErrorNote>
        {!data && !error && <Loading />}
        {data && first && first.days.map((_, n) => {
          const day = first.days[n].day;
          // Family-wide events once per day; a boy's own events in his column.
          const famEvents = [];
          data.kids.forEach(({ week }) => week.events.forEach(e => {
            if (!e.member_id && e.starts_on <= day && e.ends_on >= day && !famEvents.some(x => x.id === e.id)) famEvents.push(e);
          }));
          return (
            <Card key={day} pad={12} style={{ marginBottom: 10, borderColor: day === today ? 'var(--signal)' : 'var(--border)' }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                <div style={{ fontSize: 15, fontWeight: 500 }}>{dayLabel(day)}</div>
                {day === today && <Pill color="var(--signal)" bg="var(--signal-tint)">Today</Pill>}
                {famEvents.map(e => <Pill key={e.id}>{e.label}</Pill>)}
              </div>
              <div style={{ ...grid(150), marginTop: 8 }}>
                {data.kids.map(({ kid, week }) => (
                  <KidDay key={kid.db_id} name={kid.name} d={week.days[n]}
                    events={week.events.filter(e => e.member_id && e.starts_on <= day && e.ends_on >= day)} />
                ))}
              </div>
            </Card>
          );
        })}
      </>
    );
  }

  function KidDay({ name, d, events }) {
    const team = (d.blocks || []).filter(b => TEAM_KINDS.includes(b.kind));
    const home = (d.items || []).filter(i => i.area === 'physical' || i.area === 'skill');
    const groups = {};
    home.forEach(i => { const t = hm(i.start_time) || '—'; (groups[t] = groups[t] || []).push(i); });
    const rows = [
      ...events.map(e => ({ t: hm(e.start_time) || '', kind: 'event', e })),
      ...team.map(b => ({ t: hm(b.start_time), kind: 'team', b })),
      ...Object.keys(groups).map(t => ({ t: t === '—' ? '99:99' : t, kind: 'home', items: groups[t] })),
    ].sort((a, b) => a.t.localeCompare(b.t));

    return (
      <div style={{ minWidth: 0, background: 'var(--bg-sunken)', borderRadius: 6, padding: '8px 10px' }}>
        <Eyebrow style={{ marginBottom: 4 }}>{name}</Eyebrow>
        {rows.length === 0 && <div style={{ fontSize: 13, color: 'var(--fg-subtle)' }}>Free</div>}
        {rows.map((r, idx) => {
          if (r.kind === 'event') return (
            <div key={'e' + r.e.id} style={{ fontSize: 13, padding: '3px 0', overflowWrap: 'anywhere' }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--warning)' }}>{span(r.e.start_time, r.e.end_time) || 'All day'}</span>
              {' '}<strong style={{ fontWeight: 600 }}>{r.e.label}</strong>{r.e.note ? <span style={muted}> · {r.e.note}</span> : null}
            </div>
          );
          if (r.kind === 'team') return (
            <div key={'b' + r.b.id} style={{ fontSize: 13, padding: '3px 0', overflowWrap: 'anywhere' }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600 }}>{span(r.b.start_time, r.b.end_time)}</span>
              {' '}<span style={{ fontWeight: 500 }}>{r.b.label}</span>
              {r.b.optional ? <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}> · optional</span> : null}
            </div>
          );
          const doneN = r.items.filter(i => i.status === 'done').length;
          return (
            <div key={'h' + idx} style={{ fontSize: 13, padding: '3px 0', ...muted, overflowWrap: 'anywhere' }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12 }}>{r.t === '99:99' ? '' : r.t}</span>
              {' '}Home: {r.items.length === 1 ? r.items[0].title : `${r.items.length} exercises`}
              {doneN > 0 && <span style={{ color: 'var(--success)' }}> · {doneN === r.items.length ? 'done' : `${doneN} done`}</span>}
            </div>
          );
        })}
      </div>
    );
  }

  // ==================================================================== SIGNALS
  // Organised sport per week, in hours: under the boy's age (Otto 11 from 29 Mar 2027).
  function hourLimit(name, day) {
    if (/^axel/i.test(name)) return 13;
    if (/^otto/i.test(name)) return day >= '2027-03-29' ? 11 : 10;
    return null;
  }

  function Signals() {
    const [data, setData] = useState(null);
    const [load, setLoad] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
      let live = true;
      Promise.all([API().getSignals(4), API().getFamilyWeek(API().today()).catch(() => null)])
        .then(([s, w]) => { if (live) { setData(s); setLoad(w); } })
        .catch(e => { if (live) setError(errMsg(e)); });
      return () => { live = false; };
    }, []);

    const today = API().today();
    return (
      <>
        <div style={{ fontSize: 13, ...muted, textAlign: 'center', margin: '8px 0 4px' }}>The last four weeks, from the morning check-ins and the plan.</div>
        <ErrorNote>{error && `Could not load signals: ${error}`}</ErrorNote>
        {!data && !error && <Loading />}
        {data && data.map(s => {
          const wk = load && load.kids.find(x => x.kid.db_id === s.kid.db_id);
          return <KidSignals key={s.kid.db_id} s={s} week={wk && wk.week} limit={hourLimit(s.kid.name, today)} />;
        })}
        <FourSignalCard />
      </>
    );
  }

  function Tile({ label, value, unit, read, tone }) {
    const c = tone === 'warn' ? 'var(--warning)' : tone === 'bad' ? 'var(--danger)' : 'var(--fg-muted)';
    return (
      <div style={{ minWidth: 0, border: '1px solid var(--border)', borderRadius: 8, padding: '10px 12px', background: 'var(--bg-raised)' }}>
        <Eyebrow>{label}</Eyebrow>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4, fontVariantNumeric: 'tabular-nums' }}>
          <span style={{ fontSize: 24, fontWeight: 500, letterSpacing: '-0.02em' }}>{value}</span>
          {unit && <span style={{ fontSize: 12, fontFamily: 'var(--font-mono)', ...muted }}>{unit}</span>}
        </div>
        {read && <div style={{ fontSize: 12, marginTop: 4, color: c, lineHeight: 1.35 }}>{read}</div>}
      </div>
    );
  }

  function KidSignals({ s, week, limit }) {
    const rate = s.days ? s.checkinDays / s.days : 0;
    const sleepRead = s.sleep == null ? ['No sleep logged yet', null]
      : s.sleep < 9 ? ['Sleep under 9 h average — protect bedtime', 'warn'] : ['Sleep on track', null];
    const energyRead = s.energy == null ? ['No data yet', null]
      : s.energy < 3 ? ['Running low — look at load and sleep first', 'warn'] : ['Energy looks fine', null];
    const soreRead = s.soreness == null ? ['No data yet', null]
      : s.soreness >= 3.5 ? ['Often sore — more mobility, less volume', 'warn'] : ['Soreness is normal', null];
    const ciRead = rate >= 0.7 ? ['The habit is holding', null] : rate >= 0.4 ? ['Patchy — a nudge at breakfast helps', 'warn'] : ['Mostly missed — ask him why', 'warn'];
    const pct = s.total ? Math.round(100 * s.done / s.total) : null;
    const doneRead = pct == null ? ['Nothing released yet', null] : pct >= 70 ? ['Keeping up', null] : pct >= 40 ? ['About half — is the plan too big?', 'warn'] : ['Most is not happening — talk before adding more', 'warn'];
    const skipRead = s.skipped === 0 ? ['None skipped', null] : s.total && s.skipped / s.total > 0.25 ? ['Skipping a lot — find the pattern', 'warn'] : ['A few, that is normal', null];

    // Organised sport this week: practice blocks (not the "Games if scheduled" windows, not
    // dressing time) plus the games actually entered as events. A game with no time counts 1 h.
    let hours = null;
    if (week) {
      let mins = 0;
      week.days.forEach(d => (d.blocks || []).filter(b => b.kind === 'team' && !/^(Dress|Undress)\b/i.test(b.label || '')).forEach(b => {
        const a = minutes(b.start_time), z = minutes(b.end_time); if (a != null && z != null && z > a) mins += z - a;
      }));
      (week.events || []).filter(e => e.member_id && (e.kind === 'game' || e.kind === 'tournament')).forEach(e => {
        const a = minutes(e.start_time), z = minutes(e.end_time);
        mins += (a != null && z != null && z > a) ? z - a : 60;
      });
      hours = mins / 60;
    }
    const loadRead = hours == null ? ['Schedule not loaded', null]
      : limit == null ? [null, null]
      : hours >= limit ? [`At or over the ${limit} h limit — drop optional free ice first`, 'bad']
      : hours >= limit - 1 ? [`Close to the ${limit} h limit`, 'warn'] : [`Under the ${limit} h limit`, null];

    return (
      <>
        <H sub={`Last ${s.days} days`}>{s.kid.name}</H>
        <div style={grid(150)}>
          <Tile label="Check-ins" value={`${s.checkinDays}/${s.days}`} read={ciRead[0]} tone={ciRead[1]} />
          <Tile label="Sleep" value={fmt1(s.sleep)} unit="h avg" read={sleepRead[0]} tone={sleepRead[1]} />
          <Tile label="Energy" value={fmt1(s.energy)} unit="/ 5" read={energyRead[0]} tone={energyRead[1]} />
          <Tile label="Soreness" value={fmt1(s.soreness)} unit="/ 5" read={soreRead[0]} tone={soreRead[1]} />
          <Tile label="Done" value={`${s.done}/${s.total}`} unit={pct != null ? `${pct}%` : ''} read={doneRead[0]} tone={doneRead[1]} />
          <Tile label="Skipped" value={s.skipped} read={skipRead[0]} tone={skipRead[1]} />
          <Tile label="Organised sport" value={hours == null ? '–' : fmt1(hours)} unit={limit ? `h / ${limit} h` : 'h'}
            read={loadRead[0] ? `${loadRead[0]} (this week, from the schedule)` : 'This week, from the schedule'} tone={loadRead[1]} />
        </div>
        <Card pad={12} style={{ marginTop: 10, borderLeft: `3px solid ${s.painDays.length ? 'var(--danger)' : 'var(--border)'}` }}>
          <Eyebrow>Pain days · {s.painDays.length}</Eyebrow>
          {s.painDays.length === 0
            ? <div style={{ fontSize: 13, ...muted, marginTop: 4 }}>No pain reported.</div>
            : <div style={{ marginTop: 4 }}>
                {s.painDays.map(c => (
                  <div key={c.id || c.day} style={{ fontSize: 13, padding: '2px 0', overflowWrap: 'anywhere' }}>
                    <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, ...muted }}>{shortDate(c.day)}</span> · {c.pain_where || 'not said where'}
                  </div>
                ))}
                {s.painDays.length >= 2 && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>More than once — if it is the same place, book a physio.</div>}
              </div>}
        </Card>
        <GrowthCard kid={s.kid} />
      </>
    );
  }

  // Growth (023, coach playbook 11): height, seated height and weight every three months.
  // Two measurements ~3 months apart are the minimum before the curve says anything.
  function GrowthCard({ kid }) {
    const [rows, setRows] = useState(null);
    const [open, setOpen] = useState(false);
    const [f, setF] = useState({ day: API().today(), height_cm: '', seated_height_cm: '', weight_kg: '' });
    const [err, setErr] = useState(null);
    const load = () => API().body.list(kid.db_id).then(setRows).catch(e => setErr(errMsg(e)));
    useEffect(() => { load(); }, [kid.db_id]);
    if (!rows) return null;
    const g = API().body.growth(rows);
    const num = (v) => (v === '' ? null : Number(v));
    async function save() {
      setErr(null);
      try {
        await API().body.add(kid.db_id, { day: f.day, height_cm: num(f.height_cm), seated_height_cm: num(f.seated_height_cm), weight_kg: num(f.weight_kg) });
        setOpen(false); setF({ ...f, height_cm: '', seated_height_cm: '', weight_kg: '' }); load();
      } catch (e) { setErr(errMsg(e)); }
    }
    const read = !g.last ? ['Not measured yet. Measure height, seated height and weight: five minutes, same time of day.', 'warn']
      : g.perYear == null ? ['One measurement is a point, not a curve. Measure again about three months after the first.', null]
      : g.spurt ? [`Growing ${fmt1(g.perYear)} cm a year: likely in the growth spurt. Fewer jumps and sprints, more mobility, no new load records. Clumsiness now is expected and temporary.`, 'bad']
      : [`Growing ${fmt1(g.perYear)} cm a year: steady.`, null];
    return (
      <Card pad={12} style={{ marginTop: 10, borderLeft: `3px solid ${read[1] === 'bad' ? 'var(--danger)' : read[1] === 'warn' ? 'var(--warning)' : 'var(--border)'}` }}>
        <Eyebrow>Growth</Eyebrow>
        {g.last && (
          <div style={{ fontSize: 14, marginTop: 4 }}>
            {g.last.height_cm != null && <>{fmt1(g.last.height_cm)} cm</>}
            {g.last.seated_height_cm != null && <> · seated {fmt1(g.last.seated_height_cm)} cm</>}
            {g.last.weight_kg != null && <> · {fmt1(g.last.weight_kg)} kg</>}
            <span style={{ fontSize: 12, ...muted }}> · {shortDate(g.last.day)}</span>
          </div>
        )}
        <div style={{ fontSize: 13, marginTop: 4, color: read[1] === 'bad' ? 'var(--danger)' : 'var(--fg)' }}>{read[0]}</div>
        {g.last && g.stale && <div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 2 }}>Last measured over three months ago. Time to measure again.</div>}
        {!open
          ? <div style={{ marginTop: 8 }}><Button size="sm" kind="secondary" onClick={() => setOpen(true)}>Log a measurement</Button></div>
          : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: 6, marginTop: 8 }}>
              <input type="date" value={f.day} onChange={e => setF({ ...f, day: e.target.value })} style={inputStyle} />
              <input inputMode="decimal" placeholder="Height, cm" value={f.height_cm} onChange={e => setF({ ...f, height_cm: e.target.value })} style={inputStyle} />
              <input inputMode="decimal" placeholder="Seated height, cm" value={f.seated_height_cm} onChange={e => setF({ ...f, seated_height_cm: e.target.value })} style={inputStyle} />
              <input inputMode="decimal" placeholder="Weight, kg" value={f.weight_kg} onChange={e => setF({ ...f, weight_kg: e.target.value })} style={inputStyle} />
              <div style={{ display: 'flex', gap: 6 }}>
                <Button size="sm" disabled={!f.height_cm && !f.weight_kg} onClick={save}>Save</Button>
                <Button size="sm" kind="ghost" onClick={() => setOpen(false)}>Cancel</Button>
              </div>
            </div>
          )}
        <ErrorNote>{err}</ErrorNote>
        {rows.length > 1 && (
          <div style={{ marginTop: 8, fontSize: 12, ...muted }}>
            {rows.slice(-4).reverse().map(r => <div key={r.id}>{shortDate(r.day)} · {r.height_cm != null ? fmt1(r.height_cm) + ' cm' : '–'}{r.seated_height_cm != null ? ` · seated ${fmt1(r.seated_height_cm)}` : ''}{r.weight_kg != null ? ` · ${fmt1(r.weight_kg)} kg` : ''}</div>)}
          </div>
        )}
      </Card>
    );
  }

  function FourSignalCard() {
    const signals = [
      ['Sleep', 'Falls asleep easily, wakes without a fight.'],
      ['Mood', 'In a good mood and wants to go to practice.'],
      ['Grades', 'The grade trend holds.'],
      ['Joy', 'Still enjoys each sport.'],
    ];
    return (
      <>
        <H sub="Once a month, both parents, five minutes.">Monthly four-signal check</H>
        <Card pad={14}>
          {signals.map(([k, v], n) => (
            <div key={k} style={{ display: 'flex', gap: 10, padding: '6px 0', borderTop: n ? '1px solid var(--border)' : 'none' }}>
              <div style={{ width: 20, fontFamily: 'var(--font-mono)', fontSize: 12, ...muted, paddingTop: 1 }}>{n + 1}</div>
              <div style={{ fontSize: 14 }}><strong style={{ fontWeight: 500 }}>{k}</strong> — {v}</div>
            </div>
          ))}
          <div style={{ fontSize: 13, marginTop: 10, padding: '8px 10px', background: 'var(--bg-sunken)', borderRadius: 6 }}>
            Two slipping → drop in this order: optional free ice → skills → home training. Homework and sleep go last.
          </div>
          <div style={{ fontSize: 13, ...muted, marginTop: 10 }}>
            Load limit: organised sport per week stays under age in hours — Axel under 13 h, Otto under 10 h (11 h from 29 Mar 2027).
          </div>
        </Card>
      </>
    );
  }

  window.ParentsScreen = ParentsScreen;
})();
