/* UPCOZ Migration Console — frontend.

   Wired to the FastAPI backend in /backend. The mockup's simulated phase
   data is gone; everything reads from the real API. Visual design is the
   same as the original mockup with three substantive corrections:
     • DB is Aurora MySQL, not Postgres.
     • Phase names match the real pipeline (1 / 2 / 2b / 3 / 4 / 5 / 5b / 5c / 6).
     • Run history shows real rows, not fake script stats.
*/

const { useState, useEffect, useRef, useCallback } = React;


/* ─────────────────────────────────────────────────────────
   Pipeline metadata — must stay in sync with the backend
   and run_incremental_pipeline.py.
   ───────────────────────────────────────────────────────── */

const TIERS = [
  { id: '1',   name: 'Tier 1', desc: 'Critical only',           etaH: 5    },
  { id: '2',   name: 'Tier 2', desc: '+ Tags & Deposits',       etaH: 5    },
  { id: '3',   name: 'Tier 3', desc: '+ Racing Domain',         etaH: 7.5  },
  { id: 'all', name: 'All',    desc: 'Full pipeline',           etaH: 7.5  },
];

const TIER_LABEL = id => (TIERS.find(t => t.id === id) || {}).name || id;
const TIER_DESC  = id => (TIERS.find(t => t.id === id) || {}).desc || '';

/* Real phase set as run by run_incremental_pipeline.py. The "tier"
   column is what the orchestrator gates each phase on. */
const PHASES = [
  { num: '1',   label: 'New user discovery',          tier: 1 },
  { num: '2',   label: 'New user data migration',     tier: 1 },
  { num: '3',   label: 'Transactions + betslips',     tier: 1 },
  { num: '4',   label: 'Wallet + bonus balances',     tier: 1 },
  { num: '4b',  label: 'Tag + template sync',         tier: 1 },
  { num: '5',   label: 'User status + blocks',        tier: 1 },
  { num: '5b',  label: 'Type / category safety fix',  tier: 1 },
  { num: '5c',  label: 'Racing domain (UI tables)',   tier: 3 },
  { num: '6',   label: 'Final counts + SMS',          tier: 1 },
];

const SUMMARY_METRICS = [
  'Users', 'Transactions', 'Betslips', 'Wallets',
  'Blocked users', 'Deposit limits', 'Tags',
];


/* ─────────────────────────────────────────────────────────
   API helpers
   ───────────────────────────────────────────────────────── */

const api = async (path, opts = {}) => {
  const res = await fetch(path, {
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
    ...opts,
  });
  if (res.status === 401) {
    location.href = '/login';
    throw new Error('Not authenticated');
  }
  if (!res.ok) {
    let detail = `${res.status} ${res.statusText}`;
    try { const j = await res.json(); if (j.detail) detail = j.detail; } catch {}
    throw new Error(detail);
  }
  if (res.status === 204) return null;
  return res.json();
};

const fmtNum = n => (n == null ? '—' : Number(n).toLocaleString('en-AU'));

const fmtDateTime = iso => {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  const pad = n => String(n).padStart(2, '0');
  return `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};

const fmtDuration = (startIso, endIso) => {
  if (!startIso) return '—';
  const start = new Date(startIso).getTime();
  const end = endIso ? new Date(endIso).getTime() : Date.now();
  const sec = Math.max(0, Math.floor((end - start) / 1000));
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0) return `${m}m ${s}s`;
  return `${s}s`;
};


/* ─────────────────────────────────────────────────────────
   UI primitives (unchanged from mockup)
   ───────────────────────────────────────────────────────── */

const Card = ({ title, subtitle, right, children, padded = true, style }) => (
  <section style={{
    background: 'var(--surface)',
    border: '1px solid var(--border)',
    borderRadius: 14,
    boxShadow: '0 1px 2px rgba(15,23,42,0.04)',
    overflow: 'hidden',
    display: 'flex', flexDirection: 'column',
    ...style,
  }}>
    {(title || right) && (
      <header style={{
        display:'flex', alignItems:'center', justifyContent:'space-between',
        gap:12, padding:'14px 18px',
        borderBottom: '1px solid var(--border)',
      }}>
        <div>
          <div style={{ fontWeight:600, fontSize:14, color:'var(--text)' }}>{title}</div>
          {subtitle && <div style={{ fontSize:12, color:'var(--text-3)', marginTop:2 }}>{subtitle}</div>}
        </div>
        <div>{right}</div>
      </header>
    )}
    <div style={{ padding: padded ? 18 : 0, flex:1, minHeight:0, display:'flex', flexDirection:'column' }}>
      {children}
    </div>
  </section>
);

const Badge = ({ kind = 'neutral', children, large = false }) => {
  const styles = {
    idle:     { bg:'#e5e7eb', fg:'#374151' },
    running:  { bg:'var(--green)', fg:'#fff' },
    complete: { bg:'var(--green)', fg:'#fff' },
    failed:   { bg:'var(--red)', fg:'#fff' },
    stopped:  { bg:'var(--text-3)', fg:'#fff' },
    warnings: { bg:'var(--amber)', fg:'#fff' },
    neutral:  { bg:'#e5e7eb', fg:'#374151' },
  }[kind] || { bg:'#e5e7eb', fg:'#374151' };
  return (
    <span style={{
      display: 'inline-flex', alignItems:'center', gap: 6,
      padding: large ? '6px 14px' : '3px 9px',
      borderRadius: 999,
      background: styles.bg, color: styles.fg,
      fontWeight: 600,
      fontSize: large ? 13 : 11,
      letterSpacing: 0.4,
      textTransform: 'uppercase',
      lineHeight: 1.4,
    }}>
      {children}
    </span>
  );
};

const Toggle = ({ checked, onChange, leftLabel, rightLabel, disabled }) => (
  <div style={{ display:'flex', alignItems:'center', gap:10 }}>
    {leftLabel && <span style={{ fontSize:13, color: checked ? 'var(--text-3)' : 'var(--text)', fontWeight: checked ? 400 : 500 }}>{leftLabel}</span>}
    <button
      onClick={() => !disabled && onChange(!checked)}
      disabled={disabled}
      aria-pressed={checked}
      style={{
        position:'relative', width:38, height:22, padding:0,
        background: checked ? 'var(--blue)' : '#cbd5e1',
        border: 'none', borderRadius: 999,
        transition: 'background .15s',
        opacity: disabled ? 0.5 : 1,
        cursor: disabled ? 'not-allowed' : 'pointer',
      }}>
      <span style={{
        position:'absolute', top:2, left: checked ? 18 : 2,
        width:18, height:18, borderRadius:'50%',
        background:'#fff', boxShadow:'0 1px 2px rgba(0,0,0,.2)',
        transition: 'left .15s',
      }}/>
    </button>
    {rightLabel && <span style={{ fontSize:13, color: !checked ? 'var(--text-3)' : 'var(--text)', fontWeight: !checked ? 400 : 500 }}>{rightLabel}</span>}
  </div>
);

const TextInput = (props) => (
  <input {...props} style={{
    height: 36, padding: '0 11px',
    border:'1px solid var(--border-strong)', borderRadius:8,
    fontSize:13, color:'var(--text)', background:'#fff',
    outline:'none', ...props.style,
  }} onFocus={e => e.target.style.borderColor='var(--blue)'} onBlur={e => e.target.style.borderColor='var(--border-strong)'}/>
);

const Select = ({ value, onChange, options, style, disabled }) => (
  <select value={value} onChange={e => onChange(e.target.value)} disabled={disabled} style={{
    height: 36, padding: '0 30px 0 11px',
    border:'1px solid var(--border-strong)', borderRadius:8,
    fontSize:13, color:'var(--text)', background:'#fff',
    appearance:'none',
    backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'6\' viewBox=\'0 0 10 6\'><path d=\'M1 1l4 4 4-4\' stroke=\'%236b7280\' stroke-width=\'1.5\' fill=\'none\' stroke-linecap=\'round\' stroke-linejoin=\'round\'/></svg>")',
    backgroundRepeat:'no-repeat',
    backgroundPosition:'right 11px center',
    outline:'none',
    opacity: disabled ? 0.5 : 1,
    ...style,
  }}>
    {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
  </select>
);

const IconBtn = ({ children, onClick, danger, title, disabled }) => (
  <button onClick={onClick} title={title} disabled={disabled} style={{
    height:30, width:30,
    display:'inline-flex', alignItems:'center', justifyContent:'center',
    background:'transparent',
    border:'1px solid var(--border)', borderRadius:7,
    color: danger ? 'var(--red)' : 'var(--text-3)',
    cursor: disabled ? 'not-allowed' : 'pointer',
    opacity: disabled ? 0.5 : 1,
  }} onMouseOver={e => { if (!disabled) e.currentTarget.style.background = danger ? 'var(--red-soft)' : '#f3f4f6'; }}
     onMouseOut={e => { e.currentTarget.style.background = 'transparent'; }}>
    {children}
  </button>
);


/* ─────────────────────────────────────────────────────────
   Top header — standalone ops tool (no fake main-product nav tabs).
   ───────────────────────────────────────────────────────── */

const TopNav = ({ onLogout }) => (
  <div style={{
    background:'#fff', borderBottom:'1px solid var(--border)',
    display:'flex', alignItems:'center', gap:14,
    padding:'12px 26px',
  }}>
    <img src="/assets/betcore-migrate-logo.png" alt="Betcore Migrate" style={{ height:26 }} onError={e => e.target.style.display='none'}/>
    <div style={{ fontWeight:700, fontSize:15, color:'var(--text)', letterSpacing:-0.02 }}>
      Migration Console
    </div>
    <div style={{ marginLeft:'auto', display:'flex', alignItems:'center', gap:18 }}>
      <button onClick={onLogout} style={{
        background:'transparent', border:'none',
        color:'var(--red)', fontWeight:600, fontSize:13,
      }}>Logout</button>
    </div>
  </div>
);


const StatusBanner = ({ run, lastRun }) => {
  const status = run ? 'running' : (lastRun ? lastRun.status : 'idle');
  const meta = ({
    idle:     { dot:'#9ca3af', text:'Pipeline is idle. Ready to start a new run.' },
    running:  { dot:'#22c55e', text:'A pipeline run is in progress.' },
    complete: { dot:'#16a34a', text:'Most recent run completed successfully.' },
    warnings: { dot:'#f59e0b', text:'Most recent run completed with warnings.' },
    failed:   { dot:'#dc2626', text:'Most recent run failed. See log for details.' },
    stopped:  { dot:'#6b7280', text:'Most recent run was stopped manually.' },
  })[status] || { dot:'#9ca3af', text:'' };

  const dur = run ? fmtDuration(run.started_at) : (lastRun ? fmtDuration(lastRun.started_at, lastRun.finished_at) : '');

  return (
    <div style={{
      background:'#fff', border:'1px solid var(--border)',
      borderRadius:14, padding:'18px 22px',
      display:'grid', gridTemplateColumns:'auto 1fr auto', alignItems:'center', gap:24,
      boxShadow:'0 1px 2px rgba(15,23,42,.04)',
      borderLeft: `4px solid ${meta.dot}`,
    }}>
      <div>
        <div style={{ fontSize:11, color:'var(--text-3)', textTransform:'uppercase', letterSpacing:0.6, fontWeight:600 }}>Pipeline</div>
        <div style={{ fontSize:20, fontWeight:700, marginTop:4 }}>Betcore → Aurora MySQL Migration</div>
        <div style={{ fontSize:12, color:'var(--text-3)', marginTop:2 }}>{meta.text}</div>
        {run && !run.process_paused && (
          <div style={{
            marginTop:8, padding:'8px 12px',
            background:'#fef2f2', border:'1px solid #fca5a5',
            borderRadius:8, fontSize:12, color:'#991b1b', fontWeight:600,
          }}>
            ⚠ Pipeline is running — do <strong>not</strong> restart the console service (deploy.sh / systemctl restart) until this run finishes. Doing so will kill the pipeline mid-run.
          </div>
        )}
        {run && run.process_paused && (
          <div style={{
            marginTop:8, padding:'8px 12px',
            background:'var(--amber-soft)', border:'1px solid #fcd34d',
            borderRadius:8, fontSize:12, color:'var(--amber-text)', fontWeight:600,
          }}>
            Runner is paused (SIGSTOP): the subprocess is frozen — the live log will not advance until you press <strong>Resume</strong>.
          </div>
        )}
        {run?.started_by && (
          <div style={{ fontSize:11, color:'var(--text-3)', marginTop: run?.process_paused ? 6 : 8 }}>
            {String(run.started_by).startsWith('schedule:')
              ? <>Triggered by <strong>schedule</strong> ({run.started_by.replace(/^schedule:/, '').trim()})</>
              : <>Started by {run.started_by}</>}
          </div>
        )}
      </div>
      <div>
        {run && (
          <div>
            <div style={{ display:'flex', justifyContent:'space-between', fontSize:12, color:'var(--text-3)', marginBottom:6 }}>
              <span>{TIER_LABEL(run.tier)} · {run.mode} mode</span>
              <span>elapsed {dur}</span>
            </div>
            <div style={{ height:8, background:'#e5e7eb', borderRadius:999, overflow:'hidden' }}>
              <div style={{
                width: '100%', height:'100%',
                background: 'linear-gradient(90deg, #22c55e, #16a34a)',
                opacity: 0.85,
                animation: 'shimmer 2s linear infinite',
                backgroundSize: '200% 100%',
              }}/>
            </div>
            <style>{`@keyframes shimmer { from { background-position: 200% 0; } to { background-position: 0 0; } }`}</style>
          </div>
        )}
      </div>
      <div style={{ textAlign:'right' }}>
        <Badge kind={status} large>
          <span style={{ width:8, height:8, borderRadius:'50%', background:'#fff', display:'inline-block', opacity:.9 }}/>
          {status}
        </Badge>
        <div style={{ fontSize:11, color:'var(--text-3)', marginTop:8 }}>
          {run
            ? <>Started {fmtDateTime(run.started_at)}</>
            : (lastRun ? <>Last run · {fmtDateTime(lastRun.started_at)} · {dur}</> : 'No previous runs recorded')}
        </div>
      </div>
    </div>
  );
};


/* ─────────────────────────────────────────────────────────
   Start a Run
   ───────────────────────────────────────────────────────── */

const StartRun = ({ mode, setMode, bankMode, setBankMode,
                    running, procPaused, busy, onStart, onStop,
                    onPauseProcess, onResumeProcess }) => {

  const fullModeWarn = mode === 'full';

  return (
    <Card title="Start a run" subtitle="Configure options, then start the full pipeline (~7.5 hrs estimated)">

      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'12px 0' }}>
        <div>
          <div style={{ fontWeight:500, fontSize:13 }}>Mode</div>
          <div style={{ fontSize:12, color:'var(--text-3)' }}>Full mode is for clean-DB repopulation only</div>
        </div>
        <Toggle
          checked={mode === 'full'}
          onChange={v => setMode(v ? 'full' : 'incremental')}
          leftLabel="Incremental"
          rightLabel="Full"
          disabled={running || busy}
        />
      </div>

      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'12px 0', borderTop:'1px solid var(--border)' }}>
        <div>
          <div style={{ fontWeight:500, fontSize:13 }}>Bank scan mode</div>
          <div style={{ fontSize:12, color:'var(--text-3)' }}>Full reconciles all users (slow)</div>
        </div>
        <Toggle
          checked={bankMode === 'full'}
          onChange={v => setBankMode(v ? 'full' : 'new_only')}
          leftLabel="New only"
          rightLabel="Full reconcile"
          disabled={running || busy}
        />
      </div>

      {fullModeWarn && (
        <div style={{
          marginTop:10, padding:'10px 12px',
          background:'var(--amber-soft)', border:'1px solid #fde68a',
          borderRadius:8, fontSize:12, color:'var(--amber-text)',
          lineHeight:1.5,
        }}>
          <div>⚠ Full mode pages every transaction for every user — expect ~20+ hours and only use after clearing the DB.</div>
        </div>
      )}

      <div style={{ marginTop:16 }}>
        {running ? (
          <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
            <div style={{ display:'flex', gap:10 }}>
              <button type="button" onClick={procPaused ? onResumeProcess : onPauseProcess} disabled={busy} style={{
                flex:1, minHeight:46,
                background:'linear-gradient(180deg,#fbbf24,#d97706)', color:'#1f2937',
                border:'none', borderRadius:10,
                fontWeight:700, fontSize:13, letterSpacing:.2,
                cursor: busy ? 'wait' : 'pointer',
                opacity: busy ? 0.65 : 1,
                boxShadow:'0 3px 10px rgba(217,119,6,.35)',
              }} title={procPaused ? 'Send SIGCONT to the runner process group' : 'Send SIGSTOP — freezes checkpoints in place'}>
                {procPaused ? '▶ Resume' : '⏸ Pause'}
              </button>
              <button type="button" onClick={onStop} disabled={busy} style={{
                flex:1, minHeight:46,
                background:'var(--red)', color:'#fff',
                border:'none', borderRadius:10,
                fontWeight:700, fontSize:13, letterSpacing:.2,
                display:'inline-flex', alignItems:'center', justifyContent:'center', gap:8,
                boxShadow:'0 4px 14px rgba(220,38,38,.25)',
                cursor: busy ? 'wait' : 'pointer',
              }} title="SIGTERM the whole pipeline (same as before)">
                {!procPaused && (
                  <>
                    <span style={{
                      width:12, height:12, borderRadius:3,
                      border:'2px solid #fff', borderTopColor:'transparent',
                      display:'inline-block', animation:'spin 1s linear infinite',
                    }}/>
                    <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
                  </>
                )}
                Stop
              </button>
            </div>
            <div style={{
              fontSize:11, color:'var(--text-3)',
              textAlign:'center',
              lineHeight:1.45,
              padding:'2px 4px',
            }}>
              Pause freezes the runner (SIGSTOP). Stop aborts with SIGTERM. Close the browser anytime — runner stays on EC2.
            </div>
          </div>
        ) : (
          <button onClick={onStart} disabled={busy} style={{
            width:'100%', height:48,
            background:'var(--green)', color:'#fff',
            border:'none', borderRadius:10,
            fontWeight:600, fontSize:15, letterSpacing:.2,
            display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10,
            boxShadow:'0 4px 14px rgba(22,163,74,.25)',
            cursor: busy ? 'wait' : 'pointer',
            opacity: busy ? 0.7 : 1,
          }} onMouseOver={e => { if (!busy) e.currentTarget.style.background = 'var(--green-hover)'; }}
             onMouseOut={e => { e.currentTarget.style.background = 'var(--green)'; }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="#fff"><path d="M5 4l14 8-14 8V4z"/></svg>
            {busy ? 'Starting…' : 'Start pipeline'}
          </button>
        )}
      </div>
    </Card>
  );
};


/* ─────────────────────────────────────────────────────────
   Scheduled runs
   ───────────────────────────────────────────────────────── */

const Scheduler = ({ schedules, onAdd, onRemove, busy }) => {
  const [time, setTime] = useState('02:00');
  const [days, setDays] = useState('Daily');
  const [adding, setAdding] = useState(false);

  const submit = async () => {
    setAdding(true);
    try { await onAdd({ time, days, tier: 'all' }); }
    finally { setAdding(false); }
  };

  return (
    <Card title="Scheduled runs" subtitle={`Clock is Australia/Melbourne · ${schedules.length} row${schedules.length===1?'':'s'} below`}>
      <div style={{ display:'flex', flexDirection:'column', gap:8, marginBottom:14 }}>
        {schedules.map(s => (
          <div key={s.id} style={{
            display:'grid', gridTemplateColumns:'auto 1fr auto', alignItems:'center', gap:12,
            padding:'10px 12px', border:'1px solid var(--border)', borderRadius:10,
            background:'#fafbfc',
          }}>
            <div style={{
              width:36, height:36, borderRadius:8,
              background:'#fff', border:'1px solid var(--border)',
              display:'inline-flex', alignItems:'center', justifyContent:'center',
              color:'var(--text-3)',
            }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
            </div>
            <div>
              <div style={{ fontWeight:600, fontSize:13 }}>{s.time_hhmm} · {s.days}</div>
              <div style={{ fontSize:11, color:'var(--text-3)' }}>
                Full pipeline (incremental), tier All
                {s.created_at ? <> · added {fmtDateTime(s.created_at)}</> : null}
                {s.last_run_at ? <> · last fired {fmtDateTime(s.last_run_at)}</> : null}
              </div>
            </div>
            <IconBtn danger onClick={() => onRemove(s.id)} title="Remove" disabled={busy}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>
            </IconBtn>
          </div>
        ))}
        {schedules.length === 0 && (
          <div style={{ fontSize:12, color:'var(--text-3)', textAlign:'center', padding:'18px 0' }}>No schedules configured</div>
        )}
      </div>

      <div style={{
        display:'grid', gridTemplateColumns:'90px 1fr auto', gap:8,
        padding:'12px 12px', background:'#fafbfc', border:'1px dashed var(--border-strong)', borderRadius:10,
      }}>
        <TextInput type="time" value={time} onChange={e => setTime(e.target.value)} />
        <Select value={days} onChange={setDays} options={[
          { value:'Daily',   label:'Daily' },
          { value:'Mon-Fri', label:'Mon-Fri' },
          { value:'Mon',     label:'Monday' },
          { value:'Tue',     label:'Tuesday' },
          { value:'Wed',     label:'Wednesday' },
          { value:'Thu',     label:'Thursday' },
          { value:'Fri',     label:'Friday' },
          { value:'Sat',     label:'Saturday' },
          { value:'Sun',     label:'Sunday' },
        ]}/>
        <button onClick={submit} disabled={adding} style={{
          padding:'0 14px', height:36, border:'none', background:'var(--blue)', color:'#fff',
          borderRadius:8, fontWeight:600, fontSize:13,
          opacity: adding ? 0.6 : 1,
        }}>{adding ? '…' : 'Add'}</button>
      </div>
    </Card>
  );
};


/* ─────────────────────────────────────────────────────────
   SMS Recipients
   ───────────────────────────────────────────────────────── */

const SmsRecipients = ({
  recipients, onAdd, onRemove, busy,
  smsEnabled, smsBusy, onToggleSms,
  onPatchRecipientSms, recipientSmsBusyId,
}) => {
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const [adding, setAdding] = useState(false);

  const submit = async () => {
    if (!name.trim() || !phone.trim()) return;
    setAdding(true);
    try {
      await onAdd({ name: name.trim(), phone: phone.trim() });
      setName(''); setPhone('');
    } finally { setAdding(false); }
  };

  return (
    <Card
      title="SMS recipients"
      subtitle="Twilio alerts. Master switch kills all sends; each row can mute one number (boss overnight) while you still receive texts."
    >
      <label style={{
        display:'flex', alignItems:'flex-start', gap:10, marginBottom:14,
        cursor: (smsBusy || busy) ? 'wait' : 'pointer',
        fontSize:13, color:'var(--text)',
      }}>
        <input
          type="checkbox"
          checked={smsEnabled}
          disabled={smsBusy || busy}
          onChange={() => onToggleSms()}
          style={{ marginTop:3 }}
        />
        <span>
          <strong>Send SMS</strong> when the pipeline starts / completes / fails
          {!smsEnabled && (
            <span style={{ display:'block', fontSize:11, color:'var(--amber-text)', marginTop:4 }}>
              Muted — recipients stay listed but receive nothing until re-enabled.
            </span>
          )}
        </span>
      </label>
      <div style={{ display:'flex', flexDirection:'column', gap:6, marginBottom:14 }}>
        {recipients.length === 0 && (
          <div style={{ fontSize:12, color:'var(--text-3)', textAlign:'center', padding:'14px 0' }}>No recipients yet — add one below.</div>
        )}
        {recipients.map(r => (
          <div key={r.id} style={{
            display:'grid',
            gridTemplateColumns:'auto 1fr auto auto',
            alignItems:'center', gap:10,
            padding:'8px 12px', border:'1px solid var(--border)', borderRadius:10,
            background:'#fafbfc',
          }}>
            <div style={{
              width:30, height:30, borderRadius:'50%',
              background:'#fff', border:'1px solid var(--border)',
              display:'inline-flex', alignItems:'center', justifyContent:'center',
              fontWeight:600, fontSize:11, color:'var(--text-3)',
            }}>{r.name.split(' ').map(p=>p[0]).slice(0,2).join('').toUpperCase()}</div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontWeight:600, fontSize:13 }}>{r.name}</div>
              <div style={{ fontSize:11, color:'var(--text-3)', fontVariantNumeric:'tabular-nums' }}>{r.phone}</div>
              {!r.sms_enabled && (
                <div style={{ fontSize:10, color:'var(--amber-text)', marginTop:2 }}>SMS paused for this number</div>
              )}
            </div>
            <label style={{
              display:'flex', flexDirection:'column', alignItems:'center', gap:2,
              cursor: (busy || recipientSmsBusyId === r.id) ? 'wait' : 'pointer',
              fontSize:9, color:'var(--text-mute)', textTransform:'uppercase', letterSpacing:0.3,
            }}>
              SMS
              <input
                type="checkbox"
                checked={!!r.sms_enabled}
                disabled={busy || recipientSmsBusyId === r.id}
                title="Receive pipeline SMS on this number"
                onChange={(e) => onPatchRecipientSms(r.id, e.target.checked)}
              />
            </label>
            <IconBtn danger onClick={() => onRemove(r.id)} title="Remove" disabled={busy}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>
            </IconBtn>
          </div>
        ))}
      </div>

      <div style={{
        display:'flex', flexDirection:'column', gap:8,
        padding:'12px', background:'#fafbfc', border:'1px dashed var(--border-strong)', borderRadius:10,
      }}>
        <div style={{ display:'flex', gap:8 }}>
          <TextInput placeholder="Name" value={name} onChange={e => setName(e.target.value)} style={{ flex:1 }}/>
          <TextInput placeholder="+61 4XX XXX XXX" value={phone} onChange={e => setPhone(e.target.value)} style={{ flex:1 }}/>
        </div>
        <button onClick={submit} disabled={adding || !name || !phone} style={{
          padding:'0 14px', height:36, border:'none', background:'var(--blue)', color:'#fff',
          borderRadius:8, fontWeight:600, fontSize:13, width:'100%',
          opacity: (adding || !name || !phone) ? 0.6 : 1,
        }}>{adding ? '…' : 'Add'}</button>
      </div>
    </Card>
  );
};


/* ─────────────────────────────────────────────────────────
   Live log (real terminal — fed by SSE or full-log fetch)
   ───────────────────────────────────────────────────────── */

const classifyLine = (line) => {
  if (/^PHASE\s/i.test(line) || /^==+/.test(line)) return 'phase';
  if (/^\[?[\d:]*\]?\s*PHASE\s/i.test(line)) return 'phase';
  if (/ERROR/.test(line) || /FAILED/.test(line) || /ABORTING/.test(line)) return 'error';
  if (/WARN/i.test(line)) return 'warn';
  if (/COMPLETE/i.test(line) || /✓|✔/.test(line) || /\bOK\b/.test(line)) return 'ok';
  return 'normal';
};

const LiveLog = ({ lines, paused, setPaused, onClear, status }) => {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || paused) return;
    ref.current.scrollTop = ref.current.scrollHeight;
  }, [lines, paused]);

  const live = status === 'running';

  return (
    <div id="live-log-anchor" style={{ scrollMarginTop: 14 }}>
    <Card
      title="Live log"
      subtitle="Streaming output from migration runner"
      right={
        <div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap', justifyContent:'flex-end' }}>
          <div style={{ display:'flex', alignItems:'center', gap:6, fontSize:11, color:'var(--text-3)' }}>
            <span style={{
              width:8, height:8, borderRadius:'50%',
              background: live ? (paused ? '#9ca3af' : '#22c55e') : '#9ca3af',
              boxShadow: (live && !paused) ? '0 0 0 3px rgba(34,197,94,.2)' : 'none',
              animation: (live && !paused) ? 'pulse 1.4s infinite' : 'none',
            }}/>
            <style>{`@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }`}</style>
            {live ? (paused ? 'Paused' : 'Live') : 'Idle'}
          </div>
          <button onClick={() => setPaused(!paused)} disabled={!live} style={{
            height:30, padding:'0 12px', background:'#fff', border:'1px solid var(--border-strong)',
            borderRadius:7, fontSize:12, fontWeight:500, color:'var(--text-2)',
            opacity: live ? 1 : 0.5, cursor: live ? 'pointer' : 'not-allowed',
          }}>{paused ? 'Resume scroll' : 'Pause scroll'}</button>
          <button onClick={onClear} style={{
            height:30, padding:'0 12px', background:'#fff', border:'1px solid var(--border-strong)',
            borderRadius:7, fontSize:12, fontWeight:500, color:'var(--text-2)',
          }}>Clear</button>
        </div>
      }
      padded={false}
      style={{ minHeight: 0 }}
    >
      <div ref={ref} style={{
        background:'var(--term-bg)',
        color:'var(--term-text)',
        fontFamily: "'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace",
        fontSize: 12.5, lineHeight: 1.55,
        padding:'14px 18px',
        overflowY:'auto',
        maxHeight: 'min(52vh, 520px)',
        minHeight: 220,
      }}>
        {lines.length === 0 && (
          <div style={{ color:'var(--term-mute)' }}>$ Waiting for pipeline activity…</div>
        )}
        {lines.map((l, i) => {
          const kind = l.kind || classifyLine(l.text);
          let color = 'var(--term-text)';
          let weight = 400;
          if (kind === 'phase')  { color = 'var(--term-cyan)'; weight = 700; }
          if (kind === 'error')  { color = 'var(--term-red)';  weight = 600; }
          if (kind === 'warn')   { color = 'var(--term-amber)';}
          if (kind === 'ok')     { color = 'var(--term-green)';}
          return (
            <div key={i} style={{ color, fontWeight:weight, whiteSpace:'pre-wrap', wordBreak:'break-word' }}>
              {l.text}
            </div>
          );
        })}
      </div>
    </Card>
    </div>
  );
};


/* ─────────────────────────────────────────────────────────
   Run Summary (before vs after) + Live DB counts
   ───────────────────────────────────────────────────────── */

const RunSummary = ({ run, currentCounts }) => {
  const hasBeforeAfter = run && run.before_counts && run.after_counts;
  const subtitle = hasBeforeAfter
    ? `Database state for the most recent completed run (${fmtDateTime(run.started_at)})`
    : 'No completed run yet — showing current DB state';

  return (
    <Card title="Run summary — before vs after" subtitle={subtitle}>
      <table style={{ width:'100%', borderCollapse:'collapse', fontSize:13 }}>
        <thead>
          <tr style={{ textAlign:'left', color:'var(--text-3)', fontSize:11, textTransform:'uppercase', letterSpacing:.5 }}>
            <th style={{ padding:'10px 12px', borderBottom:'1px solid var(--border)' }}>Metric</th>
            {hasBeforeAfter
              ? <>
                  <th style={{ padding:'10px 12px', borderBottom:'1px solid var(--border)', textAlign:'right' }}>Before</th>
                  <th style={{ padding:'10px 12px', borderBottom:'1px solid var(--border)', textAlign:'right' }}>After</th>
                  <th style={{ padding:'10px 12px', borderBottom:'1px solid var(--border)', textAlign:'right' }}>Change</th>
                </>
              : <th style={{ padding:'10px 12px', borderBottom:'1px solid var(--border)', textAlign:'right' }}>Current</th>}
          </tr>
        </thead>
        <tbody>
          {SUMMARY_METRICS.map(metric => {
            const before = hasBeforeAfter ? run.before_counts[metric] : null;
            const after = hasBeforeAfter ? run.after_counts[metric] : (currentCounts ? currentCounts[metric] : null);
            const diff = (before != null && after != null) ? Number(after) - Number(before) : null;
            const positive = diff != null && diff >= 0;
            return (
              <tr key={metric} style={{ borderBottom:'1px solid var(--border)' }}>
                <td style={{ padding:'12px', fontWeight:500 }}>{metric}</td>
                {hasBeforeAfter && (
                  <td style={{ padding:'12px', textAlign:'right', fontVariantNumeric:'tabular-nums', color:'var(--text-2)' }}>{fmtNum(before)}</td>
                )}
                <td style={{ padding:'12px', textAlign:'right', fontVariantNumeric:'tabular-nums', fontWeight:600 }}>{fmtNum(after)}</td>
                {hasBeforeAfter && (
                  <td style={{ padding:'12px', textAlign:'right' }}>
                    {diff == null ? <span style={{ color:'var(--text-3)' }}>—</span> : (
                      <span style={{
                        display:'inline-flex', alignItems:'center', gap:4,
                        fontWeight:600,
                        color: diff === 0 ? 'var(--text-3)' : (positive ? 'var(--green-text)' : 'var(--red-text)'),
                        background: diff === 0 ? '#f3f4f6' : (positive ? 'var(--green-soft)' : 'var(--red-soft)'),
                        padding:'2px 10px', borderRadius:999,
                        fontVariantNumeric:'tabular-nums',
                      }}>
                        {diff === 0 ? '—' : (positive ? `+${fmtNum(diff)}` : fmtNum(diff))}
                      </span>
                    )}
                  </td>
                )}
              </tr>
            );
          })}
        </tbody>
      </table>
    </Card>
  );
};


/* ─────────────────────────────────────────────────────────
   Run history — transposed column-per-run layout with
   per-phase script breakdown (matching design template)
   ───────────────────────────────────────────────────────── */

const SCRIPT_ROWS = [
  { num: '01',  name: 'user-discovery',     phase: '1'  },
  { num: '02',  name: 'user-migration',     phase: '2'  },
  { num: '4b',  name: 'tags-templates',     phase: '4b' },
  { num: '03',  name: 'txn-betslip-sync',   phase: '3'  },
  { num: '04',  name: 'balance-refresh',    phase: '4'  },
  { num: '05',  name: 'status-blocks',      phase: '5'  },
  { num: '5b',  name: 'type-category-fix',  phase: '5b' },
  { num: '5c',  name: 'racing-domain',      phase: '5c' },
  { num: '06',  name: 'counts-sms',         phase: '6'  },
];

const runRowsTotal = (r) => {
  if (!r.before_counts || !r.after_counts) return null;
  let total = 0;
  for (const k of SUMMARY_METRICS) {
    const before = Number(r.before_counts[k]);
    const after = Number(r.after_counts[k]);
    if (Number.isFinite(before) && Number.isFinite(after)) {
      total += Math.abs(after - before);
    }
  }
  return total > 0 ? total : 0;
};

const runWarnings = (r) => {
  if (r.status === 'warnings') return r.exit_code != null ? Math.max(1, Math.abs(r.exit_code)) : 1;
  return 0;
};

const runErrors = (r) => {
  if (r.status === 'failed') return r.exit_code != null ? Math.max(1, Math.abs(r.exit_code)) : 1;
  return 0;
};

const RunHistory = ({ runs, onView }) => {
  const cell = { padding: '12px 14px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' };
  const labelCell = { ...cell, position:'sticky', left:0, background:'#fff', fontWeight:600, color:'var(--text-2)', fontSize:12, textTransform:'uppercase', letterSpacing:.5, borderRight:'1px solid var(--border)', zIndex:1 };
  const dataCell = { ...cell, textAlign:'right', fontVariantNumeric:'tabular-nums' };

  const dateLine = (iso) => {
    const d = new Date(iso);
    if (isNaN(d.getTime())) return <div style={{ textAlign:'right', fontSize:13 }}>{iso}</div>;
    const pad = n => String(n).padStart(2,'0');
    const date = `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()}`;
    const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
    return (
      <div style={{ textAlign:'right' }}>
        <div style={{ fontWeight:600, fontSize:13 }}>{date}</div>
        <div style={{ fontSize:11, color:'var(--text-3)', fontWeight:400 }}>{time}</div>
      </div>
    );
  };

  const statusBadge = (s) => (
    <Badge kind={s}>
      {s === 'complete' && '✓'}
      {s === 'failed'   && '✗'}
      {s === 'warnings' && '!'}
      {s === 'stopped'  && '■'}
      {s === 'running'  && '●'}
      &nbsp;{s === 'complete' ? 'Complete' : s === 'failed' ? 'Failed' : s === 'warnings' ? 'Warnings' : s === 'stopped' ? 'Stopped' : s === 'running' ? 'Running' : s}
    </Badge>
  );

  if (runs.length === 0) {
    return (
      <Card title="Run history" subtitle="0 runs" padded>
        <div style={{ fontSize:13, color:'var(--text-3)', textAlign:'center', padding:'30px 0' }}>
          No runs yet — start one from the Start a run card above.
        </div>
      </Card>
    );
  }

  return (
    <Card title="Run history" subtitle={`${runs.length} runs · latest column left · If status is Complete, the run succeeded — script numbers below are hints, not pass/fail scores.`} padded={false}>
      <div style={{ overflowX:'auto' }}>
        <table style={{ width:'100%', borderCollapse:'collapse', fontSize:13, minWidth: 900 }}>
          <thead>
            <tr>
              <th style={{ ...labelCell, borderBottom:'1px solid var(--border)', textAlign:'left' }}>Run</th>
              {runs.map(r => (
                <th key={r.id} style={{ ...cell, textAlign:'right', background:'#fafbfc', borderBottom:'1px solid var(--border)' }}>
                  {dateLine(r.started_at)}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            <tr>
              <td style={labelCell}>Status</td>
              {runs.map(r => <td key={r.id} style={{ ...cell, textAlign:'right' }}>{statusBadge(r.status)}</td>)}
            </tr>
            <tr>
              <td style={labelCell}>Tier</td>
              {runs.map(r => (
                <td key={r.id} style={{ ...cell, textAlign:'right' }}>
                  <div style={{ fontWeight:600 }}>{TIER_LABEL(r.tier)}</div>
                  <div style={{ fontSize:11, color:'var(--text-3)' }}>{TIER_DESC(r.tier)}</div>
                </td>
              ))}
            </tr>
            <tr>
              <td style={labelCell}>Duration</td>
              {runs.map(r => <td key={r.id} style={dataCell}>{fmtDuration(r.started_at, r.finished_at)}</td>)}
            </tr>
            <tr>
              <td style={labelCell}>Summary activity total</td>
              {runs.map(r => {
                const total = runRowsTotal(r);
                return (
                  <td key={r.id} style={{ ...dataCell, maxWidth: 220 }}>
                    <div style={{ fontVariantNumeric:'tabular-nums', fontWeight:600 }}>{total != null ? fmtNum(total) : '—'}</div>
                    <div style={{ fontSize:10, color:'var(--text-3)', fontWeight:400, marginTop:4, whiteSpace:'normal', lineHeight:1.35 }}>
                      Official net movement in the database for this run (before → after). Smaller numbers in “Scripts” logs do not mean failure — they measure different steps (see Scripts note).
                    </div>
                  </td>
                );
              })}
            </tr>
            <tr>
              <td style={labelCell}>Warnings</td>
              {runs.map(r => {
                const w = runWarnings(r);
                return (
                  <td key={r.id} style={dataCell}>
                    <span style={{ color: w > 0 ? 'var(--amber-text)' : 'var(--text-3)', fontWeight: w > 0 ? 600 : 400 }}>{w}</span>
                  </td>
                );
              })}
            </tr>
            <tr>
              <td style={labelCell}>Errors</td>
              {runs.map(r => {
                const e = runErrors(r);
                return (
                  <td key={r.id} style={dataCell}>
                    <span style={{ color: e > 0 ? 'var(--red-text)' : 'var(--text-3)', fontWeight: e > 0 ? 600 : 400 }}>{e}</span>
                  </td>
                );
              })}
            </tr>
            <tr>
              <td style={labelCell}>Started by</td>
              {runs.map(r => <td key={r.id} style={{ ...cell, textAlign:'right', color:'var(--text-2)' }}>{r.started_by || '—'}</td>)}
            </tr>

            {/* Scripts / phases section header */}
            <tr>
              <td colSpan={runs.length + 1} style={{
                padding:'14px 14px 8px', background:'#fafbfc',
                borderTop:'1px solid var(--border)', borderBottom:'1px solid var(--border)',
                position:'sticky', left:0,
              }}>
                <div style={{ fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:.6, color:'var(--text-2)' }}>
                  Scripts
                </div>
                <div style={{ fontSize:11, color:'var(--text-3)', marginTop:2, fontWeight:400, lineHeight:1.5 }}>
                  <strong>For managers:</strong> the green check is what matters — it means that phase finished without aborting the pipeline.
                  The text beside each script comes from engineering logs (counts of “walked” users or queued inserts); those totals often differ from the summary table above.
                  That mismatch is normal and is <strong>not</strong> missing data unless Status shows Failed.
                  Where shown, “Net DB change … matches summary card” is the plain-English reconciliation with the table.
                </div>
              </td>
            </tr>

            {SCRIPT_ROWS.map(s => (
              <tr key={s.phase}>
                <td style={{ ...labelCell, textTransform:'none', letterSpacing:0 }}>
                  <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                    <span style={{
                      fontFamily:"'JetBrains Mono', monospace", fontSize:10,
                      color:'var(--text-mute)', background:'#f3f4f6',
                      padding:'2px 6px', borderRadius:4, fontWeight:600,
                    }}>{s.num}</span>
                    <span style={{
                      fontFamily:"'JetBrains Mono', monospace", fontSize:12,
                      color:'var(--text)', fontWeight:500,
                    }}>{s.name}</span>
                  </div>
                </td>
                {runs.map(r => {
                  const tierNum = r.tier === 'all' ? 99 : parseInt(r.tier, 10) || 1;
                  const isRacing = s.phase === '5c';
                  const skippedRacing = isRacing && r.skip_racing;
                  const ps = r.phase_stats && r.phase_stats[s.phase];
                  const rowHint = ps && ps.rows ? ps.rows : null;
                  const durHint = ps && ps.duration ? ps.duration : null;

                  if (skippedRacing) {
                    return (
                      <td key={r.id} style={{ ...cell, textAlign:'right' }}>
                        <span style={{ color:'var(--text-mute)', fontSize:12, fontStyle:'italic' }}>skipped</span>
                      </td>
                    );
                  }
                  if (isRacing && tierNum < 3) {
                    return <td key={r.id} style={{ ...cell, textAlign:'right', color:'var(--text-mute)' }}>—</td>;
                  }

                  const isFailed = r.status === 'failed';
                  const isWarn = r.status === 'warnings';
                  const tint = isFailed ? 'var(--red)' : isWarn ? 'var(--amber)' : 'var(--green)';
                  const icon = isFailed ? '✗' : isWarn ? '!' : '✓';

                  return (
                    <td key={r.id} style={{ ...cell, textAlign:'right' }}>
                      <div style={{ display:'inline-flex', alignItems:'center', gap:10, justifyContent:'flex-end' }}>
                        <div style={{ textAlign:'right', minWidth:108, maxWidth:160 }}>
                          <div style={{ fontSize:10, color:'var(--text-mute)', textTransform:'uppercase', letterSpacing:0.3 }}>Log detail</div>
                          <div style={{
                            fontSize:11, color:'var(--text-3)', whiteSpace:'normal', lineHeight:1.35,
                            wordBreak:'break-word',
                          }}>{rowHint || '—'}</div>
                          <div style={{ fontSize:10, color:'var(--text-mute)', marginTop:4 }}>Duration</div>
                          <div style={{ fontVariantNumeric:'tabular-nums', fontSize:11, color:'var(--text-3)' }}>{durHint || '—'}</div>
                        </div>
                        <span style={{
                          width:20, height:20, borderRadius:'50%',
                          background: tint, color:'#fff',
                          display:'inline-flex', alignItems:'center', justifyContent:'center',
                          fontSize:12, fontWeight:700, flexShrink:0,
                        }}>{icon}</span>
                      </div>
                    </td>
                  );
                })}
              </tr>
            ))}

            <tr>
              <td style={{ ...labelCell, borderBottom:'none' }}></td>
              {runs.map(r => (
                <td key={r.id} style={{ ...cell, textAlign:'right', borderBottom:'none' }}>
                  <button onClick={() => onView(r)} style={{
                    background:'transparent', border:'1px solid var(--border-strong)',
                    padding:'6px 12px', borderRadius:7, fontSize:12, fontWeight:500,
                    color:'var(--text-2)',
                  }}>View log</button>
                </td>
              ))}
            </tr>
          </tbody>
        </table>
      </div>
    </Card>
  );
};


/* ─────────────────────────────────────────────────────────
   Toast / error banner
   ───────────────────────────────────────────────────────── */

const Toast = ({ msg, kind = 'error', onClose }) => {
  if (!msg) return null;
  const colors = {
    error: { bg: '#fee2e2', fg: '#991b1b', border: '#fca5a5' },
    info:  { bg: '#eff6ff', fg: '#1d4ed8', border: '#93c5fd' },
    ok:    { bg: '#dcfce7', fg: '#166534', border: '#86efac' },
  }[kind];
  return (
    <div style={{
      position:'fixed', top:18, right:18, zIndex:100,
      padding:'12px 16px',
      background: colors.bg, color: colors.fg,
      border: `1px solid ${colors.border}`, borderRadius:10,
      fontSize:13, fontWeight:500,
      maxWidth: 420, boxShadow:'0 12px 32px rgba(0,0,0,.12)',
      display:'flex', gap:10, alignItems:'flex-start',
    }}>
      <div style={{ flex:1 }}>{msg}</div>
      <button onClick={onClose} style={{
        background:'transparent', border:'none',
        color: colors.fg, fontWeight:700, fontSize:18, lineHeight:1, padding:0,
      }}>×</button>
    </div>
  );
};


/* ─────────────────────────────────────────────────────────
   App
   ───────────────────────────────────────────────────────── */

const App = () => {
  // Run config (form state — tier is always 'all', no skip-racing)
  const [mode, setMode] = useState('incremental');
  const [bankMode, setBankMode] = useState('new_only');

  // Live state
  const [currentRun, setCurrentRun] = useState(null);
  const [history, setHistory] = useState([]);
  const [counts, setCounts] = useState(null);
  const [recipients, setRecipients] = useState([]);
  const [schedules, setSchedules] = useState([]);

  // Log buffer
  const [lines, setLines] = useState([]);
  const [paused, setPaused] = useState(false);
  const [viewingLogOf, setViewingLogOf] = useState(null);  // null = current/latest; else a run object

  // Misc
  const [busy, setBusy] = useState(false);
  const [toast, setToast] = useState(null);
  const [smsEnabled, setSmsEnabled] = useState(true);
  const [smsBusy, setSmsBusy] = useState(false);
  const [recipientSmsBusyId, setRecipientSmsBusyId] = useState(null);
  const esRef = useRef(null);

  const showError = (msg) => setToast({ kind:'error', msg });
  const showInfo  = (msg) => setToast({ kind:'info',  msg });

  // ---- Data loaders ----------------------------------------------------

  const refreshStatus = useCallback(async () => {
    try {
      const s = await api('/api/status');
      setCurrentRun(s.current_run);
    } catch (e) { /* ignore — toast already handled by api wrapper */ }
  }, []);

  const refreshHistory = useCallback(async () => {
    try { const r = await api('/api/runs?limit=20'); setHistory(r.runs); } catch {}
  }, []);

  const refreshCounts = useCallback(async () => {
    try { const r = await api('/api/db-counts'); setCounts(r.counts); } catch {}
  }, []);

  const refreshRecipients = useCallback(async () => {
    try { const r = await api('/api/recipients'); setRecipients(r.recipients); } catch {}
  }, []);

  const refreshSchedules = useCallback(async () => {
    try { const r = await api('/api/schedules'); setSchedules(r.schedules); } catch {}
  }, []);

  const refreshAll = useCallback(async () => {
    await Promise.all([refreshStatus(), refreshHistory(), refreshRecipients(), refreshSchedules()]);
  }, [refreshStatus, refreshHistory, refreshRecipients, refreshSchedules]);

  // Initial load + DB counts (counts are slow so do them separately)
  useEffect(() => { refreshAll(); refreshCounts(); }, [refreshAll, refreshCounts]);

  useEffect(() => {
    api('/api/settings')
      .then((s) => setSmsEnabled(!!s.sms_notifications_enabled))
      .catch(() => {});
  }, []);

  // Keep log viewer's selected run in sync with poll (status / finished_at) without needless rerenders.
  useEffect(() => {
    setViewingLogOf((prev) => {
      if (!prev?.id) return prev;
      const fresh = history.find((h) => h.id === prev.id);
      if (!fresh) return prev;
      if (fresh.status !== prev.status || fresh.finished_at !== prev.finished_at)
        return fresh;
      return prev;
    });
  }, [history]);

  // Periodic poll of status + history (cheap) every 6 s
  useEffect(() => {
    const t = setInterval(() => { refreshStatus(); refreshHistory(); }, 6000);
    return () => clearInterval(t);
  }, [refreshStatus, refreshHistory]);

  // ---- SSE log streaming ----------------------------------------------

  const closeStream = useCallback(() => {
    if (esRef.current) { esRef.current.close(); esRef.current = null; }
  }, []);

  useEffect(() => {
    closeStream();
    const target = viewingLogOf || currentRun;
    if (!target) return;

    if (target.status !== 'running') {
      setLines([]);
      fetch(`/api/runs/${target.id}/log`, { credentials:'include' })
        .then(r => r.text())
        .then(text => {
          const ls = text.split('\n').filter(l => l.length).map(t => ({ text: t }));
          setLines(ls);
        })
        .catch(e => showError(`Could not fetch log: ${e.message}`));
      return undefined;
    }

    setLines([]);
    let reconnectTimer = null;
    let cancelled = false;

    const connect = () => {
      if (cancelled) return;
      closeStream();
      const es = new EventSource(`/api/runs/${target.id}/stream`);
      esRef.current = es;
      es.onmessage = (e) => {
        try {
          const data = JSON.parse(e.data);
          if (data.text != null) {
            setLines(prev => [...prev.slice(-2999), { text: data.text }]);
          }
        } catch {}
      };
      es.addEventListener('end', () => {
        cancelled = true;
        if (reconnectTimer) clearTimeout(reconnectTimer);
        refreshStatus();
        refreshHistory();
        refreshCounts();
        closeStream();
      });
      es.onerror = () => {
        if (cancelled) return;
        try { es.close(); } catch {}
        if (cancelled) return;
        reconnectTimer = setTimeout(connect, 2500);
      };
    };

    connect();
    return () => {
      cancelled = true;
      if (reconnectTimer) clearTimeout(reconnectTimer);
      closeStream();
    };
  }, [
    currentRun?.id, currentRun?.status,
    viewingLogOf?.id, viewingLogOf?.status,
    closeStream, refreshStatus, refreshHistory, refreshCounts,
  ]);

  // ---- Actions --------------------------------------------------------

  const onStart = async () => {
    setBusy(true);
    try {
      const run = await api('/api/runs', {
        method: 'POST',
        body: JSON.stringify({ tier: 'all', mode, bank_mode: bankMode, skip_racing: false }),
      });
      setCurrentRun(run);
      setViewingLogOf(null);
      setLines([]);
      showInfo(`Pipeline started — Run #${run.id}`);
      refreshHistory();
    } catch (e) {
      showError(`Could not start: ${e.message}`);
    } finally { setBusy(false); }
  };

  const onStop = async () => {
    if (!currentRun) return;
    if (!confirm('Stop the running pipeline with SIGTERM? You can start again later.')) return;
    setBusy(true);
    try {
      await api(`/api/runs/${currentRun.id}/stop`, { method: 'POST' });
      showInfo('Stop signal sent');
    } catch (e) {
      showError(`Could not stop: ${e.message}`);
    } finally { setBusy(false); }
  };

  const onPauseProcess = async () => {
    if (!currentRun) return;
    setBusy(true);
    try {
      await api(`/api/runs/${currentRun.id}/pause`, { method: 'POST' });
      showInfo('Pause sent — runner frozen with SIGSTOP');
      await refreshStatus();
    } catch (e) {
      showError(`Could not pause: ${e.message}`);
    } finally { setBusy(false); }
  };

  const onResumeProcess = async () => {
    if (!currentRun) return;
    setBusy(true);
    try {
      await api(`/api/runs/${currentRun.id}/resume`, { method: 'POST' });
      showInfo('Resume sent — SIGCONT');
      await refreshStatus();
    } catch (e) {
      showError(`Could not resume: ${e.message}`);
    } finally { setBusy(false); }
  };

  const onAddRecipient = async ({ name, phone }) => {
    try { await api('/api/recipients', { method:'POST', body: JSON.stringify({ name, phone }) }); refreshRecipients(); }
    catch (e) { showError(`Could not add: ${e.message}`); throw e; }
  };

  const onRemoveRecipient = async (rid) => {
    if (!confirm('Remove this recipient?')) return;
    try { await api(`/api/recipients/${rid}`, { method:'DELETE' }); refreshRecipients(); }
    catch (e) { showError(`Could not remove: ${e.message}`); }
  };

  const onToggleSmsNotifications = async () => {
    setSmsBusy(true);
    try {
      const s = await api('/api/settings', {
        method: 'PATCH',
        body: JSON.stringify({ sms_notifications_enabled: !smsEnabled }),
      });
      setSmsEnabled(!!s.sms_notifications_enabled);
      showInfo(
        s.sms_notifications_enabled
          ? 'SMS alerts enabled for manual and scheduled runs'
          : 'SMS muted — pipeline still runs; nobody is paged until you turn this back on',
      );
    } catch (e) {
      showError(`Could not update SMS setting: ${e.message}`);
    } finally {
      setSmsBusy(false);
    }
  };

  const onPatchRecipientSms = async (rid, enabled) => {
    setRecipientSmsBusyId(rid);
    try {
      await api(`/api/recipients/${rid}`, {
        method: 'PATCH',
        body: JSON.stringify({ sms_enabled: enabled }),
      });
      await refreshRecipients();
      showInfo(enabled ? 'Recipient will receive pipeline SMS' : 'Recipient muted — others unchanged');
    } catch (e) {
      showError(`Could not update recipient: ${e.message}`);
    } finally {
      setRecipientSmsBusyId(null);
    }
  };

  const onAddSchedule = async ({ time, days, tier }) => {
    try { await api('/api/schedules', { method:'POST', body: JSON.stringify({ time, days, tier }) }); refreshSchedules(); }
    catch (e) { showError(`Could not add: ${e.message}`); throw e; }
  };

  const onRemoveSchedule = async (sid) => {
    if (!confirm('Remove this schedule?')) return;
    try { await api(`/api/schedules/${sid}`, { method:'DELETE' }); refreshSchedules(); }
    catch (e) { showError(`Could not remove: ${e.message}`); }
  };

  const onLogout = async () => {
    try { await api('/api/auth/logout', { method:'POST' }); } catch {}
    location.href = '/login';
  };

  const onViewLog = (run) => {
    setViewingLogOf(run);
    requestAnimationFrame(() => {
      document.getElementById('live-log-anchor')?.scrollIntoView({
        behavior: 'smooth',
        block: 'nearest',
      });
    });
  };
  const onCloseViewLog = () => { setViewingLogOf(null); };

  // Find the most recent finished run for the summary card
  const lastRun = history.find(r => r.status !== 'running') || null;
  const summaryRun = lastRun;
  const showingHistoricalLog = !!viewingLogOf && viewingLogOf.id !== currentRun?.id;
  const logSubject = viewingLogOf || currentRun;
  const streamActive = !!(logSubject && logSubject.status === 'running');

  return (
    <div style={{ minHeight:'100vh', background:'var(--bg)' }}>
      <TopNav onLogout={onLogout}/>
      <main style={{ padding:'18px 26px 40px', maxWidth:1600, margin:'0 auto' }}>
        <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', margin:'8px 0 20px' }}>
          <h1 style={{ fontSize:24, fontWeight:700, margin:0 }}>Migration Console</h1>
          <div style={{ fontSize:12, color:'var(--text-3)' }}>
            Legacy → Aurora MySQL · runner v2.14.4
          </div>
        </div>

        <StatusBanner run={currentRun} lastRun={lastRun}/>

        <div style={{
          display:'grid',
          gridTemplateColumns: 'minmax(360px, 420px) 1fr',
          gap:18, marginTop:18,
        }}>
          <div style={{ display:'flex', flexDirection:'column', gap:18 }}>
            <StartRun
              mode={mode} setMode={setMode}
              bankMode={bankMode} setBankMode={setBankMode}
              running={!!currentRun}
              procPaused={!!currentRun?.process_paused}
              busy={busy}
              onStart={onStart}
              onStop={onStop}
              onPauseProcess={onPauseProcess}
              onResumeProcess={onResumeProcess}
            />
            <Scheduler schedules={schedules} onAdd={onAddSchedule} onRemove={onRemoveSchedule} busy={busy}/>
            <SmsRecipients
              recipients={recipients}
              onAdd={onAddRecipient}
              onRemove={onRemoveRecipient}
              busy={busy}
              smsEnabled={smsEnabled}
              smsBusy={smsBusy}
              onToggleSms={onToggleSmsNotifications}
              onPatchRecipientSms={onPatchRecipientSms}
              recipientSmsBusyId={recipientSmsBusyId}
            />
          </div>
          <div style={{ display:'flex', flexDirection:'column', gap:18 }}>
            {showingHistoricalLog && (
              <div style={{
                padding:'10px 16px', background:'var(--blue-soft)',
                border:'1px solid #bfdbfe', borderRadius:10,
                display:'flex', justifyContent:'space-between', alignItems:'center',
                fontSize:13, color:'var(--blue)',
              }}>
                <div style={{ lineHeight:1.45 }}>
                  <strong>Saved log</strong> for run #{viewingLogOf.id} ({fmtDateTime(viewingLogOf.started_at)}).
                  This pane follows whichever run you chose — not necessarily the currently running job.
                  {currentRun && viewingLogOf.id !== currentRun.id && (
                    <span> Another run may be live; press Back to live to attach to run #{currentRun.id}.</span>
                  )}
                </div>
                <button onClick={onCloseViewLog} style={{
                  background:'transparent', border:'1px solid var(--blue)', color:'var(--blue)',
                  padding:'4px 10px', borderRadius:6, fontSize:12, fontWeight:600,
                }}>Back to live</button>
              </div>
            )}
            <LiveLog
              lines={lines} paused={paused} setPaused={setPaused}
              onClear={() => setLines([])}
              status={streamActive ? 'running' : 'idle'}
            />
            <RunSummary run={summaryRun} currentCounts={counts}/>
          </div>
        </div>

        <div style={{ marginTop:18 }}>
          <RunHistory runs={history} onView={onViewLog}/>
        </div>

        <div style={{ marginTop:24, textAlign:'center', fontSize:11, color:'var(--text-mute)' }}>
          Betcore Migrate · build 2026.05.15 · runner connected to <span style={{ color:'var(--text-3)' }}>migrate.upcoz.internal</span>
        </div>
      </main>
      <Toast msg={toast?.msg} kind={toast?.kind} onClose={() => setToast(null)}/>
    </div>
  );
};


/* On boot, check auth status. If not authed, bounce to /login. */
(async () => {
  try {
    const s = await fetch('/api/auth/status', { credentials:'include' }).then(r => r.json());
    if (!s.authed) { location.href = '/login'; return; }
    if (!s.password_set) {
      document.getElementById('root').innerHTML =
        '<div style="padding:60px;text-align:center;font-family:Inter,sans-serif">' +
        '<h2>Console password not set</h2>' +
        '<p>Run <code>python3 -m backend.set_password</code> on the server, then refresh.</p></div>';
      return;
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
  } catch (e) {
    document.getElementById('root').innerHTML =
      '<div style="padding:60px;text-align:center;font-family:Inter,sans-serif;color:#dc2626">' +
      'Could not contact backend. Is the console service running?</div>';
  }
})();
