// fantasy/src/dashboard.jsx — every league at a glance.
//
// Reads derived/fantasySnapshot (built server-side after every sync) for
// everything that is a judgement — projections, legality, swaps, health —
// and the league docs only for what the platform said. Nothing is computed
// here beyond formatting, so this page and the Telegram alert can never
// disagree about whether a lineup is legal.
//
// Order is the running Status page's: act now, this week per league, then
// the pipeline.

const { useContext } = React;
const { DataContext, SyncChip, InjuryTag, timeAgo, projLabel, kickoffLabel } = window.FantasyShared;

const SEV_TAG = { critical: 'tag-red', warn: 'tag-amber', info: 'tag-blue' };

function ActNow({ snapshot }) {
  const items = (snapshot && snapshot.actNow) || [];
  if (!items.length) return (
    <div className="card-sm" style={{ borderColor: 'rgba(16,185,129,0.3)', marginBottom: 16, display: 'flex', gap: 10, alignItems: 'center' }}>
      <span className="tag tag-green">all clear</span>
      <span className="muted" style={{ fontSize: 12 }}>Every lineup is legal and nothing on the bench beats a starter by 2+.</span>
    </div>
  );
  return (
    <div className="card" style={{ marginBottom: 16, borderColor: items.some(i => i.severity === 'critical') ? 'rgba(239,68,68,0.4)' : '#1e2a3a' }}>
      <div className="section-header">Act now</div>
      <div style={{ display: 'grid', gap: 8 }}>
        {items.map((it, i) => (
          <div key={i} style={{ display: 'flex', gap: 10, alignItems: 'baseline', fontSize: 12 }}>
            <span className={`tag ${SEV_TAG[it.severity] || 'tag-grey'}`} style={{ flexShrink: 0 }}>{it.severity === 'critical' ? 'fix' : it.severity === 'warn' ? 'check' : 'gain'}</span>
            <span style={{ flex: 1 }}>{it.text}</span>
            {Number.isFinite(it.deadlineMs) && <span className="muted" style={{ flexShrink: 0 }}>by {kickoffLabel(it.deadlineMs)}</span>}
          </div>
        ))}
      </div>
    </div>
  );
}

function PlayerRow({ p, slot, issue }) {
  if (!p) return (
    <tr><td><span className="slot">{slot}</span></td><td className="red">— empty slot —</td><td></td><td></td><td></td></tr>
  );
  const bad = issue && issue.severity === 'critical';
  return (
    <tr style={bad ? { background: 'rgba(239,68,68,0.06)' } : undefined}>
      <td><span className="slot">{slot}</span></td>
      <td>{p.name}<InjuryTag status={p.injuryStatus} />{issue && issue.kind === 'bye' && <span className="tag tag-red" style={{ marginLeft: 6 }}>BYE</span>}</td>
      <td className="muted">{p.position}{p.team ? ` · ${p.team}` : ''}</td>
      <td style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }} title={p.bySource ? Object.entries(p.bySource).map(([k, v]) => `${k}: ${Number(v).toFixed(1)}`).join('\n') : ''}>{projLabel(p)}{p.sources && p.sources.length > 1 ? <span className="muted" style={{ fontSize: 10 }}> ·{p.sources.length}{Number.isFinite(p.spread) && p.spread > 0 ? ` ±${(p.spread / 2).toFixed(1)}` : ''}</span> : null}</td>
      <td className="muted" style={{ fontSize: 11 }}>{kickoffLabel(p.kickoffMs)}</td>
    </tr>
  );
}

function LeagueCard({ league, snap }) {
  const L = snap; // snapshot entry for this league, may be null before the first build
  if (!L) return (
    <div className="card">
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{ fontSize: 15, fontWeight: 700 }}>{league.name}</div>
        <span className="tag tag-accent">{league.platform}</span>
        <span style={{ marginLeft: 'auto' }}><SyncChip league={league} /></span>
      </div>
      <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>No snapshot yet — sync from Settings.</div>
    </div>
  );
  const byId = Object.fromEntries((L.players || []).map(p => [p.id, p]));
  const slots = (league.slots && league.slots.starters) || [];
  const mine = (league.rosters || []).find(r => r.teamId === league.myTeamId);
  const starters = mine ? mine.starters : [];
  const issuesByIndex = Object.fromEntries(((L.lineup && L.lineup.issues) || []).map(i => [i.index, i]));
  const bench = (L.players || []).filter(p => p.role === 'bench').sort((a, b) => (b.projected || 0) - (a.projected || 0));
  const ir = (L.players || []).filter(p => p.role === 'ir');
  const rec = L.record;
  return (
    <div className="card">
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 15, fontWeight: 700 }}>{league.name}</div>
        <span className="tag tag-accent">{league.platform}</span>
        {league.scoring && <span className="tag tag-grey">{league.scoring.ppr === 1 ? 'PPR' : league.scoring.ppr === 0.5 ? 'Half PPR' : 'Standard'}{league.slots && league.slots.superflex ? ' · SF' : ''}</span>}
        {Number.isFinite(L.faabLeft) && <span className="tag tag-grey">FAAB ${L.faabLeft} left</span>}
        {L.lineup && (L.lineup.legal ? <span className="tag tag-green">lineup legal</span> : <span className="tag tag-red">{L.lineup.issues.filter(i => i.severity === 'critical').length} lineup issue(s)</span>)}
        <span style={{ marginLeft: 'auto' }}><SyncChip league={league} /></span>
      </div>
      {!L.mine ? (
        <div className="muted" style={{ fontSize: 12 }}>Your team wasn't identified in this league — pick it under Settings → Leagues.{league.myTeamDiagnostic ? ` (${league.myTeamDiagnostic})` : ''}</div>
      ) : (
        <>
          <div style={{ display: 'flex', gap: 16, alignItems: 'baseline', marginBottom: 10, fontSize: 12, flexWrap: 'wrap' }}>
            <span style={{ fontWeight: 600 }}>{L.teamName}</span>
            {rec && <span className="muted">{rec.wins}-{rec.losses}{rec.ties ? `-${rec.ties}` : ''}{Number.isFinite(rec.pointsFor) ? ` · ${rec.pointsFor.toFixed(1)} PF` : ''}</span>}
          </div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, fontSize: 12, flexWrap: 'wrap' }}>
            <span className="label" style={{ margin: 0 }}>Week {L.week}</span>
            {L.bye ? <span className="muted">bye week</span> : (
              <>
                <span className="val-md">{L.lineup ? L.lineup.currentTotal.toFixed(1) : '—'}</span>
                <span className="muted">proj</span>
                {L.opponent && (
                  <>
                    <span className="muted">vs</span>
                    <span className="val-md muted">{Number.isFinite(L.opponent.projectedTotal) ? L.opponent.projectedTotal.toFixed(1) : '—'}</span>
                    <span className="muted">{L.opponent.name}</span>
                    {Number.isFinite(L.margin) && <span className={L.margin >= 0 ? 'green' : 'red'}>{L.margin >= 0 ? '+' : ''}{L.margin.toFixed(1)}</span>}
                  </>
                )}
                {L.lineup && L.lineup.projectedGain >= 2 && <span className="amber">+{L.lineup.projectedGain.toFixed(1)} available on the bench</span>}
                {Number.isFinite(L.points) && L.points > 0 && <span className="muted">· live {L.points.toFixed(1)}{L.opponent && Number.isFinite(L.opponent.points) ? ` – ${L.opponent.points.toFixed(1)}` : ''}</span>}
              </>
            )}
          </div>
          {L.lineup && L.lineup.swaps.length > 0 && (
            <div style={{ marginTop: 10, display: 'grid', gap: 4, fontSize: 12 }}>
              {L.lineup.swaps.filter(s => s.in).map((s, i) => (
                <div key={i}><span className="slot">{s.slot}</span> <span className="green">{s.in.name}</span> {Number.isFinite(s.in.projected) ? `(${s.in.projected.toFixed(1)})` : ''} over <span className={s.out ? 'red' : 'muted'}>{s.out ? s.out.name : 'empty'}</span> {s.out && Number.isFinite(s.out.projected) ? `(${s.out.projected.toFixed(1)})` : ''} <span className="muted">+{s.gain.toFixed(1)}</span></div>
              ))}
            </div>
          )}
          {L.pending && L.pending.count > 0 && (
            <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Pending on {league.platform}: {L.pending.count} claim{L.pending.count === 1 ? '' : 's'}{L.pending.adds.length ? ` — adding ${L.pending.adds.map(a => a.name).join(', ')}` : ''}{L.pending.drops.length ? `, dropping ${L.pending.drops.map(d => d.name).join(', ')}` : ''}</div>
          )}
          {L.picks && <Picks picks={L.picks} />}
          <hr className="divider" />
          <div className="grid-2">
            <div>
              <div className="section-header">Starters</div>
              <table><tbody>
                {slots.map((slot, i) => <PlayerRow key={`${slot}-${i}`} p={byId[starters[i]] || null} slot={slot} issue={issuesByIndex[i]} />)}
              </tbody></table>
            </div>
            <div>
              <div className="section-header">Bench{ir.length ? ' & IR' : ''}</div>
              <table><tbody>
                {bench.map(p => <PlayerRow key={p.id} p={p} slot="BN" />)}
                {ir.map(p => <PlayerRow key={p.id} p={p} slot="IR" />)}
              </tbody></table>
            </div>
          </div>
          {L.unresolved && L.unresolved.length > 0 && (
            <div className="muted" style={{ fontSize: 11, marginTop: 10 }}>
              {L.unresolved.length} ESPN player(s) not matched to the player master: {L.unresolved.map(u => u.name).join(', ')}. They're on the roster but carry no Sleeper projection.
            </div>
          )}
        </>
      )}
    </div>
  );
}

const TIER_TAG = { 'league-winner': 'tag-red', starter: 'tag-amber', flex: 'tag-blue', speculative: 'tag-grey' };
const KIND_TAG = { injury: 'tag-red', suspension: 'tag-red', release: 'tag-amber', trade: 'tag-blue', signing: 'tag-blue', role: 'tag-green', activation: 'tag-green', other: 'tag-grey' };

// The wire: this week's claims per league, with the bid and the reason —
// built server-side by waiverPlan on Tuesday morning and on demand.
// A fee league's plan: the tree of claims that clear the bar, every branch
// on the same drop, then what was held and why. The fee is on every line.
function ClaimTree({ L }) {
  const claims = L.claims || [];
  const held = L.held || [];
  return (
    <div style={{ display: 'grid', gap: 8 }}>
      {!claims.length ? <div className="muted" style={{ fontSize: 12 }}>{L.treeNote || `Nothing clears the $${L.fee} bar this week — hold.`}</div> : (
        <>
          <table>
            <thead><tr><th>#</th><th>Claim</th><th>Proj</th><th>Tier</th><th>Why it clears</th><th>Drop</th></tr></thead>
            <tbody>
              {claims.map(t => (
                <tr key={t.id}>
                  <td className="muted">{t.order}</td>
                  <td>{t.name} <span className="muted">{t.position}{t.team ? ` · ${t.team}` : ''}{Number.isFinite(t.pctOwned) ? ` · ${t.pctOwned}% rostered` : ''}</span><InjuryTag status={t.injuryStatus} /></td>
                  <td style={{ fontVariantNumeric: 'tabular-nums' }}>{Number.isFinite(t.projected) ? t.projected.toFixed(1) : '—'}</td>
                  <td><span className={`tag ${TIER_TAG[t.tier] || 'tag-grey'}`}>{t.tier}</span></td>
                  <td className="muted" style={{ fontSize: 11 }}>{t.barReason}{t.availability ? ` · ${t.availability}` : ''}</td>
                  <td className="muted" style={{ fontSize: 11 }}>{t.drop ? t.drop.name : '—'} · ${t.cost}</td>
                </tr>
              ))}
            </tbody>
          </table>
          {L.treeNote && <div className="muted" style={{ fontSize: 11 }}>{L.treeNote}</div>}
        </>
      )}
      {held.length > 0 && <div className="muted" style={{ fontSize: 11 }}>Monitor, not worth ${L.fee}: {held.slice(0, 5).map(h => `${h.name} ${h.position}`).join(', ')}</div>}
    </div>
  );
}

// Who to start and how sure: one row per slot, the certainty as a bar,
// the alternative the starter is keeping on the bench, and the factors
// that moved the numbers (weather, the line, a designation). A move is a
// slot where the pick differs from the lineup as set and clears 55%.
function CertaintyBar({ value }) {
  const pct = Math.round((value || 0) * 100);
  const color = pct >= 70 ? '#22c55e' : pct >= 58 ? '#f59e0b' : '#ef4444';
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <span style={{ display: 'inline-block', width: 56, height: 6, background: 'rgba(255,255,255,0.08)', borderRadius: 3, overflow: 'hidden' }}>
        <span style={{ display: 'block', width: `${pct}%`, height: '100%', background: color }} />
      </span>
      <span style={{ fontVariantNumeric: 'tabular-nums', minWidth: 32 }}>{pct}%</span>
    </span>
  );
}

// One opinion as a percentage, or why there is none.
function MetricCell({ value, missing = '—' }) {
  if (!Number.isFinite(value)) return <span className="muted" style={{ fontSize: 10 }}>{missing}</span>;
  const pct = Math.round(value * 100);
  const cls = pct >= 70 ? 'green' : pct >= 58 ? 'amber' : pct <= 42 ? 'red' : 'muted';
  return <span className={cls} style={{ fontVariantNumeric: 'tabular-nums' }}>{pct}%</span>;
}
const OPINION_TAG = { agree: 'tag-green', split: 'tag-amber', against: 'tag-red', lean: 'tag-grey' };

// The analysts' vote: how many of the desks that projected both players
// put the starter ahead, with each desk's pair on hover. Fewer than three
// desks in common is "too few", never a coin flip.
function AnalystCell({ vote }) {
  if (!vote || !vote.n) return <span className="muted" style={{ fontSize: 10 }}>too few desks</span>;
  const share = vote.favour / vote.n;
  const cls = share >= 0.7 ? 'green' : share >= 0.58 ? 'amber' : share <= 0.42 ? 'red' : 'muted';
  const title = (vote.sources || []).map(x => `${x.source}: ${x.starter} v ${x.alternative}`).join('\n');
  return <span className={cls} style={{ fontVariantNumeric: 'tabular-nums', cursor: 'help' }} title={title}>{vote.favour % 1 ? vote.favour.toFixed(1) : vote.favour}/{vote.n}</span>;
}

// The experts' consensus line under a name: positional rank, average, and
// the best-to-worst spread that says how contested he is.
function EcrLine({ ecr }) {
  if (!ecr || !Number.isFinite(ecr.avg)) return null;
  return <div className="muted" style={{ fontSize: 10 }}>ECR {ecr.posRank || ''} avg {ecr.avg.toFixed(1)}{Number.isFinite(ecr.best) && Number.isFinite(ecr.worst) ? ` (${ecr.best}–${ecr.worst})` : ''}{Number.isFinite(ecr.tier) ? ` · tier ${ecr.tier}` : ''}</div>;
}

function Picks({ picks }) {
  const [open, setOpen] = useState(true);
  const moves = picks.moves || [];
  return (
    <div style={{ marginTop: 12 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
        <div className="section-header" style={{ margin: 0, cursor: 'pointer' }} onClick={() => setOpen(o => !o)}>Picks {open ? '▾' : '▸'}</div>
        {moves.length ? <span className="tag tag-amber">{moves.length} move{moves.length === 1 ? '' : 's'}</span> : <span className="tag tag-green">lineup as set</span>}
        {picks.coinFlips && picks.coinFlips.length > 0 && <span className="muted" style={{ fontSize: 11 }}>{picks.coinFlips.length} coin flip{picks.coinFlips.length === 1 ? '' : 's'}</span>}
        {picks.weatherNotes && picks.weatherNotes.map(w => <span key={w.team} className="muted" style={{ fontSize: 11 }}>🌦 {w.text}</span>)}
      </div>
      {moves.length > 0 && (
        <div style={{ display: 'grid', gap: 4, fontSize: 12, marginTop: 6 }}>
          {moves.map(m => (
            <div key={m.slot + m.index}><span className="slot">{m.slot}</span> start <span className="green">{m.in.name}</span> {Number.isFinite(m.in.adjusted) ? `(${m.in.adjusted.toFixed(1)})` : ''} over <span className="red">{m.out ? m.out.name : 'empty'}</span> {m.out && Number.isFinite(m.out.adjusted) ? `(${m.out.adjusted.toFixed(1)})` : ''} <CertaintyBar value={m.certainty} />{m.metrics && Number.isFinite(m.metrics.experts) ? <span className="muted" style={{ fontSize: 11 }}> · experts {Math.round(m.metrics.experts * 100)}%</span> : null}{m.opinion ? <span className={`tag ${OPINION_TAG[m.opinion] || 'tag-grey'}`} style={{ marginLeft: 6 }}>{m.opinion}</span> : null}{m.why && m.why.length ? <span className="muted"> · {m.why.join(' · ')}</span> : null}</div>
          ))}
        </div>
      )}
      {open && (
        <table style={{ marginTop: 6 }}>
          <thead><tr><th>Slot</th><th>Start</th><th>Adj</th><th>Model</th><th>Experts</th><th>Crowd</th><th>Analysts</th><th>Verdict</th><th>Over</th><th>Why</th></tr></thead>
          <tbody>
            {picks.slots.map(s => (
              <tr key={s.slot + s.index} style={s.change ? { background: 'rgba(245,158,11,0.06)' } : undefined}>
                <td><span className="slot">{s.slot}</span></td>
                <td>{s.starter ? <>{s.starter.name} <span className="muted">{s.starter.team || ''}{s.starter.game ? ` ${s.starter.game.home ? 'vs' : '@'} ${s.starter.game.opponent}` : ''}</span><InjuryTag status={s.starter.injuryStatus} /><EcrLine ecr={s.starter.ecr} /></> : <span className="muted">—</span>}</td>
                <td style={{ fontVariantNumeric: 'tabular-nums' }}>{s.starter && Number.isFinite(s.starter.adjusted) ? s.starter.adjusted.toFixed(1) : '—'}{s.starter && Number.isFinite(s.starter.raw) && s.starter.adjusted !== s.starter.raw ? <span className="muted" style={{ fontSize: 10 }}> ({s.starter.raw.toFixed(1)})</span> : null}</td>
                <td><MetricCell value={s.alternative ? s.metrics && s.metrics.model : null} /></td>
                <td><MetricCell value={s.alternative ? s.metrics && s.metrics.experts : null} missing={s.alternative && s.alternative.viaChain ? 'positions differ' : 'no feed'} /></td>
                <td><MetricCell value={s.alternative ? s.metrics && s.metrics.crowd : null} missing="no start rate" /></td>
                <td><AnalystCell vote={s.alternative ? s.analystVote : null} /></td>
                <td>{s.alternative ? <><CertaintyBar value={s.certainty} />{s.opinion ? <span className={`tag ${OPINION_TAG[s.opinion] || 'tag-grey'}`} style={{ marginLeft: 6 }}>{s.opinion}</span> : null}</> : <span className="muted" style={{ fontSize: 11 }}>only option</span>}{s.coinFlip ? <span className="tag tag-grey" style={{ marginLeft: 6 }}>coin flip</span> : null}{s.locked ? <span className="tag tag-grey" style={{ marginLeft: 6 }}>locked</span> : null}</td>
                <td className="muted" style={{ fontSize: 11 }}>{s.alternative ? <>{s.alternative.name} {s.alternative.position || ''}{Number.isFinite(s.alternative.adjusted) ? ` (${s.alternative.adjusted.toFixed(1)})` : ''}{s.alternative.viaChain ? ` into ${s.alternative.intoSlot}` : ''}<EcrLine ecr={s.alternative.ecr} /></> : ''}</td>
                <td className="muted" style={{ fontSize: 11 }}>{(s.starter && s.starter.factors || []).filter(f => f.pct).map(f => f.text).join(' · ')}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

function Wire({ plan }) {
  if (!plan || !plan.leagues || !plan.leagues.length) return (
    <div className="card" style={{ marginTop: 16 }}>
      <div className="section-header">The wire</div>
      <div className="muted" style={{ fontSize: 12 }}>No waiver plan built yet — it is rebuilt on every sync (every 30 minutes) and pushed to Telegram Tuesday 06:00 ET, or build one now from Settings.</div>
    </div>
  );
  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 12 }}>
        <div className="section-header" style={{ margin: 0 }}>The wire</div>
        <span className="muted" style={{ fontSize: 11 }}>week {plan.week} · built {timeAgo(plan.builtAt)} · {plan.eventCount} events, {plan.trendingCount} trending</span>
      </div>
      <div style={{ display: 'grid', gap: 16 }}>
        {plan.leagues.map(L => (
          <div key={L.docId}>
            <div style={{ display: 'flex', gap: 10, alignItems: 'baseline', flexWrap: 'wrap', marginBottom: 6 }}>
              <span style={{ fontWeight: 600 }}>{L.name}</span>
              {Number.isFinite(L.faabLeft) ? <span className="tag tag-grey">FAAB ${L.faabLeft} of ${L.budgetTotal}</span> : (L.waiverPosition != null && <span className="tag tag-grey">waiver #{L.waiverPosition}</span>)}
              {L.waiverDay && <span className="muted" style={{ fontSize: 11 }}>clears {L.waiverDay}</span>}
              {L.fee > 0 && <span className="tag tag-amber">${L.fee} per add</span>}
              {L.calibration && L.calibration.n > 0 && <span className="muted" style={{ fontSize: 11 }}>league bids: median ${L.calibration.median}, p75 ${L.calibration.p75}, max ${L.calibration.max}{L.calibration.applied ? '' : ` (${L.calibration.n} on record, not yet applied)`}</span>}
            </div>
            {!L.mine ? <div className="muted" style={{ fontSize: 12 }}>Your team wasn't identified here.</div>
              : L.fee > 0 ? <ClaimTree L={L} />
              : !L.targets.length ? <div className="muted" style={{ fontSize: 12 }}>Nothing worth a claim this week.</div> : (
              <table>
                <thead><tr><th>Target</th><th>Proj</th><th>Tier</th><th>Bid</th><th>Why</th></tr></thead>
                <tbody>
                  {L.targets.slice(0, 6).map(t => (
                    <tr key={t.id}>
                      <td>{t.name} <span className="muted">{t.position}{t.team ? ` · ${t.team}` : ''}</span><InjuryTag status={t.injuryStatus} /></td>
                      <td style={{ fontVariantNumeric: 'tabular-nums' }}>{Number.isFinite(t.projected) ? t.projected.toFixed(1) : '—'}</td>
                      <td><span className={`tag ${TIER_TAG[t.tier] || 'tag-grey'}`}>{t.tier}</span></td>
                      <td title={t.bidRationale || ''}>{Number.isFinite(t.bid) ? `$${t.bid}` : '—'}</td>
                      <td className="muted" style={{ fontSize: 11 }}>{(t.reasons || []).slice(0, 2).map(r => r.text).join(' · ')}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
            {L.mine && L.drops && L.drops.length > 0 && (
              <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>Drop candidates: {L.drops.map(d => `${d.name}${Number.isFinite(d.projected) ? ` (${d.projected.toFixed(1)})` : ''}`).join(', ')}</div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

const ACTION_MARK = { swap: '🔁', start: '🔁', claim: '➕', ir: '🏥', drop: '➖', watch: '👀', none: 'ℹ️' };
const RELATION_LABEL = { 'mine-starter': 'your starter', 'mine-bench': 'on your bench', 'mine-ir': 'on your IR', opponent: "opponent's", 'other-team': "another team's", unrostered: 'on the wire' };

// The latest classified items, each with its assessment: what the event
// means for my team in every league (newsImpact), so a headline is never
// shown without the answer to "so what do I do". `via` says which road
// classified it — the rules or the model — so a modelled reading is never
// dressed as a fact.
function News({ news, snapshot }) {
  if (!news || !news.length) return null;
  const mine = new Set(((snapshot && snapshot.leagues) || []).flatMap(l => (l.players || []).map(p => p.id)));
  const actionable = news.filter(n => n.impact && n.impact.needsAction).length;
  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 12 }}>
        <div className="section-header" style={{ margin: 0 }}>News</div>
        {actionable > 0 && <span className="tag tag-red">{actionable} need action</span>}
      </div>
      <div style={{ display: 'grid', gap: 10 }}>
        {news.slice(0, 15).map(n => {
          const leaguesWithActions = ((n.impact && n.impact.leagues) || []).filter(L => L.actions && L.actions.length);
          return (
            <div key={n.id} style={{ borderLeft: `2px solid ${n.impact && n.impact.needsAction ? '#ef4444' : '#1e2a3a'}`, paddingLeft: 10 }}>
              <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', fontSize: 12, flexWrap: 'wrap' }}>
                {n.event ? <span className={`tag ${KIND_TAG[n.event.kind] || 'tag-grey'}`} title={`${n.event.via}${n.event.matched ? ` · ${n.event.matched}` : ''}`}>{n.event.kind}{n.event.severity && n.event.severity !== 'none' ? ` · ${n.event.severity}` : ''}</span> : <span className="tag tag-grey">unread</span>}
                {n.playerId && mine.has(n.playerId) && <span className="tag tag-accent">mine</span>}
                {n.impact && n.impact.needsAction && <span className={`tag ${n.impact.urgency === 'now' ? 'tag-red' : 'tag-amber'}`}>{n.impact.urgency === 'now' ? 'act now' : n.impact.urgency === 'this-week' ? 'this week' : 'by Tuesday'}</span>}
                <span style={{ flex: 1 }}>{n.url ? <a href={n.url} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>{n.headline}</a> : n.headline}</span>
                <span className="muted" style={{ fontSize: 11 }}>{timeAgo(n.publishedMs)}{n.event && n.event.via === 'model' ? ' · model' : ''}</span>
              </div>
              {leaguesWithActions.length > 0 && (
                <div style={{ display: 'grid', gap: 2, marginTop: 4, fontSize: 11 }}>
                  {leaguesWithActions.map(L => L.actions.map((x, i) => (
                    <div key={`${L.docId}-${i}`} style={{ display: 'flex', gap: 6, alignItems: 'baseline' }}>
                      <span>{ACTION_MARK[x.kind] || 'ℹ️'}</span>
                      <span className="muted" style={{ flexShrink: 0 }}>{L.name} ({RELATION_LABEL[L.relation] || L.relation})</span>
                      <span className={x.kind === 'none' || x.kind === 'watch' ? 'muted' : ''}>{x.text}{x.urgency === 'now' ? <span className="red"> — now</span> : ''}</span>
                    </div>
                  )))}
                </div>
              )}
              {n.impact && !leaguesWithActions.length && <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>No action in any league.</div>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// The picks' own record: what the model, the experts and the crowd said
// versus what the players then scored (pickLedger, scored the morning after
// the week closes). Absent until a week has been scored — a blank is "not
// yet", never 0%.
const pctOf = (v) => (Number.isFinite(v) ? `${Math.round(v * 100)}%` : '—');
const f1 = (v) => (Number.isFinite(v) ? v.toFixed(1) : '—');

function TrackRecord({ snapshot }) {
  const a = snapshot && snapshot.accuracy;
  if (!a || !a.weeksScored) return null;
  const lw = a.lastWeek;
  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div className="section-header">Track record <span className="muted" style={{ fontWeight: 400, fontSize: 11, marginLeft: 8 }}>{a.weeksScored} week(s) scored{a.thin ? ' · thin sample' : ''}</span></div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12, marginBottom: 12 }}>
        <div><div className="label">Picks right</div><div className="val-md">{a.right}/{a.comparisons} <span className="muted" style={{ fontSize: 12 }}>{pctOf(a.hitRate)}</span></div></div>
        <div><div className="label">Calls right</div><div className="val-md">{a.callsRight}/{a.calls} <span className="muted" style={{ fontSize: 12 }}>{pctOf(a.callRate)}</span></div></div>
        <div><div className="label">Following the picks</div><div className={`val-md ${a.followGain > 0 ? 'green' : a.followGain < 0 ? 'red' : ''}`}>{a.followGain > 0 ? '+' : ''}{f1(a.followGain)} pts</div></div>
        <div><div className="label">Left on benches</div><div className="val-md amber">{f1(a.leftOnBench)} pts</div></div>
      </div>
      <div style={{ overflowX: 'auto' }}>
        <table style={{ fontSize: 12, marginBottom: 12 }}>
          <thead><tr><th>Opinion</th><th style={{ textAlign: 'right' }}>Right</th><th style={{ textAlign: 'right' }}>Brier</th><th className="muted" style={{ fontWeight: 400 }}>lower is better; 0.25 is a coin flip</th></tr></thead>
          <tbody>
            {['model', 'experts', 'crowd'].map(k => {
              const s = a.sources && a.sources[k];
              if (!s || !s.n) return <tr key={k}><td style={{ textTransform: 'capitalize' }}>{k}</td><td colSpan={3} className="muted">no comparisons yet</td></tr>;
              return (
                <tr key={k}>
                  <td style={{ textTransform: 'capitalize' }}>{k}</td>
                  <td style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{s.right}/{s.n} ({pctOf(s.hitRate)})</td>
                  <td style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums' }} className={s.brier < 0.2 ? 'green' : s.brier > 0.28 ? 'red' : ''}>{Number.isFinite(s.brier) ? s.brier.toFixed(3) : '—'}</td>
                  <td></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      {a.calibration && a.calibration.some(b => b.n) && (
        <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 11, marginBottom: 12 }}>
          <span className="muted">Calibration (said → happened):</span>
          {a.calibration.filter(b => b.n).map(b => (
            <span key={b.lo}><span className="tag tag-grey">{Math.round(b.lo * 100)}–{Math.round(b.hi * 100)}%</span> {pctOf(b.predicted)} → <span className={Math.abs(b.predicted - b.observed) <= 0.1 ? 'green' : 'amber'}>{pctOf(b.observed)}</span> <span className="muted">n={b.n}</span></span>
          ))}
        </div>
      )}
      {lw && lw.leagues && lw.leagues.length > 0 && (
        <div>
          <div className="label" style={{ marginBottom: 6 }}>Week {lw.week}</div>
          <div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
            {lw.leagues.map(L => (
              <div key={L.docId}>
                <span style={{ fontWeight: 600 }}>{L.name}</span>
                {L.result && Number.isFinite(L.result.myPoints) && <span className={`tag ${L.result.won === true ? 'tag-green' : L.result.won === false ? 'tag-red' : 'tag-grey'}`} style={{ marginLeft: 8 }}>{f1(L.result.myPoints)}–{f1(L.result.oppPoints)} {L.result.won === true ? 'W' : L.result.won === false ? 'L' : ''}</span>}
                {L.bye && <span className="tag tag-grey" style={{ marginLeft: 8 }}>bye</span>}
                <span className="muted" style={{ marginLeft: 8 }}>picks {L.right}/{L.comparisons} · followed {L.followed}/{L.followable}{L.followGain ? ` (${L.followGain > 0 ? '+' : ''}${f1(L.followGain)} if all)` : ''} · set {f1(L.asSetPts)}, best {f1(L.hindsightPts)}</span>
                {(L.misses || []).slice(0, 2).map((m, i) => (
                  <div key={i} className="muted" style={{ fontSize: 11, marginLeft: 12 }}>{m.name} {f1(m.pts)}{m.over ? ` sat behind ${m.over.name} ${f1(m.over.pts)}` : ''}{m.slot ? ` (${m.slot})` : ''}</div>
                ))}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function Pipeline({ snapshot, playersMeta }) {
  const h = (snapshot && snapshot.health) || {};
  const pr = h.projections || {};
  const stateTag = (s) => s === 'ok' ? 'tag-green' : s === 'failing' ? 'tag-red' : 'tag-grey';
  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div className="section-header">Pipeline</div>
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 12 }}>
        {(h.leagues || []).map(l => (
          <span key={l.docId} title={l.lastError || ''}><span className={`tag ${stateTag(l.state)}`}>{l.state}</span> <span className="muted">{l.name}</span></span>
        ))}
        <span><span className={`tag ${pr.sleeperCount ? 'tag-green' : 'tag-grey'}`}>{pr.sleeperCount || 0}</span> <span className="muted">Sleeper projections {pr.sleeperAt ? timeAgo(pr.sleeperAt) : ''}</span></span>
        <span><span className={`tag ${pr.espnCount ? 'tag-green' : 'tag-grey'}`}>{pr.espnCount || 0}</span> <span className="muted">ESPN projections {pr.espnAt ? timeAgo(pr.espnAt) : ''}</span></span>
        {(pr.sites || []).map(x => (
          <span key={x.source} title={x.error || ''}><span className={`tag ${x.count ? (x.error ? 'tag-amber' : 'tag-green') : 'tag-red'}`}>{x.count || 0}</span> <span className="muted">{x.source} projections{x.fetchedAt ? ` ${timeAgo(x.fetchedAt)}` : ''}{x.unresolved ? ` · ${x.unresolved} unplaced` : ''}{x.error ? ' · partial' : ''}</span></span>
        ))}
        <span><span className={`tag ${h.schedule && h.schedule.ok ? 'tag-green' : 'tag-amber'}`}>{h.schedule && h.schedule.ok ? 'schedule ok' : 'schedule incomplete'}</span></span>
        {h.rankings && h.rankings.linked && (h.rankings.formats || []).map(f => (
          <span key={f.scoring} title={f.error || ''}><span className={`tag ${f.count ? 'tag-green' : 'tag-red'}`}>{f.count || 0}</span> <span className="muted">experts {f.scoring}{f.source ? ` · ${f.source}` : ''}{f.fetchedAt ? ` ${timeAgo(f.fetchedAt)}` : ''}{f.error ? ' · failed' : ''}</span></span>
        ))}
        {h.rankings && !h.rankings.linked && <span className="muted">no expert rankings yet</span>}
        <span><span className="tag tag-grey">{playersMeta ? `${playersMeta.kept} players` : 'no players'}</span> <span className="muted">{playersMeta ? `refreshed ${timeAgo(playersMeta.refreshedAt)}` : ''}</span></span>
        {snapshot && <span className="muted">snapshot built {timeAgo(snapshot.builtAt)}</span>}
      </div>
    </div>
  );
}

function Dashboard({ setTab }) {
  const { config, leagues, snapshot, playersMeta, waiverPlan, news } = useContext(DataContext);
  const linked = config && (config.sleeper || (config.espn && config.espn.leagueIds && config.espn.leagueIds.length));
  if (!linked) return (
    <div className="page">
      <div className="empty-state">
        <h3>No platform linked yet</h3>
        <div>Link Sleeper or ESPN to pull every league in read-only.</div>
        <button className="btn-primary" style={{ marginTop: 16 }} onClick={() => setTab('Settings')}>Open Settings</button>
      </div>
    </div>
  );
  const snapByDoc = Object.fromEntries(((snapshot && snapshot.leagues) || []).map(l => [l.docId, l]));
  return (
    <div className="page">
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
        <div className="page-title" style={{ margin: 0 }}>Leagues</div>
        <span className="muted" style={{ fontSize: 12 }}>{leagues.length} linked{snapshot && snapshot.week ? ` · week ${snapshot.week}` : ''}</span>
      </div>
      <ActNow snapshot={snapshot} />
      {leagues.length === 0 && (
        <div className="empty-state"><h3>No leagues found</h3><div>The linked accounts returned no leagues for the current season. Sync again from Settings once your draft is set up.</div></div>
      )}
      <div style={{ display: 'grid', gap: 16 }}>
        {leagues.map(l => <LeagueCard key={l.docId} league={l} snap={snapByDoc[l.docId] || null} />)}
      </div>
      <Wire plan={waiverPlan} />
      <News news={news} snapshot={snapshot} />
      <TrackRecord snapshot={snapshot} />
      <Pipeline snapshot={snapshot} playersMeta={playersMeta} />
    </div>
  );
}

window.FantasyViews = Object.assign(window.FantasyViews || {}, { Dashboard });
