/* Admin "Users & accounts": approve sign-ins, see every exchange account, name
   strategies (the public name is the only thing regular users ever see of a trader),
   and pick which Telegram channels the reader follows. */

const adminCell = { padding: '9px 10px', borderBottom: '1px solid var(--border-soft)', fontSize: 12, verticalAlign: 'middle' };
const adminHead = { ...adminCell, fontSize: 9.5, color: 'var(--text-4)', letterSpacing: '0.1em', textTransform: 'uppercase', textAlign: 'left', fontWeight: 500 };
const adminFmtTs = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—';

function adminFetchJson(url, opts) {
  return fetch(url, { credentials: 'include', ...(opts || {}) }).then(r => r.json())
    .catch(e => ({ ok: false, error: String(e.message || e) }));
}

function W_UsersAdmin() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <AdminUsersCard />
      <AdminAccountsCard />
      <AdminStrategiesCard />
      <AdminTelegramChannelsCard />
    </div>
  );
}

function AdminUsersCard() {
  const [users, setUsers] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [notice, setNotice] = React.useState(null);      // a green confirmation, e.g. "email sent"
  const [busyId, setBusyId] = React.useState(null);
  const [invEmail, setInvEmail] = React.useState('');
  const [invRole, setInvRole] = React.useState('user');
  const [invBusy, setInvBusy] = React.useState(false);
  const [showDeleted, setShowDeleted] = React.useState(false);

  const load = React.useCallback((keepError) => {
    fetch('/api/admin/users' + (showDeleted ? '?include_deleted=1' : ''), { credentials: 'include' }).then(r => r.json())
      .then(j => { if (j.ok) { setUsers(j.data); if (keepError !== true) setError(null); } else setError(j.error); })
      .catch(e => setError(String(e.message || e)));
  }, [showDeleted]);

  React.useEffect(() => { load(); }, [load]);

  // Soft delete / restore. A deleted person leaves the list (their row, and so the record of who joined and
  // when, stays); "Show deleted" brings them back into view.
  async function deleteOrRestore(u, kind) {
    if (kind === 'delete' && !window.confirm(
      `Delete ${u.name || u.email}?\n\nThey lose access and leave this list. The record that they joined stays, `
      + `and "Show deleted" brings them back into view. Refused if they still have open positions.`)) return;
    setBusyId(u.id); setError(null); setNotice(null);
    const j = await adminFetchJson(`/api/admin/users/${u.id}/${kind}`, { method: 'POST' });
    setBusyId(null);
    if (!j.ok) setError(j.error || 'That did not work');
    else setNotice(kind === 'delete' ? `${u.email} deleted.` : `${u.email} restored (as pending: approve to give access).`);
    load(true);
  }

  async function setRole(u, role) {
    setBusyId(u.id);
    setError(null);
    setNotice(null);
    // A request that never answers must not freeze the page: give up after 20s and say so.
    const ctl = new AbortController();
    const timer = setTimeout(() => ctl.abort(), 20000);
    try {
      const j = await fetch(`/api/admin/users/${u.id}/role`, {
        method: 'POST', credentials: 'include', signal: ctl.signal,
        headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }),
      }).then(r => r.json());
      if (!j.ok) setError(j.error || 'Could not change the role');
      else if (j.email_sent) setNotice(`Role updated. ${u.email} was notified by email.`);
      else if (j.email_error) setNotice(`Role updated, but the email to ${u.email} could not be sent (${j.email_error}).`);
    } catch (e) {
      setError(e && e.name === 'AbortError' ? 'The server did not answer in 20s; the role may not have changed.' : String((e && e.message) || e));
    } finally {
      clearTimeout(timer);
      setBusyId(null);
      load(true);          // refresh the list but keep any error just shown
    }
  }

  async function sendInvite(e) {
    e.preventDefault();
    if (!invEmail.trim()) { setError('Enter an email address to invite.'); return; }
    setInvBusy(true); setError(null); setNotice(null);
    const j = await adminFetchJson('/api/admin/users/invite', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: invEmail, role: invRole }),
    });
    setInvBusy(false);
    if (!j.ok) { setError(j.error || 'Could not send the invitation'); return; }
    setInvEmail('');
    setNotice(j.data.email_sent
      ? `Invitation sent to ${j.data.email}. They are on the list as awaiting sign-up.`
      : `${j.data.email} was added as awaiting sign-up, but the email could not be sent (${j.data.email_error}). Use Resend to try again.`);
    load(true);
  }

  async function inviteAction(u, kind) {
    setBusyId(u.id); setError(null); setNotice(null);
    const j = kind === 'resend'
      ? await adminFetchJson(`/api/admin/users/${u.id}/resend-invite`, { method: 'POST' })
      : await adminFetchJson(`/api/admin/users/${u.id}/invite`, { method: 'DELETE' });
    setBusyId(null);
    if (!j.ok) setError(j.error || 'That did not work');
    else if (kind === 'resend') setNotice(j.data.email_sent ? `Invitation sent again to ${u.email}.` : `The email to ${u.email} could not be sent (${j.data.email_error}).`);
    else setNotice(`Invitation to ${u.email} withdrawn.`);
    load(true);
  }

  const roleChip = { admin: 'gold', arena: 'info', user: 'bull', pending: 'neutral' };
  const roleName = { admin: 'admin', arena: 'arena member', user: 'user', pending: 'pending' };
  const cell = adminCell, head = adminHead, fmt = adminFmtTs;

  return (
    <div className="card">
      <div className="card-h">
        <h3>Users</h3>
        <span style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--text-3)', cursor: 'pointer' }}>
            <input type="checkbox" checked={showDeleted} onChange={e => setShowDeleted(e.target.checked)} />
            Show deleted
          </label>
          <span className="tag">{users ? users.length : '…'}</span>
        </span>
      </div>
      <div className="card-b">
        <form onSubmit={sendInvite} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
          <input type="email" value={invEmail} onChange={e => setInvEmail(e.target.value)} placeholder="Invite by email, e.g. name@example.com"
                 autoComplete="off" style={{ flex: '1 1 260px', minWidth: 220, background: 'var(--bg-elev)', color: 'var(--text)',
                 border: '1px solid var(--border-soft)', borderRadius: 6, padding: '7px 10px', fontSize: 12.5, fontFamily: 'var(--font-sans)' }} />
          <select value={invRole} onChange={e => setInvRole(e.target.value)} title="User: the investor view. Arena member: the same, plus the trader's messages and the decision history, read-only."
                  style={{ background: 'var(--bg-elev)', color: 'var(--text)', border: '1px solid var(--border-soft)', borderRadius: 6, padding: '7px 10px', fontSize: 12.5, fontFamily: 'var(--font-sans)' }}>
            <option value="user">User</option>
            <option value="arena">Arena member</option>
          </select>
          <button type="submit" disabled={invBusy} style={{ ...adminBtn('var(--gold)'), padding: '7px 14px', fontSize: 12.5, opacity: invBusy ? 0.6 : 1 }}>
            {invBusy ? 'Sending…' : 'Send invite'}
          </button>
        </form>
        <div style={{ color: 'var(--text-4)', fontSize: 11, lineHeight: 1.5, marginBottom: 14 }}>
          They get an email with how to sign in, and appear below as awaiting sign-up. When they sign in with that same
          address they get the role you chose straight away, with no approval step.
        </div>
        {error && <div style={{ color: 'var(--bear)', fontSize: 12, marginBottom: 10 }}>{error}</div>}
        {notice && <div style={{ color: 'var(--bull)', fontSize: 12, marginBottom: 10 }}>{notice}</div>}
        {!users ? (
          <div style={{ color: 'var(--text-4)', fontSize: 12 }}>Loading…</div>
        ) : (
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead>
              <tr>
                <th style={head}>User</th>
                <th style={head}>Role</th>
                <th style={head}>First seen</th>
                <th style={head}>Last seen</th>
                <th style={head}></th>
              </tr>
            </thead>
            <tbody>
              {users.map(u => (
                <tr key={u.id} style={{ opacity: u.deleted_at ? 0.55 : 1 }}>
                  <td style={cell}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      {u.picture
                        ? <img src={u.picture} alt="" style={{ width: 24, height: 24, borderRadius: '50%' }} />
                        : <span style={{ width: 24, height: 24, borderRadius: '50%', background: 'var(--bg-elev)', display: 'inline-block' }} />}
                      <div>
                        <div style={{ color: 'var(--text)', fontWeight: 500 }}>{u.name || u.email}</div>
                        <div style={{ color: 'var(--text-4)', fontSize: 10.5 }}>{u.email}</div>
                      </div>
                    </div>
                  </td>
                  <td style={cell}>
                    <span className={'chip ' + (roleChip[u.role] || 'neutral')}>{roleName[u.role] || u.role}</span>
                    {u.deleted_at && <span className="chip neutral" style={{ marginLeft: 6 }} title={`Deleted ${fmt(u.deleted_at)}`}>deleted {fmt(u.deleted_at).slice(0, 10)}</span>}
                    {u.invited && <span className="chip gold" style={{ marginLeft: 6 }} title={`Invited ${fmt(u.invited_at)}${u.invited_by ? ' by ' + u.invited_by : ''}. Has not signed in yet.`}>invited · awaiting sign-up</span>}
                  </td>
                  <td style={{ ...cell, fontFamily: 'var(--font-mono)', color: 'var(--text-3)' }}>{u.invited ? <span title="Invited">{fmt(u.invited_at)}</span> : fmt(u.first_seen_at)}</td>
                  <td style={{ ...cell, fontFamily: 'var(--font-mono)', color: 'var(--text-3)' }}>{u.invited ? '—' : fmt(u.last_seen_at)}</td>
                  <td style={{ ...cell, textAlign: 'right' }}>
                    {u.deleted_at && (
                      <button disabled={busyId === u.id} onClick={() => deleteOrRestore(u, 'restore')} style={adminBtn('var(--bull)')}>Restore</button>
                    )}
                    {!u.deleted_at && u.invited && (
                      <span style={{ display: 'inline-flex', gap: 6 }}>
                        <button disabled={busyId === u.id} onClick={() => inviteAction(u, 'resend')} style={adminBtn('var(--gold)')}>Resend</button>
                        <button disabled={busyId === u.id} onClick={() => inviteAction(u, 'revoke')} style={adminBtn('var(--bear)')}>Withdraw</button>
                      </span>
                    )}
                    {!u.deleted_at && !u.invited && u.role === 'pending' && (
                      <span style={{ display: 'inline-flex', gap: 6 }}>
                        <button disabled={busyId === u.id} onClick={() => setRole(u, 'user')} style={adminBtn('var(--bull)')}>Approve</button>
                        <button disabled={busyId === u.id} onClick={() => setRole(u, 'arena')} title="A user who can also read the trader's messages and the decision history, read-only"
                                style={adminBtn('var(--gold)')}>Approve as arena member</button>
                      </span>
                    )}
                    {!u.deleted_at && !u.invited && (u.role === 'user' || u.role === 'arena') && (
                      <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center' }}>
                        <select value={u.role} disabled={busyId === u.id} onChange={e => setRole(u, e.target.value)}
                                title="User: the investor view. Arena member: the same, plus the trader's messages and the decision history, read-only."
                                style={{ background: 'var(--bg-elev)', color: 'var(--text)', border: '1px solid var(--border-soft)',
                                         borderRadius: 6, padding: '4px 8px', fontSize: 11.5, fontFamily: 'var(--font-sans)' }}>
                          <option value="user">User</option>
                          <option value="arena">Arena member</option>
                        </select>
                        <button disabled={busyId === u.id} onClick={() => setRole(u, 'pending')} style={adminBtn('var(--bear)')}>Revoke</button>
                      </span>
                    )}
                    {!u.deleted_at && u.role !== 'admin' && !u.is_owner && (
                      <button disabled={busyId === u.id} onClick={() => deleteOrRestore(u, 'delete')}
                              title="Remove from this list (soft delete: the record stays and can be shown again)"
                              style={{ ...adminBtn('var(--text-3)'), marginLeft: 8 }}>Delete</button>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

function adminBtn(color) {
  return {
    background: 'transparent', border: '1px solid ' + color, color,
    borderRadius: 6, padding: '4px 12px', fontSize: 11.5, cursor: 'pointer', fontFamily: 'var(--font-sans)',
  };
}

const ADMIN_STATUS_CHIP = { active: 'bull', pending: 'gold', error: 'bear', disabled: 'neutral' };

function AdminAccountsCard() {
  const [rows, setRows] = React.useState(null);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    const load = () => adminFetchJson('/api/admin/accounts')
      .then(j => { if (j.ok) { setRows(j.data); setError(null); } else setError(j.error); });
    load();
    const id = setInterval(load, 60000);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="card">
      <div className="card-h">
        <h3>Exchange accounts</h3>
        <span className="tag">{rows ? rows.length : '…'}</span>
      </div>
      <div className="card-b" style={{ overflowX: 'auto' }}>
        {error && <div style={{ color: 'var(--bear)', fontSize: 12, marginBottom: 10 }}>{error}</div>}
        {!rows ? <div style={{ color: 'var(--text-4)', fontSize: 12 }}>Loading…</div>
          : !rows.length ? <div style={{ color: 'var(--text-4)', fontSize: 12 }}>No exchange accounts yet.</div> : (
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead><tr>
              {['Account', 'Owner', 'Venue', 'Env', 'Status', 'Mode', 'Follows', 'Open', 'Alert', 'Updated'].map(h =>
                <th key={h} style={adminHead}>{h}</th>)}
            </tr></thead>
            <tbody>
              {rows.map(a => (
                <tr key={a.id} style={{ opacity: a.status === 'disabled' ? 0.5 : 1 }}>
                  <td style={adminCell}>
                    <div style={{ color: 'var(--text)', fontWeight: 500 }}>#{a.id} {a.label}</div>
                    <div style={{ color: 'var(--text-4)', fontSize: 10.5 }}>{a.key_last4 ? '••••' + a.key_last4 : 'no key'}</div>
                  </td>
                  <td style={{ ...adminCell, color: 'var(--text-3)' }}>{a.owner_email}</td>
                  <td style={adminCell}>
                    <span className="chip neutral">{(a.venue || 'bybit')}</span>
                    {(a.capabilities && (!a.capabilities.trade || !a.capabilities.follow))
                      ? <span className="chip neutral" style={{ marginLeft: 6 }} title="Read-only — not for live copy">Portfolio</span>
                      : <span className="chip bull" style={{ marginLeft: 6 }}>Live-capable</span>}
                  </td>
                  <td style={adminCell}>
                    {(a.capabilities && a.capabilities.envDemoReal !== false)
                      ? <span className={'chip ' + (a.env === 'real' ? 'bear' : 'gold')}>{a.env === 'real' ? 'Real money' : 'Demo'}</span>
                      : <span style={{ color: 'var(--text-4)' }}>n/a</span>}
                  </td>
                  <td style={adminCell} title={a.last_error || ''}>
                    <span className={'chip ' + (ADMIN_STATUS_CHIP[a.status] || 'neutral')}>{a.status}</span>
                    {a.last_error && <div style={{ color: 'var(--bear)', fontSize: 10.5, marginTop: 3, maxWidth: 220 }}>{a.last_error}</div>}
                  </td>
                  <td style={adminCell}>
                    <span style={{ color: a.mode === 'live' ? 'var(--bear)' : 'var(--text-3)', fontWeight: a.mode === 'live' ? 600 : 400 }}>{a.mode === 'live' ? 'Live orders' : 'Paper'}</span>
                    {a.kill_switch ? <span className="chip bear" style={{ marginLeft: 6 }}>stopped</span> : null}
                    {a.daily_halt ? <span className="chip gold" style={{ marginLeft: 6 }}>halted</span> : null}
                  </td>
                  <td style={{ ...adminCell, fontSize: 11 }}>
                    {(a.capabilities && !a.capabilities.follow)
                      ? <span style={{ color: 'var(--text-4)' }}>n/a</span>
                      : (a.follows || []).length ? a.follows.map(f => (
                      <div key={f.strategy_id} style={{ color: f.enabled ? 'var(--text-2)' : 'var(--text-4)', whiteSpace: 'nowrap' }}>
                        {f.name} <span style={{ color: 'var(--text-4)' }}>· {f.risk_pct}%{f.max_position_pct ? ` · ≤${f.max_position_pct}%` : ''}{f.enabled ? '' : ' · off'}</span>
                      </div>
                    )) : <span style={{ color: 'var(--text-4)' }}>none</span>}
                  </td>
                  <td style={{ ...adminCell, fontFamily: 'var(--font-mono)' }}>{a.open_positions}</td>
                  <td style={{ ...adminCell, color: 'var(--bear)', fontSize: 11, maxWidth: 220 }}>{a.reconciliation_alert || ''}</td>
                  <td style={{ ...adminCell, fontFamily: 'var(--font-mono)', color: 'var(--text-3)' }}>{adminFmtTs(a.updated_at)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

function AdminStrategiesCard() {
  const [rows, setRows] = React.useState(null);
  const [draft, setDraft] = React.useState({});
  const [msg, setMsg] = React.useState(null);
  const [showRemoved, setShowRemoved] = React.useState(false);

  const load = React.useCallback(() => {
    adminFetchJson('/api/admin/strategies' + (showRemoved ? '?include_archived=1' : '')).then(j => { if (j.ok) setRows(j.data); else setMsg({ err: j.error }); });
  }, [showRemoved]);
  React.useEffect(() => { load(); }, [load]);

  // Remove = soft: the trader leaves every list (sidebar included) and is paused, but its signals,
  // trades and messages stay, and Restore brings it back.
  async function removeOrRestore(st, kind) {
    if (kind === 'archive' && !window.confirm(
      `Remove ${st.name}?\n\nIt leaves the sidebar and every list and is paused, so nothing trades it. `
      + `Its ${st.signals} signal${st.signals === 1 ? '' : 's'} and trade history stay, and "Show removed" brings it back. `
      + `Refused if a follower still has an open position on it.`)) return;
    setMsg(null);
    const j = await adminFetchJson(`/api/admin/strategies/${st.id}/${kind}`, { method: 'POST' });
    if (!j.ok) setMsg({ err: j.error });
    else { setMsg({ ok: kind === 'archive' ? `${st.name} removed.` : `${st.name} restored (still paused: press Activate to resume).` }); load(); }
  }

  async function save(st, body) {
    setMsg(null);
    const j = await adminFetchJson(`/api/admin/strategies/${st.id}`, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
    });
    if (!j.ok) setMsg({ err: j.error });
    else { setMsg({ ok: `${st.name} updated` }); setDraft(d => { const n = { ...d }; delete n[st.id]; return n; }); load(); }
  }

  const inp = {
    padding: '5px 8px', borderRadius: 6, fontSize: 12, width: 150,
    background: 'var(--bg-elev)', border: '1px solid var(--border-soft)', color: 'var(--text)',
    fontFamily: 'var(--font-sans)',
  };

  return (
    <div className="card">
      <div className="card-h">
        <h3>Strategies</h3>
        <span style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--text-3)', cursor: 'pointer' }}>
            <input type="checkbox" checked={showRemoved} onChange={e => setShowRemoved(e.target.checked)} />
            Show removed
          </label>
          <span className="tag">add traders from Telegram channels below</span>
        </span>
      </div>
      <div className="card-b" style={{ overflowX: 'auto' }}>
        {msg && <div style={{ color: msg.err ? 'var(--bear)' : 'var(--bull)', fontSize: 12, marginBottom: 10 }}>{msg.err || msg.ok}</div>}
        {!rows ? <div style={{ color: 'var(--text-4)', fontSize: 12 }}>Loading…</div> : (
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead><tr>
              {['Trader', 'Shown to users as', 'Chats', 'Signals', 'Followers', ''].map(h => <th key={h} style={adminHead}>{h}</th>)}
            </tr></thead>
            <tbody>
              {rows.map(st => {
                const d = draft[st.id];
                return (
                  <tr key={st.id} style={{ opacity: st.active && !st.archived_at ? 1 : 0.5 }}>
                    <td style={adminCell}>
                      <div style={{ color: 'var(--text)', fontWeight: 500 }}>{st.name}
                        {st.archived_at && <span className="chip neutral" style={{ marginLeft: 8, fontSize: 9.5 }}>removed {String(st.archived_at).slice(0, 10)}</span>}
                      </div>
                      <div style={{ color: 'var(--text-4)', fontSize: 10.5 }}>{st.slug}</div>
                    </td>
                    <td style={adminCell}>
                      <input style={inp} value={d !== undefined ? d : st.public_name} maxLength={40}
                             onChange={e => setDraft(x => ({ ...x, [st.id]: e.target.value }))} />
                      {d !== undefined && d !== st.public_name && (
                        <button onClick={() => save(st, { public_name: d })} style={{ ...adminBtn('var(--gold)'), marginLeft: 6 }}>Save</button>
                      )}
                    </td>
                    <td style={{ ...adminCell, fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-3)' }}>
                      {(st.chats || []).map(c => <div key={c}>{c}</div>)}
                    </td>
                    <td style={{ ...adminCell, fontFamily: 'var(--font-mono)' }}>{st.signals}</td>
                    <td style={{ ...adminCell, fontFamily: 'var(--font-mono)' }}>{st.followers}</td>
                    <td style={{ ...adminCell, textAlign: 'right' }}>
                      {st.archived_at ? (
                        <button onClick={() => removeOrRestore(st, 'restore')} style={adminBtn('var(--bull)')}>Restore</button>
                      ) : (
                        <span style={{ display: 'inline-flex', gap: 6 }}>
                          <button onClick={() => save(st, { active: st.active ? 0 : 1 })}
                                  style={adminBtn(st.active ? 'var(--text-3)' : 'var(--bull)')}>
                            {st.active ? 'Deactivate' : 'Activate'}
                          </button>
                          <button onClick={() => removeOrRestore(st, 'archive')}
                                  title="Remove from the lists (soft: the history stays and can be restored)"
                                  style={adminBtn('var(--bear)')}>Remove</button>
                        </span>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

// Every channel/group the reader's Telegram account is in (tg-reader publishes the list), with the
// switch that starts reading one as a strategy or commentary feed. The reader picks a change up in
// ~15s, no restart. Stopping a chat keeps its strategy and history: pause or remove the trader above.
function AdminTelegramChannelsCard() {
  const [data, setData] = React.useState(null);
  const [msg, setMsg] = React.useState(null);
  const [query, setQuery] = React.useState('');
  const [onlyReading, setOnlyReading] = React.useState(false);
  const [openId, setOpenId] = React.useState(null);
  const [refreshing, setRefreshing] = React.useState(false);

  const load = React.useCallback(() => adminFetchJson('/api/admin/telegram/channels').then(j => {
    if (j.ok) setData(j.data); else setMsg({ err: j.error });
    return j;
  }), []);
  React.useEffect(() => { load(); }, [load]);

  // Ask the reader to republish the list, then poll until it has (it checks every ~15s).
  async function refresh() {
    setMsg(null); setRefreshing(true);
    const r = await adminFetchJson('/api/admin/telegram/refresh', { method: 'POST' });
    if (!r.ok) { setRefreshing(false); return setMsg({ err: r.error }); }
    const asked = Date.now();
    for (let i = 0; i < 20; i++) {
      await new Promise(res => setTimeout(res, 3000));
      const j = await load();
      const synced = j.ok && j.data.account && j.data.account.dialogs_synced_at;
      if (synced && new Date(synced).getTime() >= asked - 5000) { setRefreshing(false); return setMsg({ ok: 'Channel list updated.' }); }
    }
    setRefreshing(false);
    setMsg({ err: 'The reader has not answered in a minute. Is the tg-reader process running?' });
  }

  async function stop(ch) {
    if (!window.confirm(`Stop reading ${ch.title}?\n\nNew posts stop arriving. Its messages, strategy and signals all stay; `
      + `pause or remove the trader in Strategies if you want it gone.`)) return;
    setMsg(null);
    const j = await adminFetchJson(`/api/admin/telegram/channels/${encodeURIComponent(ch.chat_id)}`, { method: 'DELETE' });
    if (!j.ok) setMsg({ err: j.error }); else { setMsg({ ok: `Stopped reading ${ch.title}.` }); load(); }
  }

  async function start(ch, body) {
    setMsg(null);
    const j = await adminFetchJson(`/api/admin/telegram/channels/${encodeURIComponent(ch.chat_id)}`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
    });
    if (!j.ok) return setMsg({ err: j.error });
    setOpenId(null);
    setMsg({ ok: `Reading ${ch.title}${j.created ? ' as a new strategy' : ''}. Posts start arriving within about 15 seconds.` });
    load();
  }

  const reading = (ch) => ch.enabled === 1;
  const rows = !data ? [] : data.channels.filter(ch =>
    (!onlyReading || reading(ch))
    && (!query || `${ch.title} ${ch.username || ''} ${ch.strategy_name || ''}`.toLowerCase().includes(query.toLowerCase())));
  const acct = data && data.account;

  return (
    <div className="card">
      <div className="card-h">
        <h3>Telegram channels</h3>
        <span style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
          {acct && (
            <span style={{ fontSize: 11.5, color: 'var(--text-4)' }}>
              {acct.tg_name ? `as ${acct.tg_name} · ` : ''}list from {adminFmtTs(acct.dialogs_synced_at)}
            </span>
          )}
          <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--text-3)', cursor: 'pointer' }}>
            <input type="checkbox" checked={onlyReading} onChange={e => setOnlyReading(e.target.checked)} />
            Only ones being read
          </label>
          <button onClick={refresh} disabled={refreshing} style={adminBtn('var(--text-3)')}>
            {refreshing ? 'Refreshing…' : 'Refresh'}
          </button>
        </span>
      </div>
      <div className="card-b" style={{ overflowX: 'auto' }}>
        {msg && <div style={{ color: msg.err ? 'var(--bear)' : 'var(--bull)', fontSize: 12, marginBottom: 10 }}>{msg.err || msg.ok}</div>}
        <input placeholder="Search channels" value={query} onChange={e => setQuery(e.target.value)}
               style={{ ...tgInput, width: 240, marginBottom: 10 }} />
        {!data ? <div style={{ color: 'var(--text-4)', fontSize: 12 }}>Loading…</div>
          : !acct || !acct.dialogs_synced_at ? (
            <div style={{ color: 'var(--text-4)', fontSize: 12 }}>
              The reader hasn't published its channel list yet. It does so when tg-reader starts; press Refresh to ask now.
            </div>
          ) : (
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead><tr>
              {['Channel', 'Last post', 'Status', ''].map(h => <th key={h} style={adminHead}>{h}</th>)}
            </tr></thead>
            <tbody>
              {rows.map(ch => (
                <React.Fragment key={ch.chat_id}>
                  <tr style={{ opacity: reading(ch) || openId === ch.chat_id ? 1 : 0.8 }}>
                    <td style={adminCell}>
                      <div style={{ color: 'var(--text)', fontWeight: 500 }}>{ch.title}</div>
                      <div style={{ color: 'var(--text-4)', fontSize: 10.5 }}>
                        {ch.missing ? 'not in the account’s list any more' : ch.kind}
                        {ch.username ? ` · @${ch.username}` : ''}
                        {ch.members ? ` · ${Number(ch.members).toLocaleString()} members` : ''}
                        <span style={{ fontFamily: 'var(--font-mono)', marginLeft: 6 }}>{ch.chat_id}</span>
                      </div>
                    </td>
                    <td style={{ ...adminCell, fontFamily: 'var(--font-mono)', color: 'var(--text-3)', whiteSpace: 'nowrap' }}>{adminFmtTs(ch.last_message_at)}</td>
                    <td style={adminCell}><TgChannelStatus ch={ch} /></td>
                    <td style={{ ...adminCell, textAlign: 'right', whiteSpace: 'nowrap' }}>
                      {reading(ch) ? (
                        <button onClick={() => stop(ch)} style={adminBtn('var(--bear)')}>Stop reading</button>
                      ) : ch.role && ch.strategy_id ? (
                        <button onClick={() => start(ch, { role: ch.role, backfill: 0 })} style={adminBtn('var(--bull)')}>Start again</button>
                      ) : !ch.missing && (
                        <button onClick={() => setOpenId(openId === ch.chat_id ? null : ch.chat_id)} style={adminBtn('var(--gold)')}>
                          {openId === ch.chat_id ? 'Cancel' : 'Use this channel'}
                        </button>
                      )}
                    </td>
                  </tr>
                  {openId === ch.chat_id && (
                    <tr><td colSpan={4} style={{ ...adminCell, background: 'var(--bg-elev)' }}>
                      <TgChannelForm ch={ch} data={data} onSubmit={body => start(ch, body)} onCancel={() => setOpenId(null)} />
                    </td></tr>
                  )}
                </React.Fragment>
              ))}
              {!rows.length && (
                <tr><td colSpan={4} style={{ ...adminCell, color: 'var(--text-4)' }}>No channels match.</td></tr>
              )}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

const tgInput = {
  padding: '5px 8px', borderRadius: 6, fontSize: 12,
  background: 'var(--bg-elev)', border: '1px solid var(--border-soft)', color: 'var(--text)',
  fontFamily: 'var(--font-sans)',
};

function TgChannelStatus({ ch }) {
  const strat = ch.strategy_id && (
    <span>{ch.strategy_name} <span style={{ color: 'var(--text-4)' }}>· shown as {ch.public_name}
      {ch.strategy_archived_at ? ' · removed' : !ch.strategy_active ? ' · paused' : ''}</span></span>
  );
  if (ch.enabled === 1) {
    return (
      <div>
        <span className={'chip ' + (ch.missing ? 'bear' : 'bull')} style={{ fontSize: 9.5, marginRight: 8 }}>
          {ch.missing ? 'reading · not found' : 'reading'}
        </span>
        {ch.role === 'signal_source' ? strat : <span style={{ color: 'var(--text-3)' }}>{ch.role.replace('_', ' ')}</span>}
      </div>
    );
  }
  if (ch.role) {
    return <div style={{ color: 'var(--text-4)' }}>stopped{ch.strategy_id ? <> · {strat}</> : ''}</div>;
  }
  return <span style={{ color: 'var(--text-4)' }}>not read</span>;
}

function TgChannelForm({ ch, data, onSubmit, onCancel }) {
  const [role, setRole] = React.useState('signal_source');
  const [mode, setMode] = React.useState(ch.strategy_id ? 'existing' : 'new');
  const [strategyId, setStrategyId] = React.useState(ch.strategy_id || (data.strategies[0] && data.strategies[0].id) || '');
  const [name, setName] = React.useState(ch.title.trim().slice(0, 80));
  const [publicName, setPublicName] = React.useState(data.next_public_name);
  const [profile, setProfile] = React.useState('default');
  const [backfill, setBackfill] = React.useState(100);
  const [busy, setBusy] = React.useState(false);

  async function submit() {
    setBusy(true);
    const body = { role, backfill: Number(backfill) };
    if (role === 'signal_source') {
      if (mode === 'existing') body.strategy_id = strategyId;
      else body.strategy = { name, public_name: publicName, profile };
    }
    await onSubmit(body);
    setBusy(false);
  }

  const field = (labelText, el, hint) => (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--text-3)' }}>
      {labelText}{el}{hint && <span style={{ color: 'var(--text-4)', fontSize: 10.5 }}>{hint}</span>}
    </label>
  );

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, alignItems: 'flex-end' }}>
      {field('Read it as', (
        <select value={role} onChange={e => setRole(e.target.value)} style={tgInput}>
          <option value="signal_source">Signal source (a strategy)</option>
          <option value="commentary">Commentary (Telegram page only, never traded)</option>
          <option value="news">News (shown on the Overview page, never traded)</option>
        </select>
      ))}
      {role === 'signal_source' && field('Trader', (
        <select value={mode} onChange={e => setMode(e.target.value)} style={tgInput} disabled={!!ch.strategy_id}>
          <option value="new">New trader</option>
          {data.strategies.length > 0 && <option value="existing">Existing trader (another channel of theirs)</option>}
        </select>
      ))}
      {role === 'signal_source' && mode === 'existing' && field('Strategy', (
        <select value={strategyId} onChange={e => setStrategyId(e.target.value)} style={tgInput} disabled={!!ch.strategy_id}>
          {data.strategies.map(s => <option key={s.id} value={s.id}>{s.name} ({s.public_name})</option>)}
        </select>
      ))}
      {role === 'signal_source' && mode === 'new' && <>
        {field('Trader name (admin only)', <input value={name} maxLength={80} onChange={e => setName(e.target.value)} style={{ ...tgInput, width: 180 }} />)}
        {field('Shown to users as', <input value={publicName} maxLength={40} onChange={e => setPublicName(e.target.value)} style={{ ...tgInput, width: 140 }} />)}
        {field('How posts are read', (
          <select value={profile} onChange={e => setProfile(e.target.value)} style={tgInput}>
            {data.profiles.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
          </select>
        ))}
      </>}
      {field('Past posts to import', (
        <input type="number" min={0} max={500} value={backfill} onChange={e => setBackfill(e.target.value)} style={{ ...tgInput, width: 80 }} />
      ), role === 'signal_source' ? 'each one goes through the signal pipeline' : null)}
      <span style={{ display: 'inline-flex', gap: 6 }}>
        <button onClick={submit} disabled={busy} style={adminBtn('var(--bull)')}>{busy ? 'Adding…' : 'Start reading'}</button>
        <button onClick={onCancel} style={adminBtn('var(--text-3)')}>Cancel</button>
      </span>
    </div>
  );
}
