// fantasy/src/settings.jsx — link platforms, sync, and see what's wired.
//
// Sleeper links by username (its API is keyless; a username is public data,
// so this is config, not a credential). ESPN links with the espn_s2 + SWID
// cookies of a logged-in browser plus the league ids — those cookies ARE a
// credential, go to their own document, and are never read back here: the
// page shows when they were saved and whether the last sync accepted them.

const { useState, useContext } = React;
const { DataContext, useToast, SyncChip, timeAgo } = window.FantasyShared;

function useCall() {
  const { reload } = useContext(DataContext);
  const toast = useToast();
  const [busy, setBusy] = useState(null);
  const call = async (name, args, label) => {
    setBusy(name);
    try {
      const res = await fns.httpsCallable(name)(args || {});
      const d = res.data || {};
      const leagues = d.leagues || [];
      const failed = leagues.filter(l => !l.ok);
      const teamless = leagues.filter(l => l.ok && !l.myTeamId);
      toast(`${label}: ${leagues.length} league(s)${failed.length ? `, ${failed.length} failed — ${failed[0].error}` : ''}${teamless.length ? `, ${teamless.length} without a team — pick it in the Leagues table` : ''}${d.players ? ` · ${d.players.kept} players loaded` : ''}`, failed.length && failed.length === leagues.length ? 'error' : 'success');
      await reload();
      return d;
    } catch (e) {
      toast(e.message || String(e), 'error');
      return null;
    } finally { setBusy(null); }
  };
  return { busy, call, setBusy, toast, reload };
}

// "My team" is matched from the credential (SWID / Sleeper user id); when
// the platform's owner list does not carry it, the picker is the fallback
// and the choice survives every resync.
function MyTeamCell({ league }) {
  const { busy, call, setBusy, toast, reload } = useCall();
  const rosters = league.rosters || [];
  const choose = async (teamId) => {
    setBusy('team');
    try { await fns.httpsCallable('fantasySetMyTeam')({ docId: league.docId, teamId }); toast(teamId ? 'Team set — resyncing' : 'Team choice cleared'); await reload(); }
    catch (e) { toast(e.message || String(e), 'error'); }
    finally { setBusy(null); }
  };
  if (league.myTeamName && league.myTeamSource !== 'override') return <span>{league.myTeamName}</span>;
  return (
    <div style={{ display: 'grid', gap: 4 }}>
      {league.myTeamName ? <span>{league.myTeamName} <span className="tag tag-grey">picked</span></span> : <span className="amber">not found</span>}
      {league.myTeamDiagnostic && !league.myTeamName && <div className="muted" style={{ fontSize: 11 }}>{league.myTeamDiagnostic}</div>}
      {rosters.length > 0 && (
        <select value={league.myTeamOverride || ''} disabled={busy === 'team'} onChange={e => choose(e.target.value)} style={{ fontSize: 11, padding: '4px 8px', maxWidth: 260 }}>
          <option value="">{league.myTeamName ? 'use the cookie match instead' : 'pick your team…'}</option>
          {rosters.map(r => <option key={r.teamId} value={r.teamId}>{r.teamName || r.ownerName || `Team ${r.teamId}`}{r.ownerName && r.teamName ? ` (${r.ownerName})` : ''}</option>)}
        </select>
      )}
      {!rosters.length && <span className="muted" style={{ fontSize: 11 }}>{league.sourceReport && league.sourceReport.teams > 0 ? `ESPN returned ${league.sourceReport.teams} teams but none are stored — sync again` : 'no rosters synced yet'}</span>}
    </div>
  );
}

// The per-add fee: read from the platform where published, overridden here
// when the commissioner collects it off-platform. It is the input the
// waiver tree and the news triage are disciplined by.
function FeeCell({ league }) {
  const { busy, setBusy, toast, reload } = useCall();
  const published = league.fees && Number.isFinite(league.fees.acquisition) ? league.fees.acquisition : null;
  const override = league.feeOverride && Number.isFinite(league.feeOverride.acquisition) ? league.feeOverride.acquisition : null;
  const [val, setVal] = useState(override != null ? String(override) : '');
  const save = async () => {
    const next = val.trim() === '' ? null : Number(val);
    if (next === override) return;
    setBusy('fee');
    try { await fns.httpsCallable('fantasySetLeagueRules')({ docId: league.docId, acquisitionFee: next }); toast(next == null ? 'Fee override cleared' : `$${next} per add saved`); await reload(); }
    catch (e) { toast(e.message || String(e), 'error'); }
    finally { setBusy(null); }
  };
  const effective = override != null ? override : (published != null ? published : 0);
  return (
    <div style={{ display: 'grid', gap: 4 }}>
      <span>${effective}{override != null ? <span className="tag tag-grey" style={{ marginLeft: 6 }}>override</span> : (published != null && league.platform === 'espn' ? <span className="tag tag-grey" style={{ marginLeft: 6 }}>espn</span> : null)}</span>
      <input value={val} placeholder={published != null ? `platform says $${published}` : 'per add, $'} disabled={busy === 'fee'} onChange={e => setVal(e.target.value)} onBlur={save} onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }} style={{ fontSize: 11, padding: '4px 8px', width: 110 }} />
    </div>
  );
}

function LeagueTable({ leagues }) {
  const { busy, setBusy, toast, reload } = useCall();
  if (!leagues.length) return null;
  const unlink = async (l) => {
    if (!window.confirm(`Remove ${l.name || l.leagueId} from the Control Center? Nothing changes on the platform.`)) return;
    setBusy('unlink');
    try { const r = await fns.httpsCallable('fantasyUnlinkLeague')({ docId: l.docId }); toast(`Removed ${l.name || l.leagueId}${r.data.note ? ` — ${r.data.note}` : ''}`); await reload(); }
    catch (e) { toast(e.message || String(e), 'error'); }
    finally { setBusy(null); }
  };
  return (
    <>
      <hr className="divider" />
      <div className="section-header">Leagues</div>
      <table>
        <thead><tr><th>League</th><th>Season</th><th>Status</th><th>My team</th><th>Fee / add</th><th>Sync</th><th></th></tr></thead>
        <tbody>
          {leagues.map(l => (
            <tr key={l.docId}>
              <td>{l.name || l.leagueId}<div className="muted" style={{ fontSize: 10 }}>{l.leagueId}</div></td>
              <td className="muted">{l.season}</td>
              <td className="muted">{l.status || '—'}</td>
              <td><MyTeamCell league={l} /></td>
              <td><FeeCell league={l} /></td>
              <td><SyncChip league={l} />{l.syncHealth && l.syncHealth.state === 'failing' && <div className="red" style={{ fontSize: 11, marginTop: 4 }}>{l.syncHealth.lastError}</div>}</td>
              <td><button className="btn-secondary" style={{ fontSize: 11, padding: '3px 8px' }} disabled={busy === 'unlink'} onClick={() => unlink(l)}>remove</button></td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

function SleeperCard() {
  const { config, leagues } = useContext(DataContext);
  const { busy, call } = useCall();
  const linked = config && config.sleeper;
  const [username, setUsername] = useState(linked ? linked.username || '' : '');
  const mine = leagues.filter(l => l.platform === 'sleeper');
  return (
    <div className="card">
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 15, fontWeight: 700 }}>Sleeper</div>
        {linked ? <span className="tag tag-green">linked · @{linked.username}</span> : <span className="tag tag-grey">not linked</span>}
        <span className="tag tag-blue">read · sanctioned API</span>
      </div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', maxWidth: 520 }}>
        <input value={username} onChange={e => setUsername(e.target.value)} placeholder="Sleeper username" onKeyDown={e => { if (e.key === 'Enter' && username.trim()) call('fantasyLinkSleeper', { username: username.trim() }, 'Linked'); }} />
        <button className="btn-primary" disabled={!!busy || !username.trim()} onClick={() => call('fantasyLinkSleeper', { username: username.trim() }, 'Linked')}>
          {busy === 'fantasyLinkSleeper' ? <span className="spinner" style={{ width: 14, height: 14 }} /> : (linked ? 'Relink' : 'Link')}
        </button>
      </div>
      {linked && <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Linked {timeAgo(linked.linkedAt)}.</div>}
      <LeagueTable leagues={mine} />
    </div>
  );
}

function EspnCard() {
  const { config, leagues } = useContext(DataContext);
  const { busy, call } = useCall();
  const linked = config && config.espn && config.espn.leagueIds && config.espn.leagueIds.length ? config.espn : null;
  const [s2, setS2] = useState('');
  const [swid, setSwid] = useState('');
  const [leagueIds, setLeagueIds] = useState(linked ? linked.leagueIds.join(', ') : '');
  const [season, setSeason] = useState(linked && linked.season ? String(linked.season) : '');
  const mine = leagues.filter(l => l.platform === 'espn');
  const failing = mine.filter(l => l.syncHealth && l.syncHealth.state === 'failing');
  // First link needs the cookies; afterwards a league id alone is enough
  // (the saved credential is reused) and fresh cookies are optional.
  const ready = leagueIds.trim() && (linked || (s2.trim() && swid.trim()));
  return (
    <div className="card">
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 15, fontWeight: 700 }}>ESPN</div>
        {linked ? <span className="tag tag-green">linked · {linked.leagueIds.length} league(s)</span> : <span className="tag tag-grey">not linked</span>}
        <span className="tag tag-amber">read · unofficial (cookies)</span>
        {failing.length > 0 && <span className="tag tag-red">cookies rejected — paste fresh ones</span>}
      </div>
      <div className="muted" style={{ fontSize: 12, lineHeight: 1.6, marginBottom: 12 }}>
        ESPN has no public API. Reads go through the endpoints its own web app uses, authenticated with the
        <code> espn_s2 </code> and <code> SWID </code> cookies from a logged-in browser (DevTools → Application → Cookies → espn.com).
        They expire roughly yearly and on a password change; when a sync is refused the league shows it here. The league id is the
        number in the league URL (<code>leagueId=…</code>). Relinking adds leagues and never drops one; use "remove" on a row to drop it.
      </div>
      <div style={{ display: 'grid', gap: 8, maxWidth: 640 }}>
        <input value={s2} onChange={e => setS2(e.target.value)} placeholder={linked ? 'espn_s2 cookie value (leave blank to keep the saved one)' : 'espn_s2 cookie value'} autoComplete="off" />
        <div className="grid-2" style={{ gap: 8 }}>
          <input value={swid} onChange={e => setSwid(e.target.value)} placeholder={linked ? 'SWID (blank = keep saved)' : 'SWID cookie value ({…})'} autoComplete="off" />
          <input value={season} onChange={e => setSeason(e.target.value)} placeholder="Season (blank = current)" />
        </div>
        <input value={leagueIds} onChange={e => setLeagueIds(e.target.value)} placeholder="League ids, comma-separated" />
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <button className="btn-primary" disabled={!!busy || !ready} onClick={() => call('fantasyLinkEspn', { s2: s2.trim(), swid: swid.trim(), leagueIds, season: season.trim() || null }, 'ESPN linked')}>
            {busy === 'fantasyLinkEspn' ? <span className="spinner" style={{ width: 14, height: 14 }} /> : (linked ? (s2.trim() || swid.trim() ? 'Relink with fresh cookies' : 'Add / sync leagues') : 'Link')}
          </button>
          {linked && <span className="muted" style={{ fontSize: 11 }}>Cookies saved {timeAgo(linked.linkedAt)} · linked leagues {linked.leagueIds.join(', ')}</span>}
        </div>
      </div>
      <LeagueTable leagues={mine} />
    </div>
  );
}

// Expert consensus rankings. Free by default: the public FantasyPros
// rankings pages are read with the sync. A key (Hall of Fame plan) is
// optional and switches the pull to the API; it is a credential, stored
// beside the ESPN cookies and never read back here.
function FantasyProsCard() {
  const { config, snapshot } = useContext(DataContext);
  const { busy, call } = useCall();
  const linked = config && config.fantasypros ? config.fantasypros : null;
  const [key, setKey] = useState('');
  const h = snapshot && snapshot.health && snapshot.health.rankings;
  const formats = (h && h.formats) || [];
  return (
    <div className="card" style={{ marginTop: 16 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 15, fontWeight: 700 }}>FantasyPros</div>
        <span className={`tag ${linked ? 'tag-green' : 'tag-grey'}`}>{linked ? 'API key saved' : 'free pages'}</span>
        <span className="tag tag-amber">experts · consensus of ~130</span>
      </div>
      <p className="muted" style={{ fontSize: 12, marginTop: 8 }}>
        The picks' <b>Experts</b> column is FantasyPros' weekly Expert Consensus Ranking — each player's average, best and worst rank across their experts, and the spread — turned into "how often the experts start A over B". The public rankings pages are read with every sync at no cost (unofficial, and it stops if the pages change; the Pipeline card says when). An API key from the Hall of Fame plan is optional and moves the pull to their API.
      </p>
      <div className="row" style={{ marginTop: 10, gap: 8, flexWrap: 'wrap' }}>
        <input value={key} onChange={e => setKey(e.target.value)} placeholder={linked ? 'new API key (optional)' : 'API key (optional)'} style={{ minWidth: 320 }} />
        <button className="btn-primary" disabled={!!busy || !key.trim()} onClick={() => call('fantasyLinkFantasyPros', { apiKey: key.trim() }, 'FantasyPros key')}>{busy === 'fantasyLinkFantasyPros' ? <span className="spinner" style={{ width: 14, height: 14 }} /> : 'Save key'}</button>
        {linked && <button className="btn-secondary" disabled={!!busy} onClick={() => call('fantasyLinkFantasyPros', { apiKey: '' }, 'FantasyPros key removed')}>Remove key</button>}
      </div>
      <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>
        {formats.length ? formats.map(f => <span key={f.scoring} style={{ marginRight: 12 }}><span className={`tag ${f.count ? 'tag-green' : 'tag-red'}`}>{f.count || 0} rows</span> {f.scoring}{f.source ? ` via ${f.source}` : ''}{f.fetchedAt ? ` · ${timeAgo(f.fetchedAt)}` : ''}{f.experts ? ` · ${f.experts} experts` : ''}{f.error ? <span className="red"> · {f.error}</span> : null}</span>)
          : 'No rankings pulled yet — they arrive with the next sync once a league is linked.'}
      </div>
    </div>
  );
}

function SyncCard() {
  const { config, playersMeta, snapshot } = useContext(DataContext);
  const { busy, call, setBusy, toast, reload } = useCall();
  const linked = config && (config.sleeper || (config.espn && config.espn.leagueIds && config.espn.leagueIds.length));
  if (!linked) return null;
  return (
    <div className="card">
      <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 8 }}>Sync</div>
      <div className="muted" style={{ fontSize: 12, marginBottom: 12 }}>
        Leagues, schedule and projections sync every 30 minutes; the wire (ESPN news + injuries, Sleeper trending) every 10;
        the player master refreshes daily at 06:00 ET and the waiver plan builds Tuesday 06:00 ET. Lineup alerts, material news about
        your players, and the Tuesday plan go to the Telegram chat linked in the running app.
      </div>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
        <button className="btn-secondary" disabled={!!busy} onClick={() => call('fantasySyncLeagues', {}, 'Synced')}>{busy === 'fantasySyncLeagues' ? 'Syncing…' : 'Sync everything now'}</button>
        <button className="btn-secondary" disabled={!!busy} onClick={async () => {
          setBusy('plan');
          try { const r = await fns.httpsCallable('fantasyBuildWaiverPlan')({}); toast(`Waiver plan built for week ${r.data.week}: ${r.data.leagues.reduce((n, l) => n + l.targets, 0)} target(s)`); await reload(); }
          catch (e) { toast(e.message || String(e), 'error'); }
          finally { setBusy(null); }
        }}>{busy === 'plan' ? 'Building…' : 'Build waiver plan now'}</button>
        <button className="btn-secondary" disabled={!!busy} onClick={async () => {
          setBusy('players');
          try { const r = await fns.httpsCallable('fantasyRefreshPlayers')({}); toast(`Player master: ${r.data.kept} of ${r.data.total} rows kept`); await reload(); }
          catch (e) { toast(e.message || String(e), 'error'); }
          finally { setBusy(null); }
        }}>{busy === 'players' ? 'Refreshing…' : 'Refresh player master'}</button>
        <span className="muted" style={{ fontSize: 11 }}>
          {playersMeta ? `${playersMeta.kept} players, refreshed ${timeAgo(playersMeta.refreshedAt)}` : 'player master not loaded'}
          {snapshot ? ` · snapshot ${timeAgo(snapshot.builtAt)}` : ''}
        </span>
      </div>
    </div>
  );
}

function Settings() {
  return (
    <div className="page">
      <div className="page-title">Settings</div>
      <div style={{ display: 'grid', gap: 16 }}>
        <SleeperCard />
        <EspnCard />
        <FantasyProsCard />
        <SyncCard />
      </div>
    </div>
  );
}

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