// Admin view — add / edit / pause / delete endpoints.
// Renders a top add-endpoint card plus a table of every configured server.
// All mutations go through adminFetch (Basic auth); on 401 the login prompt
// is rendered as an overlay.

const ADMIN_INPUT = {
  width: '100%', padding: '5px 9px', background: 'var(--surface)',
  border: '1px solid var(--border-2)', borderRadius: 2,
  color: 'var(--text)', fontFamily: 'var(--mono)', fontSize: 12, outline: 'none',
};

function AddEndpointCard({ groups, onAdded }) {
  const [url, setUrl] = React.useState('');
  const [group, setGroup] = React.useState(groups[0] || '');
  const [customGroup, setCustomGroup] = React.useState(false);
  const [probePath, setProbePath] = React.useState('');
  const [okCodes, setOkCodes] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  React.useEffect(() => {
    if (!group && groups.length) setGroup(groups[0]);
  }, [groups]);

  async function submit(e) {
    e.preventDefault();
    setErr(null); setBusy(true);
    try {
      const body = { url: url.trim(), group: group.trim() };
      if (probePath.trim()) body.probePath = probePath.trim();
      if (okCodes.trim()) {
        body.okCodes = okCodes.split(/[,\s]+/).filter(Boolean).map(x => {
          const n = Number(x);
          if (!Number.isFinite(n)) throw new Error(`okCodes: "${x}" is not a number`);
          return Math.floor(n);
        });
      }
      const created = await window.adminFetch('/api/admin/servers', {
        method: 'POST', body: JSON.stringify(body),
      });
      onAdded(created);
      setUrl(''); setProbePath(''); setOkCodes('');
      setCustomGroup(false);
    } catch (e2) {
      setErr(e2.message || 'add failed');
    } finally { setBusy(false); }
  }

  return (
    <form onSubmit={submit} className="card" style={{ padding: 0 }}>
      <div className="card-head">
        <h3>+ Add endpoint</h3>
        <span className="spacer" />
        <span className="sub">creates a new node and probes it immediately</span>
      </div>
      <div className="card-body" style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr auto', gap: 10, alignItems: 'end' }}>
        <div className="tweak-row">
          <label>URL</label>
          <input style={ADMIN_INPUT} required placeholder="https://example.com" value={url} onChange={e => setUrl(e.target.value)} />
        </div>
        <div className="tweak-row">
          <label>Group</label>
          {customGroup ? (
            <input style={ADMIN_INPUT} autoFocus placeholder="new group" value={group} onChange={e => setGroup(e.target.value)} />
          ) : (
            <select style={ADMIN_INPUT} value={group} onChange={e => {
              if (e.target.value === '__new__') { setCustomGroup(true); setGroup(''); }
              else setGroup(e.target.value);
            }}>
              {groups.map(g => <option key={g} value={g}>{g}</option>)}
              <option value="__new__">+ new group…</option>
            </select>
          )}
        </div>
        <div className="tweak-row">
          <label>Probe path</label>
          <input style={ADMIN_INPUT} placeholder="/ (default)" value={probePath} onChange={e => setProbePath(e.target.value)} />
        </div>
        <div className="tweak-row">
          <label>ok codes</label>
          <input style={ADMIN_INPUT} placeholder="e.g. 401,404" value={okCodes} onChange={e => setOkCodes(e.target.value)} />
        </div>
        <button type="submit" className="btn" disabled={busy || !url.trim() || !group.trim()}
          style={{ color: 'var(--accent)', borderColor: 'var(--accent-dim)', background: 'var(--accent-bg)', padding: '6px 14px' }}>
          {busy ? 'adding…' : 'add'}
        </button>
      </div>
      {err && <div style={{ padding: '0 12px 10px', color: 'var(--crit)', fontSize: 11 }}>{err}</div>}
    </form>
  );
}

function AdminRow({ server, runtime, onEdit, onTogglePause, onDelete, onRecaptureShot }) {
  const [confirming, setConfirming] = React.useState(false);
  const [shotBusy, setShotBusy] = React.useState(false);
  React.useEffect(() => {
    if (!confirming) return;
    const t = setTimeout(() => setConfirming(false), 4000);
    return () => clearTimeout(t);
  }, [confirming]);

  const paused = !!server.paused;
  const status = paused ? 'maint' : (runtime?.status || 'unknown');
  const shot = runtime?.screenshot || null;

  async function recap() {
    setShotBusy(true);
    try { await onRecaptureShot(server); }
    finally { setShotBusy(false); }
  }

  return (
    <tr>
      <td><span className={`dot ${status}`} /></td>
      <td style={{ fontWeight: 500 }}>{server.url}</td>
      <td className="mute">{server.group}</td>
      <td className="mute" style={{ maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis' }}>
        {server.probePath || <span style={{ color: 'var(--text-mute)' }}>—</span>}
      </td>
      <td className="num mute">
        {server.okCodes && server.okCodes.length
          ? server.okCodes.join(', ')
          : <span style={{ color: 'var(--text-mute)' }}>—</span>}
      </td>
      <td>
        <span className={`chip ${paused ? 'maint' : status}`}>
          {paused ? 'paused' : status}
        </span>
      </td>
      <td style={{ textAlign: 'right' }}>
        <div style={{ display: 'inline-flex', gap: 4 }}>
          <button className="btn" onClick={recap} disabled={shotBusy}
            title={shot && shot.exists ? `captured ${window.fmtRel(shot.capturedAt)}` : 'no screenshot yet'}>
            {shotBusy ? '…' : 'shot'}
          </button>
          <button className="btn" onClick={() => onEdit(server)}>edit</button>
          <button className="btn" onClick={() => onTogglePause(server)}
            style={paused ? { color: 'var(--ok)', borderColor: 'oklch(0.72 0.16 150 / 0.4)' } : {}}>
            {paused ? 'resume' : 'pause'}
          </button>
          {confirming ? (
            <button className="btn" onClick={() => { setConfirming(false); onDelete(server); }}
              style={{ color: 'var(--crit)', borderColor: 'oklch(0.66 0.22 27 / 0.5)', background: 'var(--crit-bg)' }}>
              confirm?
            </button>
          ) : (
            <button className="btn" onClick={() => setConfirming(true)}>delete</button>
          )}
        </div>
      </td>
    </tr>
  );
}

function Admin({ runtimeNodes, groups, onNodesChanged, currentUser }) {
  const auth = window.useAdminAuth();
  const [servers, setServers] = React.useState(null);
  const [editing, setEditing] = React.useState(null);
  const [loadErr, setLoadErr] = React.useState(null);
  const [filter, setFilter] = React.useState('');
  // Track an in-flight "refresh all screenshots" cycle so the button can
  // show progress without blocking the UI. We poll /api/nodes (which
  // already exposes screenshot.capturedAt) and count how many have
  // flipped past the `startedAt` watermark.
  const [shotJob, setShotJob] = React.useState(null);  // { startedAt, total }

  async function load() {
    setLoadErr(null);
    try {
      const list = await window.adminFetch('/api/admin/servers');
      setServers(list);
    } catch (e) {
      if (e.message === 'unauthorized') setServers(null);
      else setLoadErr(e.message);
    }
  }

  React.useEffect(() => { if (auth.authed) load(); else setServers(null); }, [auth.authed]);

  // Hooks must run in a stable order on every render, so memos live
  // above the early-return branches. They tolerate `servers` being null.
  const runtimeById = React.useMemo(() => {
    const m = {};
    for (const n of runtimeNodes || []) m[n.id] = n;
    return m;
  }, [runtimeNodes]);

  const rows = React.useMemo(() => {
    if (!servers) return [];
    const f = filter.trim().toLowerCase();
    if (!f) return servers;
    return servers.filter(s =>
      s.url.toLowerCase().includes(f) ||
      (s.group || '').toLowerCase().includes(f) ||
      (s.probePath || '').toLowerCase().includes(f)
    );
  }, [servers, filter]);

  // While a refresh job is running, count how many nodes have a
  // screenshot captured *after* the job kicked off. That's our progress.
  // Declared before early returns so hook order stays stable.
  const shotProgress = React.useMemo(() => {
    if (!shotJob) return null;
    let done = 0;
    for (const n of runtimeNodes || []) {
      const s = n.screenshot;
      if (s && s.capturedAt >= shotJob.startedAt) done++;
    }
    return { done, total: shotJob.total };
  }, [shotJob, runtimeNodes]);
  React.useEffect(() => {
    if (!shotProgress) return;
    if (shotProgress.done >= shotProgress.total) {
      // Small delay so the final "done/total" state is visible briefly.
      const t = setTimeout(() => setShotJob(null), 1500);
      return () => clearTimeout(t);
    }
  }, [shotProgress?.done, shotProgress?.total]);

  if (!auth.authed) return <div style={{ position: 'relative', minHeight: 300 }}><window.AdminAuthPrompt /></div>;
  if (loadErr) return <div style={{ padding: 20, color: 'var(--crit)' }}>Failed to load admin: {loadErr}</div>;
  if (!servers) return <div style={{ padding: 20, color: 'var(--text-mute)' }}>Loading…</div>;

  async function onAdded(_created) {
    await load();
    onNodesChanged && onNodesChanged();
  }
  async function onSaved(updated) {
    setEditing(null);
    setServers(cur => cur.map(s => s.id === updated.id ? ({ ...s, ...updated }) : s));
    onNodesChanged && onNodesChanged();
  }
  async function onTogglePause(s) {
    const path = s.paused ? 'resume' : 'pause';
    try {
      await window.adminFetch(`/api/admin/servers/${s.id}/${path}`, { method: 'POST' });
      setServers(cur => cur.map(x => x.id === s.id ? { ...x, paused: !s.paused } : x));
      onNodesChanged && onNodesChanged();
    } catch (e) { setLoadErr(e.message); }
  }
  async function onDelete(s) {
    try {
      await window.adminFetch(`/api/admin/servers/${s.id}`, { method: 'DELETE' });
      setServers(cur => cur.filter(x => x.id !== s.id));
      onNodesChanged && onNodesChanged();
    } catch (e) { setLoadErr(e.message); }
  }
  async function onRecaptureShot(s) {
    try {
      await window.adminFetch(`/api/admin/screenshots/${s.id}`, { method: 'POST' });
      onNodesChanged && onNodesChanged();
    } catch (e) { setLoadErr(e.message); }
  }
  async function refreshAllShots() {
    try {
      const startedAt = Date.now();
      const res = await window.adminFetch('/api/admin/screenshots/refresh', { method: 'POST' });
      setShotJob({ startedAt, total: res.queued || servers.length });
    } catch (e) { setLoadErr(e.message); }
  }

  return (
    <div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
      <AddEndpointCard groups={groups} onAdded={onAdded} />

      <div className="card" style={{ padding: 0 }}>
        <div className="card-head">
          <h3>Endpoints</h3>
          <span className="sub">{servers.length} total</span>
          <span className="spacer" />
          {shotProgress && (
            <span style={{ fontSize: 10, color: 'var(--accent)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>
              shots {shotProgress.done}/{shotProgress.total}
            </span>
          )}
          <button className="btn" onClick={refreshAllShots} disabled={!!shotJob}
            title="capture a fresh screenshot for every non-paused endpoint right now">
            {shotJob ? 'refreshing…' : 'refresh screenshots'}
          </button>
          <input style={{ ...ADMIN_INPUT, width: 220 }}
            placeholder="filter by url / group / path"
            value={filter} onChange={e => setFilter(e.target.value)} />
          <button className="btn" onClick={() => auth.signOut()}>sign out</button>
        </div>
        <div style={{ overflow: 'auto', maxHeight: 'calc(100vh - 280px)' }}>
          <table className="tbl">
            <thead>
              <tr>
                <th></th>
                <th>url</th>
                <th>group</th>
                <th>probe path</th>
                <th>ok codes</th>
                <th>status</th>
                <th style={{ textAlign: 'right' }}>actions</th>
              </tr>
            </thead>
            <tbody>
              {rows.map(s => (
                <AdminRow key={s.id} server={s} runtime={runtimeById[s.id]}
                  onEdit={setEditing}
                  onTogglePause={onTogglePause}
                  onDelete={onDelete}
                  onRecaptureShot={onRecaptureShot} />
              ))}
            </tbody>
          </table>
        </div>
      </div>

      <window.AdminEditor server={editing} groups={groups}
        onClose={() => setEditing(null)} onSaved={onSaved} />
    </div>
  );
}

Object.assign(window, { Admin });
