/* Mise Dashboard — single file (kept under 1000 lines for readability) */

/* ——— Icons ——— */
const Ic = ({ name, size = 16, color = 'currentColor' }) => {
  const s = { width: size, height: size, color, display: 'block' };
  const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  switch (name) {
    case 'dashboard': return <svg viewBox="0 0 16 16" style={s}><g {...p}><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></g></svg>;
    case 'stores':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 6h12l-1-3H3z"/><path d="M3 6v8h10V6"/><path d="M6 14V9h4v5"/></g></svg>;
    case 'alerts':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 12V7a5 5 0 0 1 10 0v5l1 2H2z"/><path d="M6 14a2 2 0 0 0 4 0"/></g></svg>;
    case 'ask':       return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H8l-3 2v-2H4a2 2 0 0 1-2-2z"/><circle cx="8" cy="7" r=".7" fill="currentColor"/><path d="M6 6.2a2 2 0 1 1 2.5 1.7"/></g></svg>;
    case 'docs':      return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 2h7l3 3v9H3z"/><path d="M10 2v3h3"/><path d="M5 8h6M5 11h4"/></g></svg>;
    case 'reports':   return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 14h12"/><rect x="4" y="6" width="2" height="6"/><rect x="7" y="3" width="2" height="9"/><rect x="10" y="8" width="2" height="4"/></g></svg>;
    case 'settings':  return <svg viewBox="0 0 16 16" style={s}><g {...p}><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></g></svg>;
    case 'chev':      return <svg viewBox="0 0 16 16" style={s}><path d="M5 6l3 3 3-3" {...p}/></svg>;
    case 'chevR':     return <svg viewBox="0 0 16 16" style={s}><path d="M6 4l3 4-3 4" {...p}/></svg>;
    case 'search':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></g></svg>;
    case 'bell':      return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 12V7a5 5 0 0 1 10 0v5l1 2H2z"/><path d="M6 14a2 2 0 0 0 4 0"/></g></svg>;
    case 'cal':       return <svg viewBox="0 0 16 16" style={s}><g {...p}><rect x="2" y="3" width="12" height="11" rx="1"/><path d="M2 6h12M5 2v3M11 2v3"/></g></svg>;
    default: return null;
  }
};

/* ——— Sidebar ——— */
const ORGS = window.MISE.orgs;

function Sidebar() {
  const UserTile = window.MISE.SidebarUserTile;
  const [openAlertsCount, setOpenAlertsCount] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    const refresh = () => window.MISE.fetchAlertsCached().then(d => {
      if (alive) setOpenAlertsCount(d.filter(a => a.status === "open").length);
    });
    refresh();
    const unsub = window.MISE.subscribeAlerts(refresh);
    return () => { alive = false; unsub && unsub(); };
  }, []);
  const [open, setOpen] = React.useState(false);
  const [orgId, setOrgId] = React.useState('acme-wingstop');
  const org = ORGS.find(o => o.id === orgId) || ORGS[0];
  const items = [
    { id: 'dash', icon: 'dashboard', label: 'Dashboard', href: '/dashboard', active: true },
    { id: 'stores', icon: 'stores', label: 'Stores', href: '/stores' },
    { id: 'alerts', icon: 'alerts', label: 'Alerts', href: '/alerts', badge: openAlertsCount },
    { id: 'ask', icon: 'ask', label: 'Ask Mise', href: '/ask' },
    { id: 'docs', icon: 'docs', label: 'Documents', href: '/documents' },
    { id: 'reports', icon: 'reports', label: 'Reports', href: '/reports' },
    { id: 'settings', icon: 'settings', label: 'Settings', href: '/settings' },
  ];
  const renderItem = (it) => (
    <a key={it.id} href={it.href || '#'} className={`nav-item ${it.active ? 'active' : ''}`}>
      <Ic name={it.icon} />
      <span>{it.label}</span>
      {it.badge && <span className="badge">{it.badge}</span>}
    </a>
  );
  return (
    <aside className="sidebar" data-screen-label="Sidebar">
      <a href="/" className="sb-brand">
        <span className="glyph">
          <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
            <path d="M3 11 V3 L6 8 L8 3 V11" fill="none" stroke="#0F2A3F" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
            <circle cx="11" cy="9" r="1.2" fill="#C8553D"/>
          </svg>
        </span>
        <span className="word">Mise</span>
      </a>

      <div className="org-switch" style={{ cursor: 'default' }}>
        <div style={{ minWidth: 0 }}>
          <div className="label">Organization</div>
          <div className="name">{org.name}</div>
        </div>
      </div>

      <nav className="nav-list">
        {items.slice(0, 4).map(renderItem)}
        <div className="nav-section">Workspace</div>
        {items.slice(4).map(renderItem)}
      </nav>

      <UserTile/>
    </aside>
  );
}

/* ——— Topbar ——— */
const DATE_RANGES = [
  { id: 'today', title: 'Today', meta: 'Tue, Apr 28' },
  { id: 'yest', title: 'Yesterday', meta: 'Mon, Apr 27' },
  { id: '7d', title: 'Last 7 days', meta: 'Apr 22 – Apr 28' },
  { id: '30d', title: 'Last 30 days', meta: 'Mar 30 – Apr 28' },
  { id: 'mtd', title: 'Month to date', meta: 'Apr 1 – Apr 28' },
  { id: 'qtd', title: 'Quarter to date', meta: 'Apr 1 – Apr 28' },
  { id: 'custom', title: 'Custom range…', meta: '' },
];

function Topbar() {
  const [open, setOpen] = React.useState(false);
  const [rangeId, setRangeId] = React.useState('today');
  const range = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[0];
  return (
    <header className="topbar" data-screen-label="Top bar">
      <div>
        <span className="crumb serif">Dashboard</span>
        <span className="crumb"><span className="sub">— 11 stores · CA &amp; NV</span></span>
      </div>
      <div className="search">
        <span className="sicon"><Ic name="search" size={14}/></span>
        <input type="text" placeholder="Ask Mise anything…" />
        <span className="kbd">⌘K</span>
      </div>
      <div className="top-right">
        <button className="icon-btn" aria-label="Notifications">
          <Ic name="bell" size={16}/>
          <span className="dot"/>
        </button>
        <div className="dd-wrap">
          {open && <div className="dd-backdrop" onClick={() => setOpen(false)}/>}
          <button className="date-chip" onClick={() => setOpen(o => !o)}>
            <Ic name="cal" size={14} color="var(--muted)"/>
            <span className="lbl">{range.id === 'today' ? 'Today' : 'Range'}</span>
            <span>{range.title === 'Today' ? range.meta : range.title}</span>
            <span className="chev" style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s', display: 'inline-flex' }}><Ic name="chev" size={12}/></span>
          </button>
          {open && (
            <div className="dd-menu right" style={{ minWidth: 240 }}>
              <div className="dd-head">Date range</div>
              {DATE_RANGES.map(r => (
                <div key={r.id} className={`dd-item ${r.id === rangeId ? 'active' : ''}`} onClick={() => { setRangeId(r.id); setOpen(false); }}>
                  <div className="dd-text">
                    <div className="dd-title">{r.title}</div>
                    {r.meta && <div className="dd-meta">{r.meta}</div>}
                  </div>
                  {r.id === rangeId && <span className="dd-check">✓</span>}
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </header>
  );
}

/* ——— Brief banner ——— */
/* Render bold (**...**) inside an otherwise plain markdown line as
 *  React children. Keeps the renderer from pulling in a markdown lib. */
function renderInline(line) {
  const parts = [];
  let i = 0;
  let key = 0;
  const re = /\*\*([^*]+)\*\*/g;
  let m;
  while ((m = re.exec(line)) !== null) {
    if (m.index > i) parts.push(line.slice(i, m.index));
    parts.push(<strong key={`b${key++}`}>{m[1]}</strong>);
    i = m.index + m[0].length;
  }
  if (i < line.length) parts.push(line.slice(i));
  return parts;
}

/* Parse the brief body produced by DailyBriefService into the bucket
 *  shape the dashboard renders. The composer's prompt locks the
 *  structure to `## Lead` / `## Watch` / `## Rollup` with bullets, so
 *  a tiny parser is cheaper than pulling in react-markdown. */
function parseBriefBody(md) {
  const out = { lead: [], watch: [], rollup: '' };
  if (!md) return out;
  const lines = md.split(/\r?\n/);
  let bucket = null;
  let rollupParts = [];
  for (const raw of lines) {
    const line = raw.trim();
    if (!line) continue;
    const heading = /^##\s+(.*)$/.exec(line);
    if (heading) {
      const h = heading[1].toLowerCase();
      if (h.startsWith('lead'))        bucket = 'lead';
      else if (h.startsWith('watch'))  bucket = 'watch';
      else if (h.startsWith('rollup')) bucket = 'rollup';
      else                             bucket = null;
      continue;
    }
    const bullet = /^[-*]\s+(.*)$/.exec(line);
    if (bullet) {
      const text = bullet[1].trim();
      // The composer writes "- None" for an empty bucket. Don't
      // render that as a real bullet — it'd produce an awkward
      // amber/red row that says "None".
      const isPlaceholder = /^(none|nothing|n\/a|no items?|no critical items? overnight\.?)$/i.test(text);
      if (isPlaceholder) continue;
      if (bucket === 'lead' || bucket === 'watch') {
        out[bucket].push(text);
      } else if (bucket === 'rollup') {
        rollupParts.push(text);
      }
      continue;
    }
    if (bucket === 'rollup') rollupParts.push(line);
  }
  out.rollup = rollupParts.join(' ').trim();
  return out;
}

function Brief() {
  const [state, setState] = React.useState('loading'); // 'loading' | 'empty' | 'ok' | 'error'
  const [brief, setBrief] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [running, setRunning] = React.useState(false);

  const refresh = React.useCallback(() => {
    setState('loading');
    fetch(`${DASH_API_BASE}/daily-brief/latest`, { cache: 'no-store' })
      .then(r => {
        if (r.status === 204) { setBrief(null); setState('empty'); return null; }
        if (!r.ok) return Promise.reject(new Error(`${r.status}`));
        return r.json();
      })
      .then(d => { if (d) { setBrief(d); setState('ok'); } })
      .catch(e => { setError(e.message || String(e)); setState('error'); });
  }, []);

  React.useEffect(refresh, [refresh]);

  async function generate() {
    setRunning(true);
    setError(null);
    try {
      const r = await fetch(`${DASH_API_BASE}/daily-brief/run-now`, { method: 'POST' });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setBrief(d);
      setState('ok');
    } catch (e) {
      setError(e.message || String(e));
      setState('error');
    } finally {
      setRunning(false);
    }
  }

  const formatDate = (s) => {
    if (!s) return '';
    try {
      const d = new Date(s);
      return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
    } catch { return s; }
  };
  const formatTime = (s) => {
    if (!s) return '';
    try {
      const d = new Date(s);
      return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
    } catch { return ''; }
  };

  const headTitle = brief
    ? formatDate(brief.generatedForDate)
    : (state === 'loading' ? 'Loading…' : 'No brief yet');

  return (
    <div className="brief" data-screen-label="01 Daily brief">
      <div className="brief-head">
        <h2>Your morning brief — <em>{headTitle}</em></h2>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          {brief && (
            <span className="brief-meta">
              GENERATED {formatTime(brief.generatedAt) || '—'} · {String(brief.trigger || 'auto').toUpperCase()}
            </span>
          )}
          <a
            href="/settings?section=brief"
            className="link-arrow"
            style={{ cursor: 'pointer' }}
          >Schedule →</a>
        </div>
      </div>

      {state === 'loading' && (
        <div style={{ fontSize: 13, color: 'var(--muted)' }}>Loading latest brief…</div>
      )}

      {state === 'error' && (
        <div style={{
          fontSize: 13, color: 'var(--red)',
          padding: '10px 14px', borderRadius: 8,
          background: 'rgba(178,58,42,.06)', border: '1px solid rgba(178,58,42,.2)',
        }}>
          Couldn't load the brief: {error}.{' '}
          <button onClick={refresh} style={{
            background: 'transparent', border: 0, color: 'var(--accent)',
            cursor: 'pointer', textDecoration: 'underline', padding: 0, fontSize: 13,
          }}>Retry</button>
        </div>
      )}

      {state === 'empty' && (
        <div style={{
          padding: '14px 18px', borderRadius: 10,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          color: 'var(--muted)', fontSize: 13.5,
        }}>
          No brief generated yet today. The next scheduled run will fire at the configured time —{' '}
          <a href="/settings?section=brief" style={{ color: 'var(--accent)' }}>change the schedule</a>{' '}
          or{' '}
          <button onClick={generate} disabled={running} style={{
            background: 'transparent', border: 0, color: 'var(--accent)',
            cursor: running ? 'wait' : 'pointer', textDecoration: 'underline',
            padding: 0, fontSize: 13.5,
          }}>{running ? 'composing…' : 'compose one now'}</button>.
        </div>
      )}

      {state === 'ok' && brief && (() => {
        const parsed = parseBriefBody(brief.bodyMd);
        const rows = [];
        for (const item of parsed.lead) {
          rows.push({ sev: 'red', text: item });
        }
        for (const item of parsed.watch) {
          rows.push({ sev: 'amber', text: item });
        }
        if (parsed.rollup) {
          rows.push({ sev: 'green', text: parsed.rollup });
        }
        if (rows.length === 0) {
          // The LLM produced something we can't parse — fall back to
          // raw markdown rendered as preformatted text. Better than
          // an empty card.
          return (
            <pre style={{
              whiteSpace: 'pre-wrap', wordBreak: 'break-word',
              fontFamily: 'inherit', fontSize: 14, lineHeight: 1.6,
              margin: 0, color: 'var(--ink)',
            }}>{brief.bodyMd}</pre>
          );
        }
        return (
          <div className="brief-list">
            {rows.map((r, i) => (
              <div key={i} className="brief-row">
                <span className={`sev-dot ${r.sev}`}/>
                <div className="text">{renderInline(r.text)}</div>
              </div>
            ))}
          </div>
        );
      })()}
    </div>
  );
}

/* ——— Sparkline (interactive) ——— */
const DAY_LABELS = ['Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue'];

function Spark({ data, color = 'var(--primary)', baseline, format = (v) => v, daysBack = 6, fillEnabled = true, mode = 'line', onHover, hoverIdx }) {
  const W = 200, H = 36, padX = 4, padY = 4;
  const wrapRef = React.useRef(null);
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const xAt = (i) => padX + (i / (data.length - 1)) * (W - padX * 2);
  const yAt = (v) => H - padY - ((v - min) / range) * (H - padY * 2);
  const pts = data.map((v, i) => `${xAt(i)},${yAt(v)}`).join(' ');
  const areaD = `M ${xAt(0)} ${H} L ${data.map((v, i) => `${xAt(i)} ${yAt(v)}`).join(' L ')} L ${xAt(data.length - 1)} ${H} Z`;
  const isBars = mode === 'bars';
  const barW = ((W - padX * 2) / data.length) * 0.62;

  function handleMove(e) {
    const rect = wrapRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const ratio = Math.max(0, Math.min(1, x / rect.width));
    const idx = Math.round(ratio * (data.length - 1));
    onHover && onHover(idx);
  }
  function handleLeave() { onHover && onHover(null); }

  const i = hoverIdx;
  const tipVisible = i != null && i >= 0 && i < data.length;
  const tipX = tipVisible ? (xAt(i) / W) * 100 : 0;
  const tipDayLabel = tipVisible ? DAY_LABELS[(DAY_LABELS.length - data.length + i + DAY_LABELS.length) % DAY_LABELS.length] : '';

  return (
    <div className="kpi-spark-wrap" ref={wrapRef} onMouseMove={handleMove} onMouseLeave={handleLeave}>
      <svg className="kpi-spark" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
        {[0, 1, 2].map(k => (
          <line key={k} x1={padX} x2={W - padX} y1={padY + k * ((H - padY * 2) / 2)} y2={padY + k * ((H - padY * 2) / 2)} stroke="var(--hair-2)" strokeDasharray="1 3" strokeWidth=".7"/>
        ))}
        {baseline != null && <line x1={padX} x2={W - padX} y1={yAt(baseline)} y2={yAt(baseline)} stroke="var(--muted-2)" strokeDasharray="3 3" strokeWidth=".8"/>}
        {!isBars && fillEnabled && <path d={areaD} fill={color} opacity=".08"/>}
        {!isBars && <polyline points={pts} fill="none" stroke={color} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round"/>}
        {isBars && data.map((v, idx) => {
          const cx = xAt(idx), top = yAt(v), bh = H - padY - top;
          return <rect key={idx} x={cx - barW / 2} y={top} width={barW} height={Math.max(2, bh)} rx="1.2" fill={color} opacity={tipVisible && i === idx ? 1 : .85}/>;
        })}
        {!isBars && data.map((v, idx) => (
          <circle key={idx} cx={xAt(idx)} cy={yAt(v)} r={tipVisible && i === idx ? 3 : (idx === data.length - 1 ? 2.2 : 0)} fill={color} stroke="var(--bg)" strokeWidth={tipVisible && i === idx ? 1.5 : 0}/>
        ))}
        {tipVisible && (
          <line x1={xAt(i)} x2={xAt(i)} y1={padY} y2={H - padY} stroke={color} strokeWidth=".8" strokeDasharray="2 2" opacity=".55"/>
        )}
      </svg>
      {tipVisible && (
        <div className="kpi-tip" style={{ left: `${tipX}%`, top: 0 }}>
          <span className="v">{format(data[i])}</span>
          <span className="d">{tipDayLabel}</span>
        </div>
      )}
    </div>
  );
}

/* ——— KPI strip ——— */
const useTweaks = (d) => [d, () => {}];
const TweaksPanel = () => null;
const TweakSection = () => null;
const TweakRadio = () => null;
const TweakToggle = () => null;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "sparkMode": "line",
  "sparkFill": true,
  "showBaselines": true
}/*EDITMODE-END*/;

function KPIs() {
  const [tw] = useTweaks(TWEAK_DEFAULTS);
  // Live aggregates pulled from /stores/list (sales/labor/food-safety
  // per store) + /alerts/scan (open alert count org-wide). The 4 KPIs
  // were previously hardcoded numbers + sparklines; now everything you
  // see updates with the data.
  const [storeRows, setStoreRows] = React.useState([]);
  const [alertCount, setAlertCount] = React.useState(null);
  const [loaded, setLoaded] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    const refresh = () => Promise.all([
      fetch(`${DASH_API_BASE}/stores/list`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : [])
        .catch(() => []),
      fetch(`${DASH_API_BASE}/alerts/scan`, { method: 'POST', cache: 'no-store' })
        .then(r => r.ok ? r.json() : [])
        .catch(() => []),
    ]).then(([rows, alerts]) => {
      if (cancelled) return;
      setStoreRows(Array.isArray(rows) ? rows : []);
      const open = (Array.isArray(alerts) ? alerts : []).filter(a => a.status === 'open');
      setAlertCount(open.length);
      setLoaded(true);
    });
    refresh();
    const unsub = window.MISE && window.MISE.subscribeAlerts
      ? window.MISE.subscribeAlerts(refresh) : null;
    return () => { cancelled = true; unsub && unsub(); };
  }, []);

  const kpis = React.useMemo(() => {
    // Sales · this week (sum across stores, with 7-day spark from each
    // store's sparkDaily aligned by index).
    const thisWeek = storeRows.reduce((s, r) => s + (r.sales?.thisWeekDollars || 0), 0);
    const priorWeek = storeRows.reduce((s, r) => s + (r.sales?.priorWeekDollars || 0), 0);
    const salesSpark = Array(7).fill(0);
    for (const r of storeRows) {
      const spark = (r.sales && r.sales.sparkDaily) || [];
      for (let i = 0; i < Math.min(7, spark.length); i++) salesSpark[i] += spark[i] || 0;
    }
    const salesDelta = priorWeek > 0 ? ((thisWeek - priorWeek) * 100 / priorWeek) : 0;

    // Avg labor variance % (only stores with labor data — empty stores
    // would otherwise drag the average to 0 and lie about performance).
    const laborStores = storeRows.filter(r => r.hasLaborData && r.labor);
    const avgVar = laborStores.length === 0 ? 0
      : laborStores.reduce((s, r) => s + (r.labor.thisWeekVarianceAbsPct || 0), 0) / laborStores.length;
    const avgPriorVar = laborStores.length === 0 ? 0
      : laborStores.reduce((s, r) => s + (r.labor.priorWeekVarianceAbsPct || 0), 0) / laborStores.length;

    // Food-safety compliant — count of stores whose recorded compliance
    // is ≥95%. Stores with no value recorded count as non-compliant
    // (encourages the operator to fill them in rather than letting
    // blanks inflate the number).
    const totalStores = storeRows.length;
    const compliant = storeRows.filter(r => (r.foodSafetyCompliancePct || 0) >= 95).length;
    const avgCompliance = totalStores === 0 ? 0
      : storeRows.reduce((s, r) => s + (r.foodSafetyCompliancePct || 0), 0) / totalStores;

    // Sparks for non-time-series KPIs are flat lines at the current
    // value — honest about lack of historical data without dropping the
    // visual.
    const flat = (v) => Array(7).fill(v);

    return [
      {
        lbl: 'Total sales · this wk',
        val: thisWeek > 0 ? `$${Math.round(thisWeek).toLocaleString()}` : '—',
        trend: priorWeek > 0
          ? { dir: salesDelta >= 0 ? 'up' : 'down',
              txt: `${salesDelta >= 0 ? '+' : ''}${salesDelta.toFixed(1)}%`,
              tone: salesDelta >= 0 ? 'green' : 'red' }
          : { dir: 'up', txt: '—', tone: 'gray' },
        cmp: 'vs prior 7 days',
        spark: salesSpark.length === 7 && salesSpark.some(v => v > 0) ? salesSpark : flat(thisWeek / 7),
        sparkColor: 'var(--primary)',
        fmt: v => `$${Math.round(v).toLocaleString()}`,
      },
      {
        lbl: 'Avg labor variance · this wk',
        val: laborStores.length > 0 ? avgVar.toFixed(1) : '—',
        sub: laborStores.length > 0 ? '%' : null,
        trend: laborStores.length > 0
          ? { dir: avgVar - avgPriorVar <= 0 ? 'down' : 'up',
              txt: `${avgVar - avgPriorVar >= 0 ? '+' : ''}${(avgVar - avgPriorVar).toFixed(1)}pt`,
              tone: avgVar - avgPriorVar <= 0 ? 'green' : 'red' }
          : { dir: 'up', txt: '—', tone: 'gray' },
        cmp: 'vs prior 7 days',
        spark: flat(avgVar),
        sparkColor: 'var(--primary)',
        baseline: 0,
        fmt: v => `${v.toFixed(1)}%`,
      },
      {
        lbl: 'Stores compliant · food safety',
        val: totalStores > 0 ? String(compliant) : '—',
        sub: totalStores > 0 ? `of ${totalStores}` : null,
        trend: { dir: compliant === totalStores ? 'up' : 'down',
                 txt: totalStores > 0 ? `${avgCompliance.toFixed(1)}% avg` : '—',
                 tone: compliant === totalStores ? 'green' : (compliant / Math.max(1, totalStores) >= 0.7 ? 'amber' : 'red') },
        cmp: '≥95% threshold',
        spark: flat(compliant),
        sparkColor: 'var(--red)',
        fmt: v => `${v} / ${totalStores}`,
      },
      {
        lbl: 'Open alerts',
        val: alertCount == null ? '—' : String(alertCount),
        trend: { dir: 'up',
                 txt: alertCount == null ? '—' : (alertCount === 0 ? 'all clear' : `${alertCount} action${alertCount === 1 ? '' : 's'}`),
                 tone: alertCount === 0 ? 'green' : (alertCount > 5 ? 'red' : 'amber') },
        cmp: 'across all stores',
        spark: flat(alertCount || 0),
        sparkColor: 'var(--amber)',
        fmt: v => `${v} open`,
      },
    ];
  }, [storeRows, alertCount]);

  const [hovered, setHovered] = React.useState({});
  const setHover = (i, idx) => setHovered(h => ({ ...h, [i]: idx }));
  return (
    <div className="kpis">
      {kpis.map((k, i) => (
        <div key={i} className={`card kpi ${hovered[i] != null ? 'hovering' : ''}`}>
          <div className="lbl">{k.lbl}</div>
          <div className="val">{!loaded ? '…' : k.val}{k.sub && <span className="sub"> {k.sub}</span>}</div>
          <div className="row" style={{ marginTop: 10 }}>
            <span className={`trend ${k.trend.tone}`}>
              {k.trend.dir === 'up' ? '↑' : '↓'} {k.trend.txt}
              <span style={{ opacity: .7, marginLeft: 4, fontSize: 10 }}>{k.cmp}</span>
            </span>
          </div>
          <Spark
            data={k.spark}
            color={k.sparkColor}
            baseline={tw.showBaselines ? k.baseline : null}
            format={k.fmt}
            mode={tw.sparkMode}
            fillEnabled={tw.sparkFill}
            hoverIdx={hovered[i]}
            onHover={(idx) => setHover(i, idx)}
          />
        </div>
      ))}
    </div>
  );
}

function Tweaks() {
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection title="Sparklines">
        <TweakRadio label="Style" value={tw.sparkMode} options={[{value:'line',label:'Line'},{value:'bars',label:'Bars'}]} onChange={v => setTweak('sparkMode', v)}/>
        <TweakToggle label="Area fill (line mode)" value={tw.sparkFill} onChange={v => setTweak('sparkFill', v)}/>
        <TweakToggle label="Show baselines" value={tw.showBaselines} onChange={v => setTweak('showBaselines', v)}/>
      </TweakSection>
    </TweaksPanel>
  );
}

/* ——— Active alerts ——— */
const DASH_API_BASE = (typeof window !== 'undefined' && window.MISE_API_BASE) || 'http://localhost:8080';
const SEV_RANK = { red: 0, amber: 1, green: 2, gray: 3 };

function formatRelativeDash(date) {
  const ms = Date.now() - date.getTime();
  if (ms < 60_000) return 'just now';
  const m = Math.floor(ms / 60_000);
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 48) return `${h}h ago`;
  const d = Math.floor(h / 24);
  return `${d}d ago`;
}

function ActiveAlerts() {
  const [alerts, setAlerts] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [scanStatus, setScanStatus] = React.useState(null);

  const refreshScanStatus = React.useCallback(() => {
    return fetch(`${DASH_API_BASE}/alerts/scan/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setScanStatus)
      .catch(() => setScanStatus(null));
  }, []);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(`${DASH_API_BASE}/alerts/scan`, { method: 'POST', cache: 'no-store' });
        if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
        const data = await res.json();
        if (cancelled) return;
        setAlerts(data);
        refreshScanStatus();
      } catch (e) {
        if (!cancelled) setError(e.message || String(e));
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [refreshScanStatus]);

  const ack = async (id) => {
    const previous = alerts;
    setAlerts(curr => curr.map(a => a.id === id ? { ...a, status: 'acked' } : a));
    try {
      const res = await fetch(`${DASH_API_BASE}/alerts/${encodeURIComponent(id)}/status`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status: 'acked' }),
      });
      if (!res.ok) throw new Error(String(res.status));
      window.MISE.notifyAlertsChanged();
    } catch (e) {
      console.warn('dashboard ack failed', e);
      setAlerts(previous);
    }
  };

  // If the user acks/resolves on /alerts in another tab and comes back,
  // the dashboard should pick up the change too.
  React.useEffect(() => {
    const refresh = async () => {
      try {
        const data = await window.MISE.fetchAlertsCached();
        setAlerts(data);
      } catch {}
    };
    return window.MISE.subscribeAlerts(refresh);
  }, []);

  const open = alerts.filter(a => a.status === 'open');
  const top = [...open].sort((a, b) => (SEV_RANK[a.sev] ?? 9) - (SEV_RANK[b.sev] ?? 9)).slice(0, 5);

  return (
    <div className="card" data-screen-label="Active alerts">
      <div className="card-head">
        <div>
          <div className="card-title">Active alerts <span className="count-chip">{loading ? '…' : open.length}</span></div>
          <div className="card-sub">
            Live from the AI compliance auditor · sorted by severity
            {scanStatus?.lastScannedAt ? (
              <>
                {' · '}
                <span title={scanStatus.lastError || new Date(scanStatus.lastScannedAt).toLocaleString()}
                  style={{ color: scanStatus.lastStatus === 'error' ? 'var(--red)' : 'inherit' }}>
                  audited {formatRelativeDash(new Date(scanStatus.lastScannedAt))}
                </span>
              </>
            ) : null}
          </div>
        </div>
      </div>
      <div className="alerts-list">
        {loading && <div style={{ padding: 20, color: 'var(--muted)', fontSize: 13 }}>Running compliance auditor…</div>}
        {error && <div style={{ padding: 20, color: 'var(--red)', fontSize: 13 }}>Failed to load alerts: {error}</div>}
        {!loading && !error && top.length === 0 && (
          <div style={{ padding: 20, color: 'var(--muted)', fontSize: 13 }}>No open alerts. Everything looks compliant.</div>
        )}
        {top.map(a => (
          <div
            key={a.id}
            className="alert-row"
            style={{ cursor: 'pointer' }}
            onClick={() => { window.location.href = `/alerts?id=${encodeURIComponent(a.id)}`; }}
          >
            <span className={`alert-sev ${a.sev}`}/>
            <div className="alert-main">
              <div className="store">
                <a className="num" href={`/stores/${a.num.replace('#', '')}`} onClick={(e) => e.stopPropagation()}>{a.num}</a>
                <span>·</span>
                <span className="name" style={{ marginLeft: 4 }}>{a.store}</span>
              </div>
              <div className="summary">{a.title}</div>
            </div>
            <div className="alert-age">{a.age}</div>
            <button
              className={`ack-btn ${a.status === 'acked' ? 'acked' : ''}`}
              onClick={(e) => { e.stopPropagation(); ack(a.id); }}
              disabled={a.status === 'acked'}
            >{a.status === 'acked' ? '✓ Acked' : 'Ack'}</button>
          </div>
        ))}
      </div>
      <div className="card-foot">
        <a className="link-arrow" href="/alerts">View all alerts →</a>
      </div>
    </div>
  );
}

/* ——— Stores at risk ——— */
function segsForScore(composite) {
  // 10-segment bar: each cell is 10pts of the composite.
  const filled = Math.round((composite || 0) / 10);
  const out = [];
  for (let i = 0; i < 10; i++) {
    if (i >= filled) out.push('empty');
    else if (i < 6) out.push('r');
    else if (i < 8) out.push('a');
    else out.push('g');
  }
  return out;
}

function StoresAtRisk() {
  const [scores, setScores] = React.useState({});
  React.useEffect(() => {
    const refresh = () => fetch(`${DASH_API_BASE}/stores/scores`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : {})
      .then(setScores)
      .catch(() => setScores({}));
    refresh();
    if (window.MISE && window.MISE.subscribeAlerts) {
      return window.MISE.subscribeAlerts(refresh);
    }
  }, []);

  // Stores in the at-risk bands only. "At risk" here = composite < 80
  // (red/amber zones). Healthy stores (≥80) and pending stores (no
  // live data → null composite) are excluded — surfacing a B/A store
  // under "at risk" is misleading, and pending stores aren't risky,
  // they're unmeasured.
  const byNum = new Map(window.MISE.stores.map(s => [s.num, s]));
  const allScored = Object.entries(scores).filter(([, s]) => s.composite != null);
  const atRisk = allScored
    .filter(([, s]) => s.composite < 80)
    .map(([num, s]) => {
      const meta = byNum.get(num) || {};
      return {
        num,
        name: (meta.name || num).replace(/^Wingstop\s+/, ''),
        city: meta.city || '—',
        score: s.composite,
        zone: s.healthZone,
        segs: segsForScore(s.composite),
      };
    })
    .sort((a, b) => a.score - b.score)
    .slice(0, 5);

  const segCls = (s) => s === 'empty' ? 'empty' : s;
  // Loading vs. all-clear vs. populated. Loading is only true on the
  // very first render before /stores/scores resolves; after that the
  // map is at least an empty object.
  const loading = !scores || Object.keys(scores).length === 0;

  return (
    <div className="card" data-screen-label="Stores at risk">
      <div className="card-head">
        <div>
          <div className="card-title">Stores at risk</div>
          <div className="card-sub">Composite below 80 — audit, food safety, labor, open alerts</div>
        </div>
        <a className="link-arrow" href="/stores">All →</a>
      </div>
      <div className="risk-list">
        {loading && (
          <div style={{ padding: 24, color: 'var(--muted)', fontSize: 13, textAlign: 'center' }}>Loading scores…</div>
        )}
        {!loading && atRisk.length === 0 && (
          <div style={{
            padding: '20px 18px', color: 'var(--muted)', fontSize: 13.5,
            background: 'var(--bg-2)', border: '1px solid var(--hair)',
            borderRadius: 10, lineHeight: 1.5,
          }}>
            No stores in the at-risk bands right now. {allScored.length > 0 && (
              <>{allScored.length} store{allScored.length === 1 ? '' : 's'} scoring B or higher.</>
            )}
          </div>
        )}
        {atRisk.map((s) => (
          <div
            key={s.num}
            className="risk-row"
            style={{ cursor: 'pointer' }}
            onClick={() => { window.location.href = `/stores/${s.num.replace('#', '')}`; }}
          >
            <div className="risk-name">
              <span className="num">{s.num}</span><span className="nm">{s.name}</span>
              <span className="city">{s.city}</span>
            </div>
            <div className="score-bar">
              {s.segs.map((seg, j) => <i key={j} className={segCls(seg)}/>)}
            </div>
            <div className="risk-score">{s.score}</div>
            <div className={`risk-delta ${s.zone === 'red' ? 'red' : s.zone === 'amber' ? 'neutral' : 'green'}`}>
              {s.zone === 'red' ? '↓' : s.zone === 'green' ? '↑' : '·'}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ——— Activity timeline ——— */
function ActivityTimeline() {
  // Live activity: alerts acknowledged/resolved/snoozed/assigned, brief
  // generations, scan completions. Backend is authoritative; the
  // timeline visual is just a horizontal day-axis projection of those
  // rows. Empty state when nothing has been logged today.
  const [events, setEvents] = React.useState(null);
  const [error, setError] = React.useState(null);

  const refresh = React.useCallback(() => {
    fetch(`${DASH_API_BASE}/activity?limit=100`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setEvents)
      .catch(e => setError(e.message || String(e)));
  }, []);

  React.useEffect(() => {
    refresh();
    // Re-fetch whenever an alert action touches the cache — those
    // emit activity rows so the timeline should reflect them.
    const unsub = window.MISE && window.MISE.subscribeAlerts
      ? window.MISE.subscribeAlerts(refresh)
      : null;
    return () => { if (unsub) unsub(); };
  }, [refresh]);

  const [hovered, setHovered] = React.useState(null);

  // Project today's local-day window (00:00 → 23:59 in browser zone)
  // onto a 6 AM → midnight axis so morning briefs at 6:30 land near
  // the left edge and afternoon activity stretches right.
  const startH = 6, endH = 22;
  const span = endH - startH;
  const pct = (h) => Math.max(0, Math.min(100, ((h - startH) / span) * 100));
  const labelFor = (h) => h === 0 ? '12 AM' : h === 12 ? '12 PM' : h < 12 ? `${h} AM` : `${h - 12} PM`;
  const hours = [6, 9, 12, 15, 18, 21];

  // Hour-of-day for "now" in the user's zone — mark on the axis.
  const now = new Date();
  const nowH = now.getHours() + now.getMinutes() / 60;

  // Filter to today's events and project to hour-of-day. Activity
  // events the API returned outside today's window are dropped here.
  const todayStart = new Date();
  todayStart.setHours(0, 0, 0, 0);

  const projected = (events || [])
    .map(e => {
      const ts = new Date(e.occurredAt);
      return { ...e, _ts: ts, _h: ts.getHours() + ts.getMinutes() / 60 };
    })
    .filter(e => e._ts >= todayStart);

  const counts = {
    rules: projected.filter(e => e.kind === 'scan.completed').length,
    alerts: projected.filter(e => e.kind && e.kind.startsWith('alert.')).length,
    briefs: projected.filter(e => e.kind === 'brief.composed').length,
  };

  const fmtTime = (d) => d.toLocaleTimeString('en-US',
    { hour: 'numeric', minute: '2-digit' });

  if (events === null && !error) {
    return (
      <div className="card timeline" data-screen-label="Activity timeline">
        <div className="card-title" style={{ padding: 0 }}>Today's activity</div>
        <div style={{ padding: '24px 0', color: 'var(--muted)', fontSize: 13 }}>Loading activity…</div>
      </div>
    );
  }
  if (error) {
    return (
      <div className="card timeline" data-screen-label="Activity timeline">
        <div className="card-title" style={{ padding: 0 }}>Today's activity</div>
        <div style={{ padding: '14px 0', color: 'var(--red)', fontSize: 13 }}>
          Couldn't load activity: {error}
        </div>
      </div>
    );
  }

  return (
    <div className="card timeline" data-screen-label="Activity timeline">
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16 }}>
        <div>
          <div className="card-title" style={{ padding: 0 }}>Today's activity</div>
          <div className="card-sub" style={{ marginTop: 4 }}>Brief generations, scan completions, alert actions · since 12:00 AM</div>
        </div>
        <div className="tl-summary">
          <span><strong>{projected.length}</strong> events</span>
          <span><strong>{counts.rules}</strong> scans</span>
          <span><strong>{counts.alerts}</strong> alert actions</span>
          <span><strong>{counts.briefs}</strong> briefs</span>
        </div>
      </div>

      {projected.length === 0 ? (
        <div style={{
          padding: '20px 18px', marginTop: 12,
          borderRadius: 10, background: 'var(--bg-2)',
          border: '1px solid var(--hair)', color: 'var(--muted)', fontSize: 13.5,
        }}>
          Nothing logged yet today. Activity appears here as scans complete, alerts get acted on, and briefs generate.
        </div>
      ) : (
        <div className="tl-axis-wrap">
          <div className="tl-axis">
            {hours.map(h => (
              <div key={h} className="tl-tick" style={{ left: `${pct(h)}%` }}>
                <span className="tlbl">{labelFor(h)}</span>
              </div>
            ))}
            <div className="tl-axis-now" style={{ left: `${pct(nowH)}%` }}/>
          </div>

          {projected.map((e, i) => (
            <div
              key={e.id || i}
              className={`tl-marker ${e.severity || 'gray'} ${e.severity === 'gray' ? 'filled' : ''}`}
              style={{ left: `${pct(e._h)}%`, top: '50%' }}
              onMouseEnter={() => setHovered(i)}
              onMouseLeave={() => setHovered(h => h === i ? null : h)}
            >
              <span className="m"/>
              {hovered === i && (
                <div className="tl-pop">
                  <span className="ttime">
                    {fmtTime(e._ts)}
                    {e.storeNum ? ` · ${e.storeNum}` : ''}
                  </span>
                  <span className="ttitle">{e.title}</span>
                  {e.detail && (
                    <span style={{ display: 'block', fontSize: 11, color: 'var(--muted)', marginTop: 4 }}>
                      {e.detail}
                    </span>
                  )}
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      <div className="tl-legend">
        <span className="tl-leg"><span className="sw" style={{ background: 'var(--red)' }}/> Critical</span>
        <span className="tl-leg"><span className="sw" style={{ background: 'var(--amber)' }}/> Needs review</span>
        <span className="tl-leg"><span className="sw" style={{ background: 'var(--green)' }}/> Resolved / clean</span>
        <span className="tl-leg"><span className="sw" style={{ background: 'var(--muted-2)' }}/> Routine</span>
        <span className="tl-leg" style={{ marginLeft: 'auto', color: 'var(--accent)' }}>Hover any marker for detail</span>
      </div>
    </div>
  );
}

/* ——— App ——— */
function App() {
  return (
    <div className="shell">
      <Tweaks/>
      <Sidebar/>
      <div>
        <Topbar/>
        <main className="main">
          <Brief/>
          <KPIs/>
          <div className="two-col">
            <ActiveAlerts/>
            <StoresAtRisk/>
          </div>
          <ActivityTimeline/>
        </main>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
