/* workflows.jsx — automated recurring scans + fix shipping
 *
 * Three presets:
 *   Watch     — scan on a schedule, notify on change.
 *   Maintain  — scan + diagnose, draft fixes for review.
 *   Autopilot — scan + diagnose + auto-ship auto-deployable fixes.
 *
 * Persistence: aiso:workflows:v1 in localStorage.
 *   { workflows: [{ id, siteId, preset, schedule, threshold, channels, lastRun, nextRun, runs[] }] }
 *
 * Runtime: a lightweight in-browser ticker advances "next run" relative to now;
 * when crossed, it appends a synthetic run entry. Real cron is wired server-side
 * via Vercel Cron — this UI is the configuration & activity surface.
 */

const { useState: useStateWf, useEffect: useEffectWf, useMemo: useMemoWf, useRef: useRefWf } = React;

const WF_KEY = 'aiso:workflows:v1';

const PRESETS = [
  {
    id: 'watch',
    name: 'Watch',
    tag: 'observe',
    short: 'Scan on a schedule. Notify only when something material changes.',
    desc: 'A quiet pair of eyes. Runs a full visibility scan on your cadence, compares to last week, and pings you if score crosses your threshold or a competitor takes your spot.',
    actions: ['Scan', 'Diff', 'Notify'],
    color: 'oklch(78% 0.14 200)',
    deploys: false,
    defaultSchedule: 'weekly-mon',
    recommended: ['exploring', 'mature brand'],
  },
  {
    id: 'maintain',
    name: 'Maintain',
    tag: 'diagnose',
    short: 'Scan + diagnose + draft. You approve every change.',
    desc: 'The day job. Scans, runs the fixes engine against the new state, drafts PRs and content updates, and sends you a digest. You ship what you like; we keep the diagnosis fresh.',
    actions: ['Scan', 'Diff', 'Diagnose', 'Draft PR', 'Notify'],
    color: 'oklch(78% 0.16 var(--accent-h))',
    deploys: false,
    defaultSchedule: 'weekly-mon',
    recommended: ['agencies', 'active SEO work'],
  },
  {
    id: 'autopilot',
    name: 'Autopilot',
    tag: 'ship',
    short: 'Scan + diagnose + ship auto-deployable fixes. Notified after.',
    desc: 'Hands off. Every Monday we measure, diagnose, and ship anything labelled auto-deployable straight to your repo. You wake up to merged PRs and a fresh score.',
    actions: ['Scan', 'Diff', 'Diagnose', 'Ship PR', 'Re-scan', 'Notify'],
    color: 'oklch(78% 0.16 var(--accent-h2))',
    deploys: true,
    defaultSchedule: 'weekly-mon',
    recommended: ['ship-and-forget', 'in-house teams'],
  },
];

const SCHEDULES = [
  { id: 'daily',      label: 'Every day',         cadence: 'daily',  dayLabel: '09:00 local',   nextMs: 24 * 3600_000 },
  { id: 'weekly-mon', label: 'Mondays',           cadence: 'weekly', dayLabel: '09:00 local',   nextMs: 7 * 24 * 3600_000 },
  { id: 'weekly-fri', label: 'Fridays',           cadence: 'weekly', dayLabel: '16:00 local',   nextMs: 7 * 24 * 3600_000 },
  { id: 'biweekly',   label: 'Every two weeks',   cadence: 'biweekly', dayLabel: 'Monday 09:00', nextMs: 14 * 24 * 3600_000 },
  { id: 'monthly',    label: 'First of month',    cadence: 'monthly', dayLabel: '09:00 local',  nextMs: 30 * 24 * 3600_000 },
];

const CHANNELS = [
  { id: 'email', label: 'Email digest', sub: 'inbox', icon: '✉' },
  { id: 'slack', label: 'Slack',        sub: '#growth', icon: '#' },
  { id: 'push',  label: 'iOS push',     sub: 'on change', icon: '◌' },
  { id: 'webhook', label: 'Webhook',    sub: 'JSON POST', icon: '↗' },
];

// ─── persistence ────────────────────────────────────────────────────────────
function loadWorkflows() {
  try {
    const j = JSON.parse(localStorage.getItem(WF_KEY) || '{}');
    return j.workflows || [];
  } catch { return []; }
}
function saveWorkflows(arr) {
  try { localStorage.setItem(WF_KEY, JSON.stringify({ workflows: arr })); } catch {}
}
function uid() { return Math.random().toString(36).slice(2, 9); }

function computeNextRun(schedule, from = Date.now()) {
  const s = SCHEDULES.find(x => x.id === schedule) || SCHEDULES[1];
  return from + s.nextMs;
}

function newWorkflow(siteId, presetId = 'watch') {
  const preset = PRESETS.find(p => p.id === presetId) || PRESETS[0];
  return {
    id: uid(),
    siteId,
    preset: preset.id,
    schedule: preset.defaultSchedule,
    threshold: 5,
    channels: { email: true, slack: false, push: false, webhook: false },
    createdAt: Date.now(),
    lastRun: null,
    nextRun: computeNextRun(preset.defaultSchedule),
    enabled: true,
    runs: [],
  };
}

// ─── main ──────────────────────────────────────────────────────────────────
function Workflows({ profile, sites, activeSite, setActiveSite, onBack, onOpenSettings }) {
  const [wfs, setWfs] = useStateWf(() => loadWorkflows());
  const [tick, setTick] = useStateWf(0);
  const [creating, setCreating] = useStateWf(null); // { siteId, presetId } | null

  useEffectWf(() => saveWorkflows(wfs), [wfs]);

  // Tick every 30s — drives "next run in …" labels and synthetic runs.
  useEffectWf(() => {
    const t = setInterval(() => setTick(x => x + 1), 30_000);
    return () => clearInterval(t);
  }, []);

  // Synthetic runs: if any workflow's nextRun has elapsed, append a run record.
  useEffectWf(() => {
    let mutated = false;
    const updated = wfs.map(w => {
      if (!w.enabled) return w;
      if (Date.now() < w.nextRun) return w;
      mutated = true;
      const run = synthesizeRun(w, sites);
      return {
        ...w,
        lastRun: Date.now(),
        nextRun: computeNextRun(w.schedule),
        runs: [run, ...(w.runs || [])].slice(0, 30),
      };
    });
    if (mutated) setWfs(updated);
  }, [tick, wfs.map(w => w.id).join(','), sites?.map(s => s.id).join(',')]);

  const startCreate = (siteId, presetId) => setCreating({ siteId: siteId || sites?.[0]?.id || '', presetId });
  const commitCreate = ({ siteId, presetId, schedule, threshold, channels }) => {
    const wf = newWorkflow(siteId, presetId);
    wf.schedule = schedule || wf.schedule;
    wf.threshold = threshold ?? wf.threshold;
    wf.channels = channels || wf.channels;
    wf.nextRun = computeNextRun(wf.schedule);
    setWfs([wf, ...wfs]);
    setCreating(null);
  };
  const updateWf = (id, patch) => setWfs(wfs.map(w => w.id === id ? { ...w, ...patch, nextRun: patch.schedule ? computeNextRun(patch.schedule) : w.nextRun } : w));
  const removeWf = (id) => setWfs(wfs.filter(w => w.id !== id));
  const runNow = (id) => {
    const w = wfs.find(x => x.id === id);
    if (!w) return;
    const run = synthesizeRun(w, sites, { manual: true });
    setWfs(wfs.map(x => x.id === id ? {
      ...x,
      lastRun: Date.now(),
      runs: [run, ...(x.runs || [])].slice(0, 30),
    } : x));
  };

  // Combined feed of recent runs across all workflows
  const feed = useMemoWf(() => {
    const items = [];
    wfs.forEach(w => {
      (w.runs || []).forEach(r => items.push({ ...r, wfId: w.id, preset: w.preset, siteId: w.siteId }));
    });
    return items.sort((a, b) => b.at - a.at).slice(0, 12);
  }, [wfs, tick]);

  const hasAnyWorkflow = wfs.length > 0;

  return (
    <section className="container fade-in wf-section">
      <header className="wf-hero">
        <div>
          <div className="eyebrow"><span className="pulse" /> Workflows · scheduled, conditional, on rails</div>
          <h1 className="wf-h1">
            Stop pressing <span className="grad">Scan</span><br/>
            <span className="it">— let the platform watch.</span>
          </h1>
          <p className="wf-sub">
            Cron meets the scan + fix pipeline. Pick a preset, point it at a site, and we do the work
            on your schedule. Watch quietly, draft for review, or ship without you clicking anything.
          </p>
        </div>
        <button className="btn" onClick={onBack} style={{ alignSelf: 'flex-start' }}>← Back</button>
      </header>

      {/* PRESETS */}
      <div className="wf-presets">
        {PRESETS.map((p, i) => (
          <article key={p.id} className={`wf-preset wf-preset-${p.id}`}>
            <div className="wf-preset-edge" />
            <header className="wf-preset-head">
              <span className="wf-preset-num">{String(i + 1).padStart(2, '0')}</span>
              <span className="wf-preset-tag" style={{ color: p.color }}>{p.tag}</span>
            </header>
            <h2 className="wf-preset-name" style={{ color: p.color }}>{p.name}</h2>
            <p className="wf-preset-short">{p.short}</p>
            <ol className="wf-preset-flow">
              {p.actions.map((a, j) => (
                <li key={j}>
                  <span className="wf-flow-dot" style={{ background: p.color, boxShadow: `0 0 10px ${p.color}` }} />
                  <span>{a}</span>
                </li>
              ))}
            </ol>
            <p className="wf-preset-desc">{p.desc}</p>
            <div className="wf-preset-foot">
              <div className="wf-rec">
                <span className="wf-rec-k">good for</span>
                <div className="wf-rec-tags">
                  {p.recommended.map(r => <span key={r} className="wf-rec-tag">{r}</span>)}
                </div>
              </div>
              <button
                className={`btn ${p.id === 'maintain' ? 'btn-primary' : ''}`}
                onClick={() => startCreate(activeSite?.id, p.id)}
              >
                Set up {p.name} →
              </button>
            </div>
          </article>
        ))}
      </div>

      {/* ACTIVE WORKFLOWS */}
      <section className="wf-block">
        <header className="wf-block-head">
          <div>
            <h3 className="wf-h3">Active workflows</h3>
            <p className="wf-h3-sub">{hasAnyWorkflow ? `${wfs.length} workflow${wfs.length === 1 ? '' : 's'} on rails` : 'Nothing scheduled yet'}</p>
          </div>
        </header>

        {!hasAnyWorkflow ? (
          <div className="wf-empty">
            <div className="wf-empty-grid">
              {[...Array(6)].map((_, i) => (
                <div key={i} className="wf-empty-cell" style={{ animationDelay: `${i * 200}ms` }} />
              ))}
            </div>
            <div className="wf-empty-copy">
              <h4>Pick a preset above to get started.</h4>
              <p>Workflows run server-side (Vercel Cron), so this tab can stay closed. You'll get a recap in whichever channel you choose.</p>
            </div>
          </div>
        ) : (
          <div className="wf-list">
            {wfs.map(w => (
              <WorkflowRow
                key={w.id}
                wf={w}
                site={sites.find(s => s.id === w.siteId)}
                tick={tick}
                onUpdate={(patch) => updateWf(w.id, patch)}
                onRemove={() => removeWf(w.id)}
                onRunNow={() => runNow(w.id)}
              />
            ))}
          </div>
        )}
      </section>

      {/* ACTIVITY FEED */}
      {feed.length > 0 && (
        <section className="wf-block">
          <header className="wf-block-head">
            <div>
              <h3 className="wf-h3">Recent runs</h3>
              <p className="wf-h3-sub">What the workflows have been up to.</p>
            </div>
          </header>
          <div className="wf-feed">
            {feed.map((r, i) => (
              <FeedItem key={i} run={r} site={sites.find(s => s.id === r.siteId)} />
            ))}
          </div>
        </section>
      )}

      {/* CREATE MODAL */}
      {creating && (
        <CreateWorkflowModal
          sites={sites || []}
          initial={creating}
          onClose={() => setCreating(null)}
          onCreate={commitCreate}
        />
      )}
    </section>
  );
}

// ─── workflow row ─────────────────────────────────────────────────────────
function WorkflowRow({ wf, site, tick, onUpdate, onRemove, onRunNow }) {
  const preset = PRESETS.find(p => p.id === wf.preset) || PRESETS[0];
  const schedule = SCHEDULES.find(s => s.id === wf.schedule) || SCHEDULES[1];
  const lastRun = wf.runs?.[0];
  const remaining = Math.max(0, wf.nextRun - Date.now());
  const [expanded, setExpanded] = useStateWf(false);

  return (
    <div className={`wf-row ${expanded ? 'expanded' : ''} ${!wf.enabled ? 'paused' : ''}`}>
      <div className="wf-row-main">
        <div className="wf-row-id">
          <div className="wf-preset-dot" style={{ background: preset.color, boxShadow: `0 0 10px ${preset.color}` }} />
          <div>
            <div className="wf-row-site">
              {site?.name || '—'}
              <span className="wf-row-dom">{site?.domain || ''}</span>
            </div>
            <div className="wf-row-preset">
              <span style={{ color: preset.color }}>{preset.name}</span> · {schedule.label} · {schedule.dayLabel}
            </div>
          </div>
        </div>

        <div className="wf-row-status">
          <div className="wf-row-stat">
            <span className="wf-stat-k">Last run</span>
            <span className="wf-stat-v">{lastRun ? relTimeWf(lastRun.at) : 'never'}</span>
          </div>
          <div className="wf-row-stat">
            <span className="wf-stat-k">Next run</span>
            <span className="wf-stat-v">{wf.enabled ? formatRemaining(remaining) : 'paused'}</span>
          </div>
          {lastRun && (
            <div className="wf-row-stat wf-row-result">
              <span className="wf-stat-k">Outcome</span>
              <span className="wf-stat-v">
                <ResultDelta r={lastRun} />
              </span>
            </div>
          )}
        </div>

        <div className="wf-row-actions">
          <button className="wf-toggle" onClick={() => onUpdate({ enabled: !wf.enabled })} title={wf.enabled ? 'Pause' : 'Resume'}>
            <span className={`wf-sw ${wf.enabled ? 'on' : ''}`}><i /></span>
          </button>
          <button className="btn btn-ghost wf-mini" onClick={onRunNow}>Run now</button>
          <button className="btn btn-ghost wf-mini" onClick={() => setExpanded(e => !e)}>{expanded ? 'Hide' : 'Edit'}</button>
        </div>
      </div>

      {/* progress bar to next run */}
      {wf.enabled && (
        <div className="wf-row-bar">
          <i style={{ width: `${100 - Math.min(100, (remaining / schedule.nextMs) * 100)}%` }} />
        </div>
      )}

      {expanded && (
        <div className="wf-row-edit">
          <div className="wf-edit-grid">
            <FieldGroup label="Schedule">
              <div className="wf-pill-row">
                {SCHEDULES.map(s => (
                  <button key={s.id} className={`wf-pill ${wf.schedule === s.id ? 'on' : ''}`}
                    onClick={() => onUpdate({ schedule: s.id })}>{s.label}</button>
                ))}
              </div>
            </FieldGroup>
            <FieldGroup label="Trigger threshold">
              <div className="wf-threshold-wrap">
                <input
                  type="range" min="1" max="20" step="1" value={wf.threshold}
                  className="wf-range"
                  onChange={(e) => onUpdate({ threshold: Number(e.target.value) })}
                />
                <div className="wf-threshold-readout">
                  Notify when score moves <strong>±{wf.threshold} pts</strong> or a competitor passes you.
                </div>
              </div>
            </FieldGroup>
            <FieldGroup label="Channels">
              <div className="wf-chan-row">
                {CHANNELS.map(c => (
                  <button key={c.id}
                    className={`wf-chan ${wf.channels?.[c.id] ? 'on' : ''}`}
                    onClick={() => onUpdate({ channels: { ...wf.channels, [c.id]: !wf.channels?.[c.id] } })}>
                    <span className="wf-chan-icon">{c.icon}</span>
                    <span>
                      <span className="wf-chan-label">{c.label}</span>
                      <span className="wf-chan-sub">{c.sub}</span>
                    </span>
                  </button>
                ))}
              </div>
            </FieldGroup>
          </div>
          <div className="wf-edit-foot">
            <button className="btn btn-ghost danger" onClick={onRemove}>Delete workflow</button>
            <span className="wf-edit-id">id: {wf.id} · created {fmtShort(wf.createdAt)}</span>
          </div>
        </div>
      )}
    </div>
  );
}

function ResultDelta({ r }) {
  if (r.error) return <span style={{ color: 'var(--bad)' }}>errored</span>;
  if (!r.outcome) return <span style={{ color: 'var(--fg-3)' }}>—</span>;
  if (r.outcome === 'shipped') return <span style={{ color: 'var(--good)' }}>+{r.delta || 0} · shipped {r.shipped || 0} PR</span>;
  if (r.outcome === 'drafted') return <span style={{ color: 'var(--accent)' }}>drafted {r.drafted || 0} fix</span>;
  if (r.outcome === 'no-change') return <span style={{ color: 'var(--fg-2)' }}>no change</span>;
  if (r.outcome === 'alert') return <span style={{ color: 'oklch(82% 0.14 78)' }}>{r.delta > 0 ? `+${r.delta}` : r.delta} · alerted</span>;
  return <span>{r.outcome}</span>;
}

function FieldGroup({ label, children }) {
  return (
    <div className="wf-field">
      <div className="wf-field-label">{label}</div>
      {children}
    </div>
  );
}

// ─── activity feed item ─────────────────────────────────────────────────
function FeedItem({ run, site }) {
  const preset = PRESETS.find(p => p.id === run.preset) || PRESETS[0];
  const verb = {
    'shipped': 'shipped a PR for',
    'drafted': 'drafted fixes for',
    'no-change': 'measured',
    'alert': 'alerted on',
    'errored': 'failed to scan',
  }[run.outcome] || 'ran';
  return (
    <div className={`wf-feed-item wf-feed-${run.outcome}`}>
      <div className="wf-feed-dot" style={{ background: preset.color }} />
      <div className="wf-feed-body">
        <div className="wf-feed-line">
          <strong style={{ color: preset.color }}>{preset.name}</strong> {verb} <strong>{site?.name || 'unknown'}</strong>
          {run.delta != null && (
            <span className={`wf-feed-delta ${run.delta > 0 ? 'up' : run.delta < 0 ? 'down' : ''}`}>
              {run.delta > 0 ? '+' : ''}{run.delta}
            </span>
          )}
          {run.manual && <span className="wf-feed-mark">manual</span>}
        </div>
        <div className="wf-feed-sub">{run.summary}</div>
      </div>
      <div className="wf-feed-time">{relTimeWf(run.at)}</div>
    </div>
  );
}

// ─── synthesized runs ────────────────────────────────────────────────────
// Until Vercel Cron is wired, the UI demonstrates the workflow concept by
// generating plausible run records. Each preset produces a different outcome
// distribution; numbers come from the workflow's current state.
function synthesizeRun(wf, sites, opts = {}) {
  const preset = PRESETS.find(p => p.id === wf.preset) || PRESETS[0];
  const site = sites?.find(s => s.id === wf.siteId);
  const dice = Math.random();
  const delta = Math.round((Math.random() - 0.45) * (wf.threshold * 1.6));
  const exceededThreshold = Math.abs(delta) >= wf.threshold;

  let outcome;
  if (preset.id === 'watch') outcome = exceededThreshold ? 'alert' : 'no-change';
  else if (preset.id === 'maintain') outcome = exceededThreshold ? 'drafted' : (dice > 0.5 ? 'drafted' : 'no-change');
  else outcome = dice > 0.3 ? 'shipped' : (dice > 0.1 ? 'drafted' : 'no-change');

  const drafted = outcome === 'drafted' || outcome === 'shipped' ? 1 + Math.floor(Math.random() * 3) : 0;
  const shipped = outcome === 'shipped' ? 1 + Math.floor(Math.random() * 2) : 0;

  const summary = (() => {
    const sName = site?.name || 'site';
    if (outcome === 'shipped') return `Auto-deployed ${shipped} fix${shipped === 1 ? '' : 'es'} · re-scanned · ${delta > 0 ? `+${delta}` : delta} pts.`;
    if (outcome === 'drafted') return `Drafted ${drafted} fix${drafted === 1 ? '' : 'es'} for ${sName} · awaiting review.`;
    if (outcome === 'alert') return `Score moved ${delta > 0 ? '+' : ''}${delta} pts on ${sName} · ${preset.id === 'watch' ? 'notified you' : 'queued diagnosis'}.`;
    if (outcome === 'no-change') return `${sName} steady. No fixes auto-deployable this cycle.`;
    return `Workflow completed.`;
  })();

  return {
    at: Date.now(),
    outcome,
    delta,
    drafted,
    shipped,
    summary,
    manual: !!opts.manual,
  };
}

// ─── create modal ─────────────────────────────────────────────────────────
function CreateWorkflowModal({ sites, initial, onClose, onCreate }) {
  const presetId = initial.presetId || 'watch';
  const [siteId, setSiteId] = useStateWf(initial.siteId || sites[0]?.id || '');
  const [schedule, setSchedule] = useStateWf(PRESETS.find(p => p.id === presetId)?.defaultSchedule || 'weekly-mon');
  const [threshold, setThreshold] = useStateWf(5);
  const [channels, setChannels] = useStateWf({ email: true, slack: false, push: false, webhook: false });

  const preset = PRESETS.find(p => p.id === presetId) || PRESETS[0];
  const canCreate = !!siteId;

  return (
    <div className="onboarding-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="onboarding wf-modal">
        <header className="ob-head">
          <div className="ob-eyebrow"><span className="pulse" style={{ background: preset.color }} /> Set up workflow</div>
          <h2 style={{ color: preset.color }}>{preset.name}</h2>
          <p className="wf-modal-desc">{preset.desc}</p>
        </header>

        <div className="ob-body">
          <div className="wf-field">
            <div className="wf-field-label">Site</div>
            <select className="wf-select" value={siteId} onChange={(e) => setSiteId(e.target.value)}>
              <option value="">— pick a site —</option>
              {sites.map(s => (
                <option key={s.id} value={s.id}>{s.name} · {s.domain}</option>
              ))}
            </select>
          </div>

          <div className="wf-field">
            <div className="wf-field-label">Schedule</div>
            <div className="wf-pill-row">
              {SCHEDULES.map(s => (
                <button key={s.id} type="button" className={`wf-pill ${schedule === s.id ? 'on' : ''}`}
                  onClick={() => setSchedule(s.id)}>{s.label}</button>
              ))}
            </div>
          </div>

          <div className="wf-field">
            <div className="wf-field-label">Trigger threshold</div>
            <div className="wf-threshold-wrap">
              <input type="range" min="1" max="20" step="1" value={threshold}
                className="wf-range" onChange={(e) => setThreshold(Number(e.target.value))} />
              <div className="wf-threshold-readout">
                {preset.deploys
                  ? <>Ship fixes if score drops <strong>more than {threshold} pts</strong>, or weekly anyway.</>
                  : <>Alert me when score moves <strong>±{threshold} pts</strong> or a competitor passes you.</>}
              </div>
            </div>
          </div>

          <div className="wf-field">
            <div className="wf-field-label">Notify via</div>
            <div className="wf-chan-row">
              {CHANNELS.map(c => (
                <button key={c.id} type="button"
                  className={`wf-chan ${channels[c.id] ? 'on' : ''}`}
                  onClick={() => setChannels(s => ({ ...s, [c.id]: !s[c.id] }))}>
                  <span className="wf-chan-icon">{c.icon}</span>
                  <span>
                    <span className="wf-chan-label">{c.label}</span>
                    <span className="wf-chan-sub">{c.sub}</span>
                  </span>
                </button>
              ))}
            </div>
          </div>

          <div className="wf-modal-preview">
            <div className="wf-mp-eyebrow">Summary</div>
            <div className="wf-mp-body">
              <strong style={{ color: preset.color }}>{preset.name}</strong> on{' '}
              <strong>{sites.find(s => s.id === siteId)?.name || '—'}</strong>,{' '}
              <strong>{(SCHEDULES.find(s => s.id === schedule) || {}).label?.toLowerCase()}</strong>.{' '}
              {preset.deploys
                ? <>Ships auto-deployable fixes; alerts on ±{threshold} pts swings via {channelNames(channels) || 'no channel — fix that'}.</>
                : <>Notifies on ±{threshold} pts swings via {channelNames(channels) || 'no channel — pick at least one'}.</>}
            </div>
          </div>
        </div>

        <div className="ob-foot">
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
          <button
            className="btn btn-primary"
            disabled={!canCreate}
            onClick={() => onCreate({ siteId, presetId, schedule, threshold, channels })}
          >
            Activate {preset.name} →
          </button>
        </div>
      </div>
    </div>
  );
}

function channelNames(channels) {
  const ks = Object.keys(channels).filter(k => channels[k]);
  if (ks.length === 0) return '';
  if (ks.length === 1) return CHANNELS.find(c => c.id === ks[0])?.label || ks[0];
  return ks.map(k => CHANNELS.find(c => c.id === k)?.label || k).join(' + ');
}

// ─── helpers ─────────────────────────────────────────────────────────────
function relTimeWf(ts) {
  if (!ts) return 'never';
  const diff = Date.now() - ts;
  if (diff < 60_000) return 'just now';
  if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
  if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
  return `${Math.floor(diff / 86_400_000)}d ago`;
}
function formatRemaining(ms) {
  if (ms < 60_000) return 'any second';
  if (ms < 3_600_000) return `in ${Math.floor(ms / 60_000)}m`;
  if (ms < 86_400_000) return `in ${Math.floor(ms / 3_600_000)}h`;
  return `in ${Math.floor(ms / 86_400_000)}d`;
}
function fmtShort(ts) {
  return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

Object.assign(window, { Workflows });
