

const { useState: useStateSc, useEffect: useEffectSc, useMemo: useMemoSc, useRef: useRefSc } = React;

function uid() { return Math.random().toString(36).slice(2, 10); }

const MAP_CODE_RE = /^\d{4}-\d{4}-\d{4}$/;

const ScheduleAPI = {
  async list() {
    const r = await fetch("/api/schedules");
    if (!r.ok) throw new Error(`list HTTP ${r.status}`);
    const data = await r.json();
    return (data.schedules || []).map(srvToFE);
  },
  async create(fe) {
    const body = feToSrv(fe);
    const r = await fetch("/api/schedules", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`create HTTP ${r.status}: ${await r.text()}`);
    return r.json();
  },
  async update(id, fe) {
    const body = feToSrv(fe);
    const r = await fetch(`/api/schedules/${encodeURIComponent(id)}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error(`update HTTP ${r.status}: ${await r.text()}`);
    return r.json();
  },
  async remove(id) {
    const r = await fetch(`/api/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
    if (!r.ok) throw new Error(`delete HTTP ${r.status}`);
  },
  async start(id) {
    const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/start`, { method: "POST" });
    if (!r.ok) throw new Error(`start HTTP ${r.status}`);
  },
  async stop(id) {
    const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/stop`, { method: "POST" });
    if (!r.ok) throw new Error(`stop HTTP ${r.status}`);
  },
  async skipStage(id) {
    const r = await fetch(`/api/schedules/${encodeURIComponent(id)}/skip-stage`, { method: "POST" });
    if (!r.ok) throw new Error(`skip HTTP ${r.status}`);
  },
};

function srvToFE(srv) {
  return {
    id:            srv.id,
    name:          srv.name,
    instances:     srv.instance_count,
    whenFinished:  srv.when_finished,
    startAt:       srv.start_at,
    finishedAt:    srv.finished_at || undefined,
    createdAt:     srv.created_at,
    state:         srv.state,             
    currentStage:  srv.current_stage,
    stageStartedAt: srv.stage_started_at || undefined,
    boundPads:     (srv.pads || []).map(p => p.pad_code),
    stages: (srv.stages || []).map(s => ({
      id:      s.id,
      code:    s.map_code,
      hours:   Math.floor(s.duration_sec / 3600),
      minutes: Math.floor((s.duration_sec % 3600) / 60),
    })),
    
    rampMode:  "immediate",
    startMode: srv.start_at > Math.floor(Date.now() / 1000) ? "later" : "now",
  };
}

function feToSrv(fe) {
  return {
    name:           fe.name,
    instance_count: Number(fe.instances) || 1,
    when_finished:  fe.whenFinished || "stop",
    start_at:       fe.startAt || null,
    stages: (fe.stages || []).map(s => ({
      map_code:     s.code,
      duration_sec: Math.max(60, (s.hours * 3600) + (s.minutes * 60)),
    })),
  };
}

function stageDurationSec(s) { return (s.hours * 3600) + (s.minutes * 60); }
function totalDurationSec(stages) { return stages.reduce((a, s) => a + stageDurationSec(s), 0); }

function classifySession(session, now = Math.floor(Date.now() / 1000)) {
  
  if (session.state === "active")   return "active";
  if (session.state === "pending")  return "upcoming";
  if (session.state === "finished" || session.state === "stopped") return "history";
  
  if (session.finishedAt) return "history";
  const total = totalDurationSec(session.stages);
  if (session.whenFinished === "loop") {
    return session.startAt <= now ? "active" : "upcoming";
  }
  if (session.startAt > now) return "upcoming";
  if (session.startAt + total <= now) return "history";
  return "active";
}

function currentStageInfo(session, now = Math.floor(Date.now() / 1000)) {
  const total = totalDurationSec(session.stages);
  if (!total || !session.stages.length) return null;
  let elapsed = Math.max(0, now - session.startAt);
  let loop = 1;
  if (session.whenFinished === "loop" && total > 0) {
    loop = Math.floor(elapsed / total) + 1;
    elapsed = elapsed % total;
  } else if (elapsed > total) {
    elapsed = total;
  }
  let acc = 0;
  for (let i = 0; i < session.stages.length; i++) {
    const dur = stageDurationSec(session.stages[i]);
    if (elapsed < acc + dur) {
      return {
        index: i,
        loop,
        stageElapsed: elapsed - acc,
        stageDuration: dur,
        totalElapsed: now - session.startAt,
        totalDuration: total,
      };
    }
    acc += dur;
  }
  const last = session.stages.length - 1;
  return {
    index: last,
    loop,
    stageElapsed: stageDurationSec(session.stages[last]),
    stageDuration: stageDurationSec(session.stages[last]),
    totalElapsed: total,
    totalDuration: total,
  };
}

function fmtDur(secs) {
  if (secs <= 0) return "0m";
  const h = Math.floor(secs / 3600);
  const m = Math.floor((secs % 3600) / 60);
  if (h && m) return `${h}h ${m}m`;
  if (h) return `${h}h`;
  return `${m}m`;
}
function fmtClock(secs) {
  const h = Math.floor(secs / 3600);
  const m = Math.floor((secs % 3600) / 60);
  return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
}
function fmtAbsTime(ts) {
  if (!ts) return "—";
  const d = new Date(ts * 1000);
  const now = new Date();
  const sameDay = d.toDateString() === now.toDateString();
  const time = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
  if (sameDay) return time;
  const days = Math.round((d - now) / 86400000);
  if (days === 1) return `tomorrow ${time}`;
  if (days === -1) return `yesterday ${time}`;
  return `${d.getMonth()+1}/${d.getDate()} ${time}`;
}

function useNowTick(periodMs = 1000) {
  const [n, setN] = useStateSc(0);
  useEffectSc(() => {
    const id = setInterval(() => setN((x) => x + 1), periodMs);
    return () => clearInterval(id);
  }, [periodMs]);
  return n;
}

function SchedulerScreen({ codes, instances }) {
  const [sessions, setSessions] = useStateSc([]);
  const [tab, setTab] = useStateSc("active");
  const [editing, setEditing] = useStateSc(null); 
  const [expanded, setExpanded] = useStateSc(() => new Set());
  const [loadError, setLoadError] = useStateSc(null);
  
  
  const [statsHistory, setStatsHistory] = useStateSc({});

  
  const refetch = React.useCallback(async () => {
    try {
      const list = await ScheduleAPI.list();
      setSessions(list);
      setLoadError(null);
    } catch (e) {
      setLoadError(String(e));
    }
  }, []);

  useEffectSc(() => { refetch(); }, [refetch]);

  
  useEffectSc(() => {
    if (!window.Live || !window.Live.subscribe) return;
    const off = window.Live.subscribe((m) => {
      if (m && (m.type === "schedule_state" || m.type === "schedule_pads"
                || m.type === "schedule_deleted")) {
        refetch();
      }
      
      if (m && m.type === "schedule_stats") {
        setStatsHistory((prev) => {
          const arr = prev[m.id] ? prev[m.id].slice() : [];
          arr.push({ ts: m.ts, in_game: m.in_game, total: m.total, assigned: m.assigned });
          
          if (arr.length > 240) arr.splice(0, arr.length - 240);
          return { ...prev, [m.id]: arr };
        });
      }
    });
    return off;
  }, [refetch]);

  const tick = useNowTick(1000); 

  const buckets = useMemoSc(() => {
    const out = { active: [], upcoming: [], history: [] };
    const now = Math.floor(Date.now() / 1000);
    sessions.forEach((s) => out[classifySession(s, now)].push(s));
    out.active.sort((a, b) => a.startAt - b.startAt);
    out.upcoming.sort((a, b) => a.startAt - b.startAt);
    out.history.sort((a, b) => (b.finishedAt || b.startAt) - (a.finishedAt || a.startAt));
    return out;
  }, [sessions, tick]);

  const visible = buckets[tab];

  
  useEffectSc(() => {
    if (tab === "active" && buckets.active.length > 0 && expanded.size === 0) {
      setExpanded(new Set([buckets.active[0].id]));
    }
  
  }, [tab]);

  const toggle = (id) => setExpanded((prev) => {
    const n = new Set(prev);
    n.has(id) ? n.delete(id) : n.add(id);
    return n;
  });

  const handleSave = async (session) => {
    
    for (const st of (session.stages || [])) {
      if (!MAP_CODE_RE.test(st.code || "")) {
        alert(`Map code должен быть в формате 0000-0000-0000 (текущее: "${st.code}")`);
        return;
      }
    }
    try {
      const isNew = !sessions.some((s) => s.id === session.id);
      if (isNew) {
        await ScheduleAPI.create(session);
        
        if (session.startMode === "now") {
          
          await refetch();
          const created = (await ScheduleAPI.list()).find(
            (s) => s.name === session.name && s.state === "pending"
          );
          if (created) await ScheduleAPI.start(created.id);
        }
      } else {
        await ScheduleAPI.update(session.id, session);
      }
      await refetch();
    } catch (e) {
      alert(`Сохранение не удалось: ${e}`);
      return;
    }
    setEditing(null);
  };

  const handleDelete = async (id) => {
    try { await ScheduleAPI.remove(id); }
    catch (e) { alert(`Удаление не удалось: ${e}`); }
    await refetch();
  };

  const handleStop = async (id) => {
    try { await ScheduleAPI.stop(id); }
    catch (e) { alert(`Stop не удалось: ${e}`); }
    await refetch();
  };

  const handleStart = async (id) => {
    try { await ScheduleAPI.start(id); }
    catch (e) { alert(`Start не удалось: ${e}`); }
    await refetch();
  };

  const handleSkipStage = async (id) => {
    if (!confirm("Перейти на следующую стадию? Текущие аккаунты будут освобождены, боты перезапустятся с новыми.")) return;
    try { await ScheduleAPI.skipStage(id); }
    catch (e) { alert(`Skip не удалось: ${e}`); }
    await refetch();
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, minHeight: 0 }}>
      <SchedulerTopbar
        counts={buckets}
        onNew={() => setEditing("new")}
      />
      <SchedulerTabs
        tab={tab} setTab={setTab}
        counts={{
          active: buckets.active.length,
          upcoming: buckets.upcoming.length,
          history: buckets.history.length,
        }}
      />
      <div style={{ flex: 1, overflow: 'auto', padding: '18px 24px 32px' }}>
        <div style={{ maxWidth: 1100, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 12 }}>
          {visible.length === 0 && (
            <div className="anim-fade-in" style={{
              padding: '48px 20px', textAlign: 'center', color: 'var(--ink-4)',
              border: '1px dashed var(--line)', borderRadius: 10,
            }}>
              <I.Clock size={26} color="var(--ink-5)" />
              <div style={{ marginTop: 12, fontSize: 14 }}>
                {tab === "active" && "No active sessions"}
                {tab === "upcoming" && "Nothing scheduled"}
                {tab === "history" && "No finished sessions yet"}
              </div>
              {tab !== "history" && (
                <button className="btn btn-sm btn-primary" style={{ marginTop: 16 }} onClick={() => setEditing("new")}>
                  <I.Plus size={12}/> New session
                </button>
              )}
            </div>
          )}
          {visible.map((s) => (
            <SessionCard
              key={s.id}
              session={s}
              kind={tab}
              instances={instances}
              history={statsHistory[s.id] || []}
              expanded={expanded.has(s.id)}
              onToggle={() => toggle(s.id)}
              onEdit={() => setEditing(s)}
              onDelete={() => handleDelete(s.id)}
              onStart={() => handleStart(s.id)}
              onStop={() => handleStop(s.id)}
              onSkipStage={() => handleSkipStage(s.id)}
            />
          ))}
        </div>
      </div>

      {editing && (
        <SessionModal
          codes={codes}
          instances={instances}
          sessions={sessions}
          existing={editing === "new" ? null : editing}
          onClose={() => setEditing(null)}
          onSave={handleSave}
        />
      )}
    </div>
  );
}

// ─── Topbar ──────────────────────────────────────────────────────────────
function SchedulerTopbar({ counts, onNew }) {
  return (
    <header style={{
      height: 68, padding: '0 24px', borderBottom: '1px solid var(--line)',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      flexShrink: 0, gap: 18, background: 'var(--bg)',
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, minWidth: 0 }}>
        <h1 style={{ margin: 0, fontSize: 19, fontWeight: 600 }}>Scheduler</h1>
      </div>
      <button className="btn btn-sm btn-primary" onClick={onNew}>
        <I.Plus size={12}/> New session
      </button>
    </header>
  );
}

function SchedulerTabs({ tab, setTab, counts }) {
  const items = [
    { id: "active",   label: "Active",   count: counts.active },
    { id: "upcoming", label: "Upcoming", count: counts.upcoming },
    { id: "history",  label: "History",  count: counts.history },
  ];
  return (
    <div style={{
      padding: '14px 24px', borderBottom: '1px solid var(--line)',
      display: 'flex', gap: 6, alignItems: 'center', background: 'var(--bg)',
    }}>
      {items.map((it) => {
        const on = tab === it.id;
        return (
          <button key={it.id} onClick={() => setTab(it.id)} style={{
            display: 'inline-flex', alignItems: 'center', gap: 7,
            padding: '6px 14px', borderRadius: 7, fontSize: 13.5, fontWeight: 500,
            background: on ? 'var(--accent)' : 'var(--card)',
            color: on ? '#fff' : 'var(--ink-3)',
            border: '1px solid ' + (on ? 'var(--accent)' : 'var(--line-strong)'),
            transition: 'background var(--t) var(--ease), color var(--t) var(--ease), border-color var(--t) var(--ease)',
          }}>
            <span>{it.label}</span>
            <span style={{
              fontSize: 11, fontFamily: 'IBM Plex Mono, SF Mono, Menlo, monospace',
              padding: '1px 6px', borderRadius: 5,
              background: on ? 'rgba(255,255,255,.18)' : 'var(--bg-deep)',
              color: on ? '#fff' : 'var(--ink-4)',
            }}>{it.count}</span>
          </button>
        );
      })}
    </div>
  );
}

function SessionCard({ session, kind, instances, history, expanded, onToggle, onEdit, onDelete, onStart, onStop, onSkipStage }) {
  const info = currentStageInfo(session);
  const total = totalDurationSec(session.stages);

  return (
    <div className="anim-fade-in" style={{
      background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10,
      overflow: 'hidden',
    }}>
      <SessionCardHeader
        session={session} kind={kind} info={info} total={total}
        expanded={expanded} onToggle={onToggle}
      />
      {expanded && (
        <SessionCardBody
          session={session} kind={kind} info={info} total={total}
          instances={instances}
          history={history}
          onEdit={onEdit} onDelete={onDelete} onStart={onStart} onStop={onStop} onSkipStage={onSkipStage}
        />
      )}
    </div>
  );
}

function CollapsedStageList({ stages }) {
  const containerRef = useRefSc(null);
  const [visible, setVisible] = useStateSc(stages.length);
  const colorForCode = useColorForCode();

  useEffectSc(() => {
    const el = containerRef.current;
    if (!el) return;
    const measure = () => {
      const items = el.querySelectorAll('[data-stage-chip]');
      if (!items.length) return;
      const containerRight = el.getBoundingClientRect().right;
      // Estimate dots width ~30px
      let last = stages.length;
      for (let i = 0; i < items.length; i++) {
        const r = items[i].getBoundingClientRect().right;
        if (r > containerRight - 34) { last = i; break; }
      }
      setVisible(last === stages.length ? stages.length : last);
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [stages]);

  const shown = stages.slice(0, visible);
  const hasMore = visible < stages.length;

  return (
    <div ref={containerRef} style={{ display: 'flex', alignItems: 'center', gap: 5, overflow: 'hidden', minWidth: 0, flex: 1 }}>
      {shown.map((s, i) => {
        const c = colorForCode(s.code);
        return (
          <span key={s.id} data-stage-chip="1" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, flexShrink: 0 }}>
            <span style={{
              display: 'inline-flex', alignItems: 'center',
              padding: '1px 7px', borderRadius: 4,
              fontFamily: 'IBM Plex Mono, SF Mono, Menlo, monospace',
              fontSize: 12, fontWeight: 500,
              background: c.bg, color: c.text,
            }}>{s.code || '—'}</span>
            {i < shown.length - 1 && !hasMore && (
              <span style={{ color: 'var(--ink-5)', fontSize: 11 }}>→</span>
            )}
            {i < shown.length - 1 && hasMore && (
              <span style={{ color: 'var(--ink-5)', fontSize: 11 }}>→</span>
            )}
          </span>
        );
      })}
      {hasMore && (
        <span style={{ color: 'var(--ink-5)', fontSize: 12, fontFamily: 'IBM Plex Mono, SF Mono, Menlo, monospace', flexShrink: 0 }}>···</span>
      )}
    </div>
  );
}

function SessionCardHeader({ session, kind, info, total, expanded, onToggle }) {
  const isActive = kind === "active";
  const isUpcoming = kind === "upcoming";
  const isHistory = kind === "history";

  return (
    <div
      onClick={onToggle}
      style={{
        padding: '14px 18px 12px',
        display: 'flex', alignItems: 'center', gap: 14,
        cursor: 'pointer',
        transition: 'background var(--t) var(--ease)',
      }}
      onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,.025)'}
      onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
    >
      <I.ChevronDown
        size={14}
        style={{
          color: 'var(--ink-4)',
          transform: expanded ? 'rotate(0)' : 'rotate(-90deg)',
          transition: 'transform var(--t) var(--ease)',
          flexShrink: 0,
        }}
      />
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0, flex: 1 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
          <span style={{
            fontSize: 15, fontWeight: 600, color: 'var(--ink)',
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          }}>{session.name}</span>
          {isUpcoming && (
            <span className="mono" style={{
              fontSize: 10.5, color: 'var(--info)', padding: '1px 7px', borderRadius: 3,
              background: 'color-mix(in oklab, var(--info) 12%, transparent)',
              border: '1px solid color-mix(in oklab, var(--info) 28%, transparent)',
              letterSpacing: '.08em', textTransform: 'uppercase',
            }}>scheduled</span>
          )}
          {isHistory && (
            <span className="mono" style={{
              fontSize: 10.5, color: 'var(--ink-4)', padding: '1px 7px', borderRadius: 3,
              background: 'var(--panel-2)', border: '1px solid var(--line)',
              letterSpacing: '.08em', textTransform: 'uppercase',
            }}>finished</span>
          )}
        </div>

        {!expanded && (
          <CollapsedStageList stages={session.stages} />
        )}
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0 }}>
        {isActive && (
          <span className="mono" style={{ fontSize: 13, color: 'var(--ink-2)' }}>
            <span style={{ color: 'var(--ink)' }}>{session.instances}</span>
            <span style={{ color: 'var(--ink-5)' }}>/{session.instances}</span>
          </span>
        )}
        {isUpcoming && (
          <span className="mono" style={{ fontSize: 12.5, color: 'var(--ink-3)' }}>
            <I.Clock size={11} style={{ marginRight: 5, verticalAlign: -1 }}/>
            {fmtAbsTime(session.startAt)}
          </span>
        )}
        {isHistory && (
          <span className="mono" style={{ fontSize: 12.5, color: 'var(--ink-4)' }}>
            {fmtAbsTime(session.finishedAt || session.startAt + total)}
          </span>
        )}
      </div>
    </div>
  );
}

function SessionCardBody({ session, kind, info, total, instances, history, onEdit, onDelete, onStart, onStop, onSkipStage }) {
  const [stageView, setStageView] = useStateSc(info ? info.index : 0);
  useEffectSc(() => { if (info) setStageView(info.index); }, [info?.index]);

  const isActive = kind === "active";
  const isUpcoming = kind === "upcoming";
  const isHistory = kind === "history";

  const currentStage = session.stages[stageView] || session.stages[0];
  const stageRemaining = isActive && info && info.index === stageView ? (info.stageDuration - info.stageElapsed) : null;
  const eta = isActive && info ? (total - info.totalElapsed) : null;
  const elapsedNice = isActive && info ? fmtClock(info.totalElapsed) : null;
  const totalNice = fmtClock(total);

  return (
    <div>
      {}
      <div style={{ padding: '6px 18px 14px' }}>
        <SessionChart session={session} info={info} stageIndex={stageView} kind={kind} history={history} />
      </div>

      {/* Stage segmented progress */}
      <div style={{ padding: '0 18px 14px' }}>
        <StageProgress
          session={session}
          info={info}
          kind={kind}
          stageView={stageView}
          onSelect={setStageView}
        />
      </div>

      {/* Footer actions */}
      <div style={{
        padding: '12px 18px', borderTop: '1px solid var(--line-soft)',
        display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
      }}>
        {isActive && (
          <>
            <BotStatusBar instances={instances} total={session.instances}
              assigned={(session.boundPads || []).length}
              boundPads={session.boundPads || []}/>
            <span style={{ flex: 1 }}/>
            <button className="btn btn-sm" onClick={onSkipStage}>
              <I.ChevronRight size={12}/> Skip stage
            </button>
            <button className="btn btn-sm" onClick={onEdit}>
              <I.Edit size={12}/> Edit upcoming
            </button>
            <button className="btn btn-sm btn-danger" onClick={onStop} style={{ borderColor: 'color-mix(in oklab, var(--bad) 36%, var(--line))' }}>
              <I.Stop size={11}/> Stop
            </button>
          </>
        )}
        {isUpcoming && (
          <>
            {}
            {(session.boundPads || []).length > 0 && (
              <BotStatusBar instances={instances} total={session.instances}
                assigned={(session.boundPads || []).length}
                boundPads={session.boundPads || []}/>
            )}
            <button className="btn btn-sm" onClick={onEdit}>
              <I.Edit size={12}/> Edit
            </button>
            <span style={{ flex: 1 }}/>
            {onStart && (
              <button className="btn btn-sm btn-primary" onClick={onStart}>
                <I.Play size={11}/> Start now
              </button>
            )}
            <button className="btn btn-sm btn-danger" onClick={onDelete}>
              <I.Trash size={11}/> Delete
            </button>
          </>
        )}
        {isHistory && (
          <>
            <span style={{ flex: 1 }}/>
            <button className="btn btn-sm" onClick={onEdit}>
              <I.Copy size={11}/> Clone
            </button>
            <button className="btn btn-sm btn-danger" onClick={onDelete}>
              <I.Trash size={11}/> Remove
            </button>
          </>
        )}
      </div>
    </div>
  );
}

function BotStatusBar({ instances, total, assigned, boundPads }) {
  const boundSet = useMemoSc(() => new Set(boundPads || []), [boundPads]);
  
  const myInstances = (instances || []).filter(i => boundSet.has(i.id));
  const hasData = myInstances.length > 0;
  const raw = {
    running: myInstances.filter(i => i.status === 'running').length,
    booting: myInstances.filter(i => i.status === 'booting').length,
    idle:    myInstances.filter(i => i.status === 'idle').length,
    error:   myInstances.filter(i => i.status === 'error').length,
  };
  
  
  
  const missingPads = (boundPads || []).filter(p => !(instances || []).some(i => i.id === p)).length;
  const queued = raw.idle + missingPads;

  const items = [
    { label: 'in game',  value: hasData || missingPads ? raw.running : '—', dot: '#5EE083' },
    { label: 'starting', value: hasData || missingPads ? raw.booting : '—', dot: '#D7B074' },
    { label: 'queued',   value: hasData || missingPads ? queued      : '—', dot: '#777'    },
    { label: 'error',    value: hasData || missingPads ? raw.error   : '—', dot: '#FF6B6B' },
  ];

  const shortage = typeof assigned === "number" && typeof total === "number" && assigned < total;

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
      {typeof assigned === "number" && (
        <div className="anim-fade-in" title={shortage ? "Свободных падов недостаточно — worker добирает по мере освобождения" : "Все слоты заняты"} style={{
          display: 'inline-flex', alignItems: 'center', gap: 7,
          padding: '4px 10px', borderRadius: 6,
          background: shortage ? 'color-mix(in oklab, var(--warn) 16%, var(--card))' : 'var(--card)',
          border: '1px solid ' + (shortage ? 'color-mix(in oklab, var(--warn) 40%, var(--line-strong))' : 'var(--line-strong)'),
          transition: 'background var(--t) var(--ease), border-color var(--t) var(--ease)',
        }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: shortage ? '#D7B074' : '#5EE083', flexShrink: 0 }}/>
          <span className="mono" style={{ fontSize: 12, color: shortage ? 'var(--warn)' : 'var(--ink-3)' }}>
            <span style={{ color: shortage ? 'var(--warn)' : 'var(--ink)', fontWeight: 600 }}>{assigned}/{total}</span>
            <span style={{ marginLeft: 5 }}>assigned</span>
          </span>
        </div>
      )}
      {items.map(({ label, value, dot }) => (
        <div key={label} className="anim-fade-in" style={{
          display: 'inline-flex', alignItems: 'center', gap: 7,
          padding: '4px 10px', borderRadius: 6,
          background: 'var(--card)', border: '1px solid var(--line-strong)',
        }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: dot, flexShrink: 0 }}/>
          <span className="mono" style={{ fontSize: 12, color: 'var(--ink-3)' }}>
            <span style={{ color: value === '—' ? 'var(--ink-5)' : 'var(--ink)', fontWeight: 600 }}>{value}</span>
            <span style={{ marginLeft: 5 }}>{label}</span>
          </span>
        </div>
      ))}
    </div>
  );
}

// ─── Stage buttons ────────────────────────────────────────────────────────
function StageProgress({ session, info, kind, stageView, onSelect }) {
  const isActive = kind === "active";
  const currentIdx = isActive && info ? info.index : -1;

  return (
    <div style={{ display: 'flex', gap: 6, marginTop: 14 }}>
      {session.stages.map((s, i) => {
        const isCurrent = i === currentIdx;
        return (
          <div key={s.id} style={{
            flex: 1, padding: '7px 10px', borderRadius: 6, fontSize: 12,
            fontFamily: "IBM Plex Mono, SF Mono, Menlo, monospace",
            background: isCurrent ? 'var(--accent)' : 'var(--panel-2)',
            border: '1px solid ' + (isCurrent ? 'var(--accent)' : 'var(--line)'),
            color: isCurrent ? '#fff' : 'var(--ink-4)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
          }}>
            {isCurrent && (
              <svg width="11" height="11" viewBox="0 0 12 12" style={{ flexShrink: 0, animation: 'spin .7s linear infinite' }}>
                <circle cx="6" cy="6" r="4.5" fill="none" stroke="rgba(255,255,255,.4)" strokeWidth="1.5"/>
                <path d="M6 1.5 A4.5 4.5 0 0 1 10.5 6" fill="none" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
              </svg>
            )}
            {s.code || '—'}
          </div>
        );
      })}
    </div>
  );
}

// ─── Chart ───────────────────────────────────────────────────────────────
// Будет получать данные через API. Пока показывает заглушку с осями.
function SessionChart({ session, info, stageIndex, kind, history }) {
  // history: [{ts, in_game, total}] — приходит через WS schedule_stats.
  // Берём последние N точек для текущей стадии и нормализуем v = in_game/total.
  const stageStartTs = session.stageStartedAt || session.startAt;
  const stageDurSec = session.stages[stageIndex] ? stageDurationSec(session.stages[stageIndex]) : 1;
  const points = useMemoSc(() => {
    if (!history || !history.length) return [];
    return history
      .filter((h) => h.ts >= stageStartTs)
      .map((h) => ({
        t: Math.min(1, Math.max(0, (h.ts - stageStartTs) / Math.max(1, stageDurSec))),
        v: h.total > 0 ? h.in_game / h.total : 0,
      }));
  }, [history, stageStartTs, stageDurSec]);
  const hasData = points.length > 0;

  const stageDur = session.stages[stageIndex] ? stageDurationSec(session.stages[stageIndex]) : 0;
  const W = 1000, H = 150, PAD_L = 32, PAD_R = 8, PAD_T = 10, PAD_B = 22;
  const innerW = W - PAD_L - PAD_R;
  const innerH = H - PAD_T - PAD_B;

  const progressX = (kind === "active" && info && info.index === stageIndex)
    ? Math.min(1, info.stageElapsed / Math.max(1, info.stageDuration))
    : null;

  return (
    <div style={{
      borderRadius: 8, overflow: 'hidden',
      background: 'rgba(0,0,0,.22)',
    }}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block' }} preserveAspectRatio="none">
        <defs>
          <linearGradient id={`grad-${session.id}-${stageIndex}`} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.3"/>
            <stop offset="100%" stopColor="var(--accent)" stopOpacity="0"/>
          </linearGradient>
        </defs>

        {}
        {[0.25, 0.5, 0.75].map((g, i) => (
          <line key={i}
            x1={PAD_L} x2={W - PAD_R}
            y1={PAD_T + g * innerH} y2={PAD_T + g * innerH}
            stroke="var(--line-strong)" strokeWidth="0.5" opacity="0.5"/>
        ))}

        {}
        {[{v:1,l:'100%'},{v:.5,l:'50%'},{v:0,l:'0%'}].map((yl,i) => (
          <text key={i} x={PAD_L - 5} y={PAD_T + (1 - yl.v) * innerH + 4}
            fontSize="9" textAnchor="end" fill="var(--ink-5)"
            fontFamily="IBM Plex Mono, SF Mono, Menlo, monospace">{yl.l}</text>
        ))}

        {hasData ? (
          /* Реальные данные от API */
          (() => {
            const path = points.map((p, i) => {
              const x = PAD_L + p.t * innerW;
              const y = PAD_T + (1 - p.v) * innerH;
              return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
            }).join(' ');
            const area = path + ` L${PAD_L + innerW},${PAD_T + innerH} L${PAD_L},${PAD_T + innerH} Z`;
            return (
              <>
                <path d={area} fill={`url(#grad-${session.id}-${stageIndex})`}/>
                <path d={path} fill="none" stroke="var(--accent)" strokeWidth="1.5" strokeLinejoin="round"/>
              </>
            );
          })()
        ) : (
          /* Заглушка — пунктирная baseline по центру */
          <line
            x1={PAD_L} x2={W - PAD_R}
            y1={PAD_T + innerH * 0.5} y2={PAD_T + innerH * 0.5}
            stroke="var(--line-strong)" strokeWidth="1" strokeDasharray="4 6"/>
        )}

        {}
        {progressX !== null && (
          <line
            x1={PAD_L + progressX * innerW} x2={PAD_L + progressX * innerW}
            y1={PAD_T} y2={PAD_T + innerH}
            stroke="var(--accent)" strokeWidth="1" strokeDasharray="3 4" opacity="0.6"/>
        )}

        {}
        <text x={PAD_L} y={H - 6} fontSize="9" fill="var(--ink-5)" fontFamily="IBM Plex Mono, SF Mono, Menlo, monospace">00:00</text>
        <text x={PAD_L + innerW / 2} y={H - 6} fontSize="9" textAnchor="middle" fill="var(--ink-5)" fontFamily="IBM Plex Mono, SF Mono, Menlo, monospace">{fmtClock(Math.floor(stageDur / 2))}</text>
        <text x={W - PAD_R} y={H - 6} fontSize="9" textAnchor="end" fill="var(--ink-5)" fontFamily="IBM Plex Mono, SF Mono, Menlo, monospace">{fmtClock(stageDur)}</text>
      </svg>
      {!hasData && (
        <div style={{
          padding: '5px 12px 7px',
          borderTop: '1px solid var(--line-soft)',
          display: 'flex', alignItems: 'center', gap: 6,
          background: 'transparent',
        }}>
          <span style={{ width: 4, height: 4, borderRadius: '50%', background: 'var(--ink-5)' }}/>
          <span className="mono" style={{ fontSize: 10, color: 'var(--ink-5)', letterSpacing: '.05em' }}>
            waiting for data
          </span>
        </div>
      )}
    </div>
  );
}

// ─── New / Edit modal ───────────────────────────────────────────────────
function SessionModal({ codes, existing, onClose, onSave, instances: allInstances, sessions }) {
  
  const occupiedByOthers = useMemoSc(() => {
    const occ = new Set();
    (sessions || []).forEach((s) => {
      if (s.id === existing?.id) return; 
      if (s.state === "active" || s.state === "pending") {
        (s.boundPads || []).forEach(p => occ.add(p));
      }
    });
    return occ;
  }, [sessions, existing?.id]);

  const availableBots = useMemoSc(() => {
    if (!allInstances) return null;
    return allInstances.filter(i => !occupiedByOthers.has(i.id)).length;
  }, [allInstances, occupiedByOthers]);

  const [name, setName] = useStateSc(existing?.name || "");
  const [instances, setInstances] = useStateSc(existing?.instances || 60);
  const [stages, setStages] = useStateSc(() =>
    existing?.stages?.length
      ? existing.stages.map((s) => ({ ...s }))
      : [{ id: uid(), code: codes[0] || "", hours: 1, minutes: 30 }]
  );
  const [whenFinished, setWhenFinished] = useStateSc(existing?.whenFinished || "stop");
  const [rampMode, setRampMode] = useStateSc(existing?.rampMode || "immediate");
  const [startMode, setStartMode] = useStateSc(existing?.startMode || "now");
  const [startInMin, setStartInMin] = useStateSc(() => {
    if (existing?.startMode === "later" && existing.startAt) {
      const diff = Math.max(0, Math.floor((existing.startAt - Date.now() / 1000) / 60));
      return diff;
    }
    return 60;
  });

  const total = totalDurationSec(stages);
  const stagesValid = stages.length > 0 && stages.every((s) => s.code && stageDurationSec(s) > 0);
  const canSave = instances > 0 && stagesValid;

  const autoName = useMemoSc(() => {
    if (!stages.length) return "session";
    const codesList = stages.map((s) => s.code).filter(Boolean);
    if (!codesList.length) return "session";
    return `auto · ${codesList[0]}${codesList.length > 1 ? " +" + (codesList.length - 1) : ""}`;
  }, [stages]);

  const updateStage = (id, patch) => setStages((prev) => prev.map((s) => s.id === id ? { ...s, ...patch } : s));
  const removeStage = (id) => setStages((prev) => prev.filter((s) => s.id !== id));
  const addStage = () => setStages((prev) => [...prev, {
    id: uid(),
    code: codes[prev.length % Math.max(1, codes.length)] || "",
    hours: 0, minutes: 30,
  }]);

  const submit = () => {
    if (!canSave) return;
    const now = Math.floor(Date.now() / 1000);
    const startAt = startMode === "now" ? now : now + startInMin * 60;
    const out = {
      id: existing?.id || uid(),
      name: name.trim() || autoName,
      instances,
      stages,
      whenFinished,
      rampMode,
      startMode,
      startAt,
      createdAt: existing?.createdAt || now,
    };
    onSave(out);
  };

  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="anim-modal" style={{
        position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
        width: 580, maxWidth: '94vw', maxHeight: '92vh',
        background: 'var(--panel)', border: '1px solid var(--line-strong)', borderRadius: 12,
        zIndex: 95, overflow: 'hidden', display: 'flex', flexDirection: 'column',
        boxShadow: '0 16px 48px rgba(0,0,0,.14)',
      }}>
        {/* Header */}
        <div style={{
          padding: '16px 20px',
          display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 14,
        }}>
          <h3 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>
            {existing ? "Edit session" : "New session"}
          </h3>
          <button className="icon-btn" onClick={onClose}><I.X size={14}/></button>
        </div>

        {}
        <div style={{ padding: '4px 20px 20px', overflow: 'auto', flex: 1 }}>

          {}
          <div style={{
            display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 18,
            padding: '14px 16px', background: 'var(--card)',
            border: '1px solid var(--line)', borderRadius: 8,
            alignItems: 'center',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <span className="section-eyebrow">bots</span>
              <NumberStepper value={instances} onChange={setInstances} min={1} max={9999} step={1} bigStep={10}/>
              {availableBots !== null && (
                <span className="mono anim-fade-in" style={{
                  fontSize: 11, color: instances > availableBots ? 'var(--warn)' : 'var(--ink-4)',
                  transition: 'color var(--t) var(--ease)',
                }}>
                  {instances > availableBots
                    ? `⚠ available: ${availableBots}`
                    : `available: ${availableBots}`}
                </span>
              )}
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'flex-end' }}>
              <span className="section-eyebrow">start</span>
              <SegToggle
                value={startMode} onChange={setStartMode}
                options={[
                  { value: "now",   label: "Now" },
                  { value: "later", label: "In…" },
                ]}
              />
              {startMode === "later" && (
                <div style={{
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                  background: 'var(--panel-2)', border: '1px solid var(--line)',
                  borderRadius: 6, padding: '4px 9px',
                }}>
                  <input
                    type="number" min={1} className="mono"
                    value={startInMin}
                    onChange={(e) => setStartInMin(Math.max(1, parseInt(e.target.value) || 0))}
                    style={{
                      width: 44, textAlign: 'right', background: 'transparent', border: 0, outline: 0,
                      color: 'var(--ink)', fontSize: 13, padding: 0,
                    }}
                  />
                  <span className="mono" style={{ color: 'var(--ink-4)', fontSize: 12 }}>min</span>
                </div>
              )}
            </div>
          </div>

          {}
          <div style={{ marginTop: 14 }}>
            <div style={{
              display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
              padding: '0 4px 8px',
            }}>
              <span className="section-eyebrow">rotation chain</span>
              {stagesValid && (
                <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-4)' }}>
                  {`${stages.length} stage${stages.length !== 1 ? 's' : ''} · ${fmtDur(total)} total`}
                </span>
              )}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {stages.map((s, i) => (
                <StageRow
                  key={s.id}
                  index={i}
                  stage={s}
                  codes={codes}
                  canRemove={stages.length > 1}
                  onChange={(patch) => updateStage(s.id, patch)}
                  onRemove={() => removeStage(s.id)}
                />
              ))}
            </div>
            <button className="btn btn-sm" style={{
              marginTop: 8, width: '100%', justifyContent: 'center',
              borderStyle: 'dashed', background: 'transparent',
              color: 'var(--ink-3)',
            }} onClick={addStage}>
              <I.Plus size={12}/> Add stage
            </button>
          </div>

        </div>

        {/* Footer */}
        <div style={{
          padding: '12px 20px', background: 'var(--panel-2)',
          display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 9,
          borderTop: '1px solid var(--line)',
        }}>
          <button className="btn btn-sm" onClick={onClose}>Cancel</button>
          <button className="btn btn-sm btn-primary" disabled={!canSave} onClick={submit}>
            <I.Play size={12}/> {existing ? "Save" : "Launch"}
          </button>
        </div>
      </div>
    </>
  );
}

function Field({ label, hint, children }) {
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
        <span style={{ fontSize: 12, color: 'var(--ink-4)', fontFamily: "IBM Plex Mono, SF Mono, Menlo, monospace", letterSpacing: '.04em' }}>{label}</span>
        {hint && <span style={{ fontSize: 11, color: 'var(--ink-5)' }}>{hint}</span>}
      </div>
      {children}
    </div>
  );
}

function NumberStepper({ value, onChange, min = 0, max = 9999, step = 1, bigStep = 10 }) {
  const clamp = (v) => Math.max(min, Math.min(max, v));
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 0, background: 'var(--panel-2)', border: '1px solid var(--line)', borderRadius: 6, overflow: 'hidden' }}>
      <button
        className="icon-btn" style={{ width: 36, height: 36, borderRadius: 0 }}
        onClick={(e) => onChange(clamp(value - (e.shiftKey ? bigStep : step)))}
      >
        <I.Minus size={12}/>
      </button>
      <input
        type="number" className="mono"
        value={value}
        onChange={(e) => onChange(clamp(parseInt(e.target.value) || 0))}
        style={{
          width: 60, textAlign: 'center', background: 'transparent', border: 0, outline: 0,
          color: 'var(--ink)', fontSize: 14, padding: '8px 0',
          borderLeft: '1px solid var(--line)', borderRight: '1px solid var(--line)',
        }}
      />
      <button
        className="icon-btn" style={{ width: 36, height: 36, borderRadius: 0 }}
        onClick={(e) => onChange(clamp(value + (e.shiftKey ? bigStep : step)))}
      >
        <I.Plus size={12}/>
      </button>
    </div>
  );
}

function StageRow({ index, stage, codes, canRemove, onChange, onRemove }) {
  const colorForCode = useColorForCode();
  const c = colorForCode(stage.code);
  const wrapRef = useRefSc(null);
  const [listOpen, setListOpen] = useStateSc(false);

  
  useEffectSc(() => {
    if (!listOpen) return;
    const onDoc = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setListOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [listOpen]);

  const value = stage.code || "";
  const valid = !value || MAP_CODE_RE.test(value);
  
  const formatMapCode = (raw) => {
    const digits = (raw || "").replace(/\D/g, "").slice(0, 12);
    const parts = [];
    if (digits.length > 0) parts.push(digits.slice(0, 4));
    if (digits.length > 4) parts.push(digits.slice(4, 8));
    if (digits.length > 8) parts.push(digits.slice(8, 12));
    return parts.join("-");
  };
  const suggestions = useMemoSc(() => {
    if (!codes || !codes.length) return [];
    const q = value;
    return codes.filter((x) => MAP_CODE_RE.test(x) && (!q || x.includes(q))).slice(0, 8);
  }, [codes, value]);

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8,
      padding: '8px 10px', background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 7,
    }}>
      <span className="mono" style={{
        width: 22, height: 22, borderRadius: 4, fontSize: 12,
        background: 'var(--panel-2)', color: 'var(--ink-4)',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
      }}>{index + 1}</span>

      {/* Inline-инпут кода + dropdown подсказок */}
      <div ref={wrapRef} style={{ position: 'relative', minWidth: 160 }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 6, padding: '4px 9px',
          background: stage.code ? c.bg : 'var(--panel-2)',
          border: '1px dashed ' + (!valid ? 'var(--bad)' : (stage.code ? c.border : 'var(--line-strong)')),
          borderRadius: 4,
        }}>
          <input
            type="text"
            value={value}
            placeholder="0000-0000-0000"
            inputMode="numeric"
            onFocus={() => setListOpen(true)}
            onChange={(e) => {
              onChange({ code: formatMapCode(e.target.value) });
              setListOpen(true);
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter' || e.key === 'Escape') { e.preventDefault(); setListOpen(false); e.target.blur(); }
            }}
            className="mono"
            style={{
              flex: 1, width: '100%', background: 'transparent', border: 0, outline: 0,
              fontSize: 13, color: !valid ? 'var(--bad)' : (stage.code ? c.text : 'var(--ink)'),
              padding: 0,
            }}
          />
          <I.ChevronDown size={11} color="var(--ink-4)"
            onClick={() => setListOpen((o) => !o)} style={{ cursor: 'pointer' }}/>
        </div>
        {listOpen && suggestions.length > 0 && (
          <div className="anim-pop" style={{
            position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
            background: 'var(--card)', border: '1px solid var(--line-strong)', borderRadius: 6,
            boxShadow: '0 8px 24px rgba(0,0,0,.12)', padding: 4, zIndex: 300,
            maxHeight: 220, overflow: 'auto',
          }}>
            {suggestions.map((code) => {
              const col = colorForCode(code);
              const on = code === stage.code;
              return (
                <div key={code} onMouseDown={(e) => { e.preventDefault(); onChange({ code }); setListOpen(false); }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8, padding: '6px 8px',
                    borderRadius: 4, cursor: 'pointer', fontSize: 13,
                    background: on ? 'var(--panel-2)' : 'transparent',
                    color: on ? 'var(--ink)' : 'var(--ink-2)',
                  }}
                  onMouseEnter={(e) => e.currentTarget.style.background = 'var(--panel-2)'}
                  onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
                  <span style={{ width: 8, height: 8, borderRadius: 2, background: col.dot, flexShrink: 0 }}/>
                  <span className="mono" style={{ flex: 1 }}>{code}</span>
                  {on && <I.Check size={11} color="var(--accent)"/>}
                </div>
              );
            })}
          </div>
        )}
      </div>

      <DurationPart label="h" value={stage.hours}   onChange={(v) => onChange({ hours: Math.max(0, Math.min(99, v)) })}/>
      <DurationPart label="m" value={stage.minutes} onChange={(v) => onChange({ minutes: Math.max(0, Math.min(59, v)) })} step={5}/>

      <span style={{ flex: 1 }}/>

      <button
        className="icon-btn" disabled={!canRemove}
        onClick={onRemove}
        style={{ width: 28, height: 28, opacity: canRemove ? 1 : 0.35, color: 'var(--ink-4)' }}
        onMouseEnter={(e) => { if (canRemove) e.currentTarget.style.color = 'var(--bad)'; }}
        onMouseLeave={(e) => e.currentTarget.style.color = 'var(--ink-4)'}
      >
        <I.Trash size={12}/>
      </button>
    </div>
  );
}

function DurationPart({ label, value, onChange, step = 1 }) {
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      background: 'var(--panel-2)', border: '1px solid var(--line)', borderRadius: 5,
      padding: '3px 6px',
    }}>
      <input
        type="number" min={0} className="mono"
        value={value}
        onChange={(e) => onChange(parseInt(e.target.value) || 0)}
        onKeyDown={(e) => {
          if (e.key === 'ArrowUp')   { e.preventDefault(); onChange(value + step); }
          if (e.key === 'ArrowDown') { e.preventDefault(); onChange(Math.max(0, value - step)); }
        }}
        style={{
          width: 32, textAlign: 'right', background: 'transparent', border: 0, outline: 0,
          color: 'var(--ink)', fontSize: 13, padding: '3px 0',
        }}
      />
      <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-4)' }}>{label}</span>
    </div>
  );
}

function SegToggle({ value, onChange, options }) {
  return (
    <div style={{ display: 'inline-flex', gap: 4 }}>
      {options.map((opt) => {
        const on = value === opt.value;
        return (
          <button key={opt.value} onClick={() => onChange(opt.value)} style={{
            padding: '5px 13px', borderRadius: 7, fontSize: 13, fontWeight: 500,
            background: on ? 'var(--accent)' : 'var(--card)',
            color: on ? '#fff' : 'var(--ink-3)',
            border: '1px solid ' + (on ? 'var(--accent)' : 'var(--line-strong)'),
            transition: 'background var(--t) var(--ease), color var(--t) var(--ease)',
          }}>{opt.label}</button>
        );
      })}
    </div>
  );
}

function SelectInput({ value, onChange, options }) {
  const ref = useRefSc(null);
  const [open, setOpen] = useStateSc(false);
  const current = options.find((o) => o.value === value);
  return (
    <>
      <button ref={ref} onClick={() => setOpen((o) => !o)} style={{
        width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '9px 13px', background: 'var(--panel-2)', border: '1px solid var(--line)',
        borderRadius: 6, fontSize: 14, color: 'var(--ink)',
      }}>
        <span>{current?.label || value}</span>
        <I.ChevronDown size={13} color="var(--ink-4)"/>
      </button>
      <Popover open={open} onClose={() => setOpen(false)} anchorRef={ref} align="left" width={220}>
        {options.map((o) => (
          <div key={o.value} className="menu-item"
            onClick={() => { onChange(o.value); setOpen(false); }}
            style={{
              padding: '8px 11px', borderRadius: 5, cursor: 'pointer', fontSize: 13.5,
              background: o.value === value ? 'var(--panel-2)' : 'transparent',
              color: o.value === value ? 'var(--ink)' : 'var(--ink-2)',
              display: 'flex', alignItems: 'center', gap: 10,
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--panel-2)'}
            onMouseLeave={(e) => { if (o.value !== value) e.currentTarget.style.background = 'transparent'; }}
          >
            <span style={{ flex: 1 }}>{o.label}</span>
            {o.value === value && <I.Check size={12} style={{ color: 'var(--accent)' }}/>}
          </div>
        ))}
      </Popover>
    </>
  );
}

Object.assign(window, { SchedulerScreen });
