/* Dashboard — Mission Control (2026-06-12 redesign).
   Per-role variants:

     aquaos_admin    → PlatformDashboard  (fleet digest + org list)
     org_admin       → OrgDashboard       (Mission Control: state hero,
     site_manager                          AI brief, KPI strip w/ deltas
     marketing_admin                       + sparklines, attention inbox,
                                           fleet bezel strip, activity)
     designer        → DesignerDashboard  (Campaigns + Media quick links)
     operator        → OperatorDashboard  (read-only fleet strip)

   Design notes:
   - The hero CHANGES SHAPE with urgency: quiet text headline on healthy
     days, a promoted `.aq-hero-card.is-danger/.is-warn` card with CTAs +
     offline-screen chips when something needs action.
   - Analytics KPIs (scans/impressions + deltas + sparklines) come from
     /api/analytics/metrics and are capability-gated by
     Auth.canViewAnalytics(); callers without it get the counts strip.
   - The AI brief reuses /api/analytics/insights (same contract as the
     Analytics page's InsightsCard) and falls back to a deterministic
     summary when the model isn't configured — the card never dead-ends.
   - Motion: staggered section entrance (.aq-dash-stagger), count-up
     numerals, breathing danger dot. All gated by prefers-reduced-motion
     (CSS via media query, JS via matchMedia). */

function todayLong() {
  const d = new Date();
  return d.toLocaleDateString(undefined, {
    weekday: 'long', day: 'numeric', month: 'long',
  });
}

/* Screen liveness — mirrors the heartbeat model used by Displays and the
   analytics evaluator. `screens.is_online` is written on every heartbeat
   (screens.js POST /heartbeat) and flipped false by the ~2-min offline sweep,
   so it's the authoritative signal; fall back to last_heartbeat freshness for
   rows where is_online was never reported. NOTE: last_seen_at is the FEED-
   reload timestamp, NOT liveness — keying off it (the previous bug) showed
   live-but-not-recently-reloaded screens as offline. */
function screenIsOnline(s) {
  if (s.is_online === true || s.is_online === 1) return true;
  if (s.is_online === false || s.is_online === 0) return false;
  const hb = s.last_heartbeat || s.last_seen_at;
  return hb ? (Date.now() - new Date(hb).getTime()) < 2 * 60 * 1000 : false;
}

function screenNeverSeen(s) {
  return !s.last_heartbeat && !s.last_seen_at;
}

function dashRelativeTime(iso) {
  if (!iso) return 'never';
  const m = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
  if (m < 1) return 'just now';
  if (m < 60) return `${m} min ago`;
  const h = Math.round(m / 60);
  if (h < 24) return `${h} h ago`;
  return `${Math.round(h / 24)} d ago`;
}

/* Day-context timestamp for the activity feed: today → "14:02",
   yesterday → "yesterday · 23:17", this week → "Tue · 14:02",
   older → "9 Jun". */
function dashDayTime(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  const now = new Date();
  const time = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
  const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
  const dayDiff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000);
  if (dayDiff <= 0) return time;
  if (dayDiff === 1) return `yesterday · ${time}`;
  if (dayDiff < 7) return `${d.toLocaleDateString(undefined, { weekday: 'short' })} · ${time}`;
  return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
}

function dashHue(str) {
  let h = 0;
  for (const c of String(str || '')) h = (h * 31 + c.charCodeAt(0)) % 360;
  return h;
}

function dashGo(href) {
  return (e) => { if (e) e.preventDefault(); window.location.hash = href; };
}

/* ── Motion primitives ─────────────────────────────────────────────── */

const dashReducedMotion = () =>
  window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/* Count-up numeral: animates from the previously shown value to the
   target with a cubic ease-out. null target = still loading. */
function useCountUp(target, duration = 650) {
  const [val, setVal] = useState(null);
  const prevRef = useRef(0);
  useEffect(() => {
    if (target == null) return;
    if (dashReducedMotion() || prevRef.current === target) {
      setVal(target); prevRef.current = target; return;
    }
    const from = prevRef.current;
    let raf;
    const t0 = performance.now();
    const tick = (t) => {
      const p = Math.min(1, (t - t0) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(Math.round(from + (target - from) * eased));
      if (p < 1) raf = requestAnimationFrame(tick);
      else prevRef.current = target;
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, duration]);
  return target == null ? null : (val == null ? 0 : val);
}

/* ── Shared atoms ──────────────────────────────────────────────────── */

function DashEyebrow({ label }) {
  return (
    <div className="aq-eyebrow">
      <span>{label || 'Slate'}</span>
      <span className="aq-sep">·</span>
      <span>{todayLong()}</span>
    </div>
  );
}

function DashSkeleton({ w = 40, h = '0.9em' }) {
  return <span className="aq-skeleton" style={{ display: 'inline-block', width: w, height: h, borderRadius: 3, verticalAlign: 'middle' }} />;
}

/* Sparkline — 7-day/24-hour micro-trend under a KPI. Falls back to a
   flat hairline when there aren't enough points (honest "no data"). */
function DashSpark({ points, tone = 'accent' }) {
  const colors = {
    accent: 'var(--aq-accent)',
    danger: 'var(--aq-danger)',
    success: 'var(--aq-success)',
    dim: 'var(--aq-text-faint)',
  };
  const pts = (points || []).map(Number).filter((n) => !Number.isNaN(n));
  if (pts.length < 2) {
    /* Dotted so an empty trend can't be mistaken for a divider rule. */
    return (
      <svg viewBox="0 0 100 22" preserveAspectRatio="none" aria-hidden="true">
        <line x1="0" y1="18" x2="100" y2="18" stroke="var(--aq-line-strong)" strokeWidth="1" strokeDasharray="1 4" />
      </svg>
    );
  }
  const max = Math.max(...pts), min = Math.min(...pts);
  const range = (max - min) || 1;
  const poly = pts.map((v, i) =>
    `${(i / (pts.length - 1)) * 100},${20 - ((v - min) / range) * 16}`
  ).join(' ');
  return (
    <svg viewBox="0 0 100 22" preserveAspectRatio="none" aria-hidden="true">
      <polyline points={poly} fill="none" stroke={colors[tone] || colors.accent} strokeWidth="1.4" />
    </svg>
  );
}

/* KPI tile — count-up value, optional /total fraction, delta trend,
   sparkline, quiet sub-link. */
function DashKpi({ label, value, frac, trend, trendTone = 'flat', spark, sparkTone, sub, href, subTone }) {
  const display = useCountUp(value);
  const trendCls = { up: 'is-up', down: 'is-down', warn: 'is-warn', flat: '' }[trendTone] || '';
  const subColor = subTone === 'warn' ? 'var(--aq-warn)' : subTone === 'danger' ? 'var(--aq-danger)' : undefined;
  return (
    <div>
      <div className="aq-stat-label">{label}</div>
      <div className="aq-stat-value">
        {value == null ? <DashSkeleton /> : display.toLocaleString()}
        {frac != null && <span className="aq-stat-frac">/{frac}</span>}
        {trend && <span className={`aq-stat-trend ${trendCls}`}>{trend}</span>}
      </div>
      {spark !== undefined && <div className="aq-stat-spark"><DashSpark points={spark} tone={sparkTone} /></div>}
      <div className="aq-stat-sub">
        {href
          ? <a href={href} onClick={dashGo(href)} style={subColor ? { color: subColor } : null}>{sub}</a>
          : <span style={subColor ? { color: subColor } : null}>{sub}</span>}
      </div>
    </div>
  );
}


function DashError({ error }) {
  return (
    <div style={{
      padding: '8px 12px',
      background: 'var(--aq-danger-soft)',
      border: '1px solid var(--aq-danger-line)',
      borderRadius: 6, color: 'var(--aq-danger)', fontSize: 12,
    }}>{error}</div>
  );
}

/* ── Org data plumbing ─────────────────────────────────────────────── */

function useOrgData() {
  const me = Auth.getUser() || {};
  const orgName = (me.organisations && me.organisations[0] && me.organisations[0].name) || 'Workspace';
  const isSiteScoped = Auth.isSiteScoped();

  const [data, setData] = useState({
    speciesTotal: null,
    screens: null,          // full list — shared by hero, KPIs, inbox, fleet
    campaigns: null,        // full list — live count + stale drafts
    pendingApprovals: null,
    sitesTotal: null,
    error: null,
  });
  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        /* Approvals only when the workflow is enabled (paused 2026-06-03,
           CP_APPROVALS_ENABLED). Re-enables itself when the flag flips. */
        const approvalsOn = Auth.canApprove() && window.CP_APPROVALS_ENABLED === true;
        const [species, screens, campaigns, approvals] = await Promise.all([
          apiFetch('/api/species?limit=1').catch(() => null),
          apiFetchSiteScoped('/api/screens').catch(() => null),
          apiFetch('/api/campaigns').catch(() => null),
          approvalsOn ? apiFetch('/api/approvals').catch(() => null) : Promise.resolve(null),
        ]);
        if (cancelled) return;
        const screensList = (screens && (screens.screens || screens)) || [];
        const campaignsList = (campaigns && (campaigns.campaigns || campaigns)) || [];
        setData((d) => ({
          ...d,
          speciesTotal: species ? species.total : null,
          screens: screensList,
          campaigns: campaignsList,
          pendingApprovals: approvals ? (approvals.approvals || []).length : null,
          sitesTotal: isSiteScoped ? 1 : (me.sites || []).length || 1,
        }));
      } catch (err) { if (!cancelled) setData((d) => ({ ...d, error: err.message })); }
    }
    load();
    return () => { cancelled = true; };
  }, []);
  return { data, orgName, isSiteScoped };
}

/* Analytics layer — 24h metrics + unacknowledged alert fires. Both are
   capability-gated; either fetch failing degrades to the counts view. */
function useDashAnalytics() {
  const canAnalytics = Auth.canViewAnalytics && Auth.canViewAnalytics();
  const canAlerts = Auth.canViewAnalyticsAlerts && Auth.canViewAnalyticsAlerts();
  const [metrics, setMetrics] = useState(null);
  const [alertEvents, setAlertEvents] = useState(null);
  useEffect(() => {
    let cancelled = false;
    if (canAnalytics) {
      apiFetch('/api/analytics/metrics?range=24h&bucket=hour')
        .then((r) => { if (!cancelled) setMetrics(r); })
        .catch(() => { if (!cancelled) setMetrics(null); });
    }
    if (canAlerts) {
      apiFetch('/api/analytics/alert-events?unack=1&limit=5')
        .then((r) => { if (!cancelled) setAlertEvents(r.events || []); })
        .catch(() => { if (!cancelled) setAlertEvents([]); });
    } else {
      setAlertEvents([]);
    }
    return () => { cancelled = true; };
  }, [canAnalytics, canAlerts]);
  return { canAnalytics, metrics, alertEvents };
}

/* ── 1 · Hero — changes shape with urgency ─────────────────────────── */

function DashHero({ data }) {
  const me = Auth.getUser() || {};
  const firstName = (me.name || '').split(' ')[0] || 'there';
  const screens = data.screens || [];
  const total = screens.length;
  const offlineScreens = screens.filter((s) => !screenIsOnline(s) && !screenNeverSeen(s));
  const offline = offlineScreens.length;

  /* Idle — brand-new workspace. */
  if (data.screens != null && total === 0 && data.speciesTotal === 0) {
    return (
      <section className="aq-hero">
        <h1>
          <span className="aq-hero-dot" style={{ background: 'var(--aq-text-faint)', boxShadow: 'none' }} />
          Your network is waiting for its first screen
        </h1>
        <div className="aq-hero-meta"><span>Good to see you, {firstName}. Pair a screen from Displays to bring Slate to life.</span></div>
      </section>
    );
  }

  /* Incident — promoted card with built-in action. Danger when the whole
     fleet is down, warn for a partial outage. */
  if (offline > 0) {
    const tone = offline === total ? 'is-danger' : 'is-warn';
    const dotColor = offline === total ? 'var(--aq-danger)' : 'var(--aq-warn)';
    const newest = offlineScreens
      .map((s) => s.last_heartbeat || s.last_seen_at)
      .filter(Boolean)
      .sort()
      .pop();
    const chips = offlineScreens.slice(0, 4);
    return (
      <section className={`aq-hero-card ${tone}`}>
        <div className="aq-hero-card-row">
          <div className="aq-hero-card-body">
            <div className="aq-hero-card-title">
              <span className="aq-hero-dot is-breathing" style={{ background: dotColor, margin: 0 }} />
              {offline} screen{offline === 1 ? '' : 's'} offline
            </div>
            <div className="aq-hero-card-sub">
              {offline === total
                ? 'The whole fleet is dark — check power and network first.'
                : `${total - offline} of ${total} still serving. Most recent drop ${dashRelativeTime(newest)}.`}
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
            <button type="button" className="aq-hero-card-cta" onClick={dashGo('#displays')}>
              View fleet <Icon name="chevron-right" size={13} />
            </button>
          </div>
        </div>
        <div className="aq-chip-row">
          {chips.map((s) => (
            <button
              key={s.id} type="button" className="aq-chip"
              onClick={dashGo(`#screen/${s.screen_code || s.code}`)}
            >
              <span className="aq-chip-dot" />
              {s.name || s.screen_code}
              <span className="aq-chip-time">{dashRelativeTime(s.last_heartbeat || s.last_seen_at)}</span>
            </button>
          ))}
          {offlineScreens.length > 4 && (
            <button type="button" className="aq-chip" onClick={dashGo('#displays')}>
              +{offlineScreens.length - 4} more
            </button>
          )}
        </div>
      </section>
    );
  }

  /* Healthy / loading — quiet text headline. */
  const loading = data.screens == null;
  return (
    <section className="aq-hero">
      <h1>
        <span className="aq-hero-dot" style={{ background: 'var(--aq-success)', boxShadow: '0 0 14px var(--aq-success)' }} />
        {loading ? 'Checking your network…' : `All ${total} screen${total === 1 ? '' : 's'} online`}
      </h1>
      <div className="aq-hero-meta"><span>Good to see you, {firstName}.</span></div>
    </section>
  );
}

/* ── 2 · Brief — AI when configured, deterministic otherwise ───────── */

function DashBrief({ data, metrics, canAnalytics }) {
  const [insight, setInsight] = useState(null);
  const [aiState, setAiState] = useState('idle'); // idle | loading | done | unavailable

  useEffect(() => {
    if (!canAnalytics || !metrics) return;
    let cancelled = false;
    setAiState('loading');
    apiFetch('/api/analytics/insights', {
      method: 'POST',
      body: JSON.stringify({
        range: '24h',
        dashboardSnapshot: { range: '24h', periodLabel: '24h', metrics, campaigns: [], species: [] },
      }),
    })
      .then((r) => { if (!cancelled) { setInsight(r); setAiState('done'); } })
      .catch(() => { if (!cancelled) setAiState('unavailable'); });
    return () => { cancelled = true; };
  }, [canAnalytics, metrics]);

  /* Deterministic fallback — composed from live signals so the card is
     honest (and useful) even without the model. */
  const fallback = (() => {
    const parts = [];
    const screens = data.screens || [];
    const online = screens.filter(screenIsOnline).length;
    const live = (data.campaigns || []).filter((c) => c.status === 'live').length;
    if (metrics && metrics.scans && metrics.scans.value > 0) {
      const d = metrics.scans.delta_pct;
      parts.push(`${metrics.scans.value.toLocaleString()} QR scans in the last 24h${d ? ` (${d > 0 ? 'up' : 'down'} ${Math.abs(d)}% on the day before)` : ''}`);
    }
    if (live > 0) parts.push(`${live} live campaign${live === 1 ? '' : 's'} running`);
    if (screens.length > 0) parts.push(`${online} of ${screens.length} screens online`);
    if (data.pendingApprovals > 0) parts.push(`${data.pendingApprovals} item${data.pendingApprovals === 1 ? '' : 's'} pending review`);
    if (parts.length === 0) parts.push('Catalogue is steady; no urgent actions');
    return parts.join(' · ') + '.';
  })();

  const showAi = aiState === 'done' && insight && insight.body;

  return (
    <div className="aq-digest">
      <div className="aq-digest-head">
        <div className="aq-digest-icon"><Icon name="spark" size={12} /></div>
        <div className="aq-digest-title">Today's brief</div>
        <div className="aq-digest-time">
          {aiState === 'loading' ? 'thinking…'
            : showAi ? `${insight.cached ? 'cached' : 'fresh'} · ${todayLong()}`
            : `live · ${todayLong()}`}
        </div>
      </div>
      <div className="aq-digest-body">
        {showAi ? (
          <React.Fragment>
            {insight.headline ? <strong style={{ color: 'var(--aq-text)', fontWeight: 500 }}>{insight.headline} </strong> : null}
            {insight.body}
          </React.Fragment>
        ) : fallback}
      </div>
    </div>
  );
}

/* ── 3 · KPI strip ─────────────────────────────────────────────────── */

function dashFmtDelta(pct) {
  if (pct == null || pct === 0) return null;
  return `${pct > 0 ? '▲' : '▼'} ${Math.abs(pct)}%`;
}

function DashKpis({ data, metrics, canAnalytics }) {
  const screens = data.screens || [];
  const online = screens.filter(screenIsOnline).length;
  const total = data.screens == null ? null : screens.length;
  const liveCampaigns = data.campaigns == null ? null
    : data.campaigns.filter((c) => c.status === 'live');
  const liveName = liveCampaigns && liveCampaigns.length === 1 ? liveCampaigns[0].name : null;
  const offline = (total || 0) - online;

  if (canAnalytics) {
    const scans = metrics && metrics.scans;
    const imps = metrics && metrics.impressions;
    return (
      <section>
        <div className="aq-stats">
          <DashKpi
            label="Scans · 24h"
            value={scans ? scans.value : (metrics === null ? null : 0)}
            trend={scans ? dashFmtDelta(scans.delta_pct) : null}
            trendTone={scans && scans.delta_pct > 0 ? 'up' : scans && scans.delta_pct < 0 ? 'down' : 'flat'}
            spark={scans && scans.sparkline}
            sub={scans && scans.prev ? `vs ${scans.prev.toLocaleString()} the day before` : 'QR scans across the fleet'}
            href="#analytics"
          />
          <DashKpi
            label="Impressions · 24h"
            value={imps ? imps.value : (metrics === null ? null : 0)}
            trend={imps ? dashFmtDelta(imps.delta_pct) : null}
            trendTone={imps && imps.delta_pct > 0 ? 'up' : imps && imps.delta_pct < 0 ? 'down' : 'flat'}
            spark={imps && imps.sparkline}
            sub={total != null ? `across ${total} screen${total === 1 ? '' : 's'}` : 'across the fleet'}
            href="#analytics"
          />
          <DashKpi
            label="Screens online"
            value={total == null ? null : online}
            frac={total}
            trend={offline > 0 ? `▼ ${offline} down` : total > 0 ? 'steady' : null}
            trendTone={offline > 0 ? 'down' : 'flat'}
            sparkTone={offline > 0 ? 'danger' : 'success'}
            spark={total != null && total > 0 ? screens.map((s) => (screenIsOnline(s) ? 1 : 0)) : null}
            sub={offline > 0 ? `${offline} need${offline === 1 ? 's' : ''} attention` : 'all serving'}
            subTone={offline > 0 ? 'danger' : null}
            href="#displays"
          />
          <DashKpi
            label="Live campaigns"
            value={liveCampaigns == null ? null : liveCampaigns.length}
            spark={null}
            sparkTone="dim"
            sub={liveName ? `${liveName} · serving now` : liveCampaigns && liveCampaigns.length > 0 ? 'serving now' : 'none scheduled'}
            href="#campaigns"
          />
        </div>
      </section>
    );
  }

  /* Counts fallback for roles without analytics capability. */
  return (
    <section>
      <div className="aq-stats">
        <DashKpi label="Species" value={data.speciesTotal} sub="in your library" href="#library" />
        <DashKpi label="Sites" value={data.sitesTotal} sub="across the org" />
        <DashKpi label="Screens" value={total} sub={`${online} online`} href="#displays" />
        <DashKpi
          label="Live campaigns"
          value={liveCampaigns == null ? null : liveCampaigns.length}
          sub={liveCampaigns && liveCampaigns.length > 0 ? 'serving now' : 'none scheduled'}
          href="#campaigns"
        />
      </div>
    </section>
  );
}

/* ── 4 · Attention inbox ───────────────────────────────────────────── */

const STALE_DRAFT_DAYS = 60;

function buildInboxItems(data, alertEvents) {
  const items = [];

  (alertEvents || []).forEach((ev) => {
    items.push({
      key: `alert-${ev.id}`,
      tone: 'is-danger',
      icon: 'bell',
      title: `Alert fired — ${ev.alert_name || ev.metric}`,
      meta: ev.observed_value != null ? `observed ${ev.observed_value} (threshold ${ev.threshold})` : 'threshold crossed',
      time: dashDayTime(ev.fired_at),
      act: 'Review',
      href: '#alerts',
    });
  });

  if (data.pendingApprovals > 0) {
    items.push({
      key: 'approvals',
      tone: 'is-warn',
      icon: 'check',
      title: `${data.pendingApprovals} item${data.pendingApprovals === 1 ? '' : 's'} pending your review`,
      meta: 'submitted by your team',
      time: '',
      act: 'Review',
      href: '#approvals',
    });
  }

  const staleDrafts = (data.campaigns || []).filter((c) => {
    if (c.status !== 'draft' || !c.updated_at) return false;
    return (Date.now() - new Date(c.updated_at).getTime()) > STALE_DRAFT_DAYS * 86400000;
  });
  if (staleDrafts.length > 0) {
    const oldest = staleDrafts.reduce((a, b) => (a.updated_at < b.updated_at ? a : b));
    const days = Math.round((Date.now() - new Date(oldest.updated_at).getTime()) / 86400000);
    items.push({
      key: 'stale-drafts',
      tone: 'is-warn',
      icon: 'clock',
      title: staleDrafts.length === 1
        ? `Stale draft — “${oldest.name}”`
        : `${staleDrafts.length} stale drafts`,
      meta: `untouched for ${days} days — archive or finish`,
      time: `${days} d`,
      act: 'Open',
      href: '#campaigns',
    });
  }

  /* Panels whose requests are being rejected (lost pairing token) —
     they look alive (offline cache + Fully mirror) while nothing
     reaches them. Highest-priority screen problem after an outage. */
  (data.screens || []).filter((s) =>
    s.device_auth_failed_at &&
    (!s.last_heartbeat || new Date(s.device_auth_failed_at) > new Date(s.last_heartbeat))
  ).slice(0, 2).forEach((s) => {
    items.push({
      key: `auth-${s.id}`,
      tone: 'is-danger',
      icon: 'monitor',
      title: `${s.name || s.screen_code} needs re-pairing`,
      meta: 'device requests rejected — re-pair with the site PIN on the panel',
      time: dashRelativeTime(s.device_auth_failed_at).replace(' ago', ''),
      act: 'Open',
      href: `#screen/${s.screen_code || s.code}`,
    });
  });

  (data.screens || []).filter(screenNeverSeen).slice(0, 2).forEach((s) => {
    const since = s.paired_at || s.created_at;
    items.push({
      key: `never-${s.id}`,
      tone: 'is-info',
      icon: 'monitor',
      title: `${s.name || s.screen_code} has never come online`,
      meta: since ? `paired ${dashRelativeTime(since)}, no first heartbeat — check the device` : 'no first heartbeat — check the device',
      time: since ? dashRelativeTime(since).replace(' ago', '') : '',
      act: 'Diagnose',
      href: `#screen/${s.screen_code || s.code}`,
    });
  });

  return items.slice(0, 5);
}

function DashInbox({ data, alertEvents }) {
  const loading = data.screens == null || data.campaigns == null || alertEvents == null;
  const items = loading ? [] : buildInboxItems(data, alertEvents);

  return (
    <section className="aq-section">
      <div className="aq-section-head">
        <div className="aq-section-title">
          <Icon name="filter" />Needs your attention
          {!loading && items.length > 0 && <span className="aq-count-pill">{items.length}</span>}
        </div>
      </div>
      <div className="aq-inbox-list">
        {loading && (
          <div style={{ display: 'flex', flexDirection: 'column' }}>
            {[70, 55].map((w, i) => (
              <div key={i} className="aq-inbox-row" style={{ cursor: 'default' }}>
                <span className="aq-skeleton" style={{ width: 26, height: 26, borderRadius: 6, flexShrink: 0 }} />
                <span className="aq-skeleton" style={{ flex: 1, maxWidth: `${w}%`, height: 12, borderRadius: 3 }} />
              </div>
            ))}
          </div>
        )}
        {!loading && items.length === 0 && (
          <div className="aq-inbox-zero">
            <div className="aq-zero-icon"><Icon name="check" size={18} /></div>
            <h4>Inbox zero</h4>
            <p>No alerts, approvals, or stale work. Enjoy it.</p>
          </div>
        )}
        {!loading && items.map((it) => (
          <div
            key={it.key}
            className={`aq-inbox-row ${it.tone}`}
            role="link" tabIndex={0}
            onClick={dashGo(it.href)}
            onKeyDown={(e) => { if (e.key === 'Enter') dashGo(it.href)(e); }}
          >
            <div className="aq-inbox-icon"><Icon name={it.icon} size={13} /></div>
            <div className="aq-inbox-body">
              <div className="aq-inbox-title">{it.title}</div>
              <div className="aq-inbox-meta">{it.meta}</div>
            </div>
            <span className="aq-inbox-act">{it.act}</span>
            {it.time ? <span className="aq-inbox-time">{it.time}</span> : null}
          </div>
        ))}
      </div>
    </section>
  );
}

/* ── 5 · Fleet strip — miniature display bezels ────────────────────── */

function DashFleetTile({ screen }) {
  const [imgFailed, setImgFailed] = useState(false);
  const never = screenNeverSeen(screen);
  const online = screenIsOnline(screen);
  const stateCls = never ? 'is-waiting' : online ? 'is-online' : 'is-offline';
  const hue = dashHue(screen.zone_name || screen.name || screen.id);
  const photo = !imgFailed && screen.last_snapshot_url;
  const stateLabel = never ? 'Never seen'
    : online ? 'Live'
    : dashRelativeTime(screen.last_heartbeat || screen.last_seen_at);

  return (
    <div
      className={`aq-fleet-tile ${stateCls}`}
      role="link" tabIndex={0}
      title={`${screen.name || screen.screen_code} — ${never ? 'never seen' : online ? 'online' : 'offline'}`}
      onClick={dashGo(`#screen/${screen.screen_code || screen.code}`)}
      onKeyDown={(e) => { if (e.key === 'Enter') dashGo(`#screen/${screen.screen_code || screen.code}`)(e); }}
    >
      <div className="aq-fleet-frame">
        <div className="aq-fleet-screen">
          {never ? (
            <div className="aq-fleet-ph" style={{ background: 'var(--aq-surface-2)' }} />
          ) : photo ? (
            <img src={photo} alt="" loading="lazy" onError={() => setImgFailed(true)} />
          ) : (
            <div className="aq-fleet-ph" style={{
              background: `radial-gradient(ellipse 90% 80% at 55% 35%, oklch(0.45 0.12 ${hue} / 0.9), oklch(0.2 0.06 ${hue}) 80%)`,
            }} />
          )}
          {!online && !never && <div className="aq-fleet-status">No signal</div>}
          {never && <div className="aq-fleet-status" style={{ color: 'var(--aq-text-faint)', textShadow: 'none' }}>Awaiting first boot</div>}
        </div>
        <span className="aq-fleet-led" />
      </div>
      <div className="aq-fleet-caption">
        <span className="aq-fleet-cdot" />
        <span className="aq-fleet-name">{screen.name || screen.screen_code}</span>
        <span className="aq-fleet-state">{stateLabel}</span>
      </div>
    </div>
  );
}

function DashFleet({ screens }) {
  if (screens == null) {
    return (
      <section className="aq-section">
        <div className="aq-section-head">
          <div className="aq-section-title"><Icon name="monitor" />Fleet</div>
        </div>
        <div className="aq-fleet">
          {[0, 1, 2, 3, 4].map((i) => (
            <div key={i}>
              <div className="aq-skeleton" style={{ aspectRatio: '16/9', borderRadius: 8 }} />
            </div>
          ))}
        </div>
      </section>
    );
  }
  if (screens.length === 0) {
    return (
      <section className="aq-section">
        <div className="aq-section-head">
          <div className="aq-section-title"><Icon name="monitor" />Fleet</div>
        </div>
        <div className="aq-card" style={{ padding: '28px 16px', textAlign: 'center' }}>
          <div style={{ fontWeight: 500, fontSize: 13, color: 'var(--aq-text-dim)', marginBottom: 4 }}>No screens yet</div>
          <div style={{ fontSize: 12, color: 'var(--aq-text-faint)' }}>
            Pair your first screen from <a href="#displays" onClick={dashGo('#displays')} style={{ color: 'var(--aq-accent)' }}>Displays</a>.
          </div>
        </div>
      </section>
    );
  }
  /* Offline first (they need eyes), then never-seen, then online. Cap at
     10 — two rows — and let "Open displays" carry the rest. */
  const rank = (s) => (screenNeverSeen(s) ? 1 : screenIsOnline(s) ? 2 : 0);
  const sorted = [...screens].sort((a, b) => rank(a) - rank(b));
  const visible = sorted.slice(0, 10);
  return (
    <section className="aq-section">
      <div className="aq-section-head">
        <div className="aq-section-title"><Icon name="monitor" />Fleet</div>
        <a className="aq-section-link" href="#displays" onClick={dashGo('#displays')}>
          Open displays <Icon name="chevron-right" size={11} />
        </a>
      </div>
      <div className="aq-fleet">
        {visible.map((s) => <DashFleetTile key={s.id} screen={s} />)}
      </div>
    </section>
  );
}

/* ── 6 · Activity — humanized sentences ────────────────────────────── */

const DASH_VERBS = {
  create: 'created', update: 'updated', delete: 'deleted',
  publish: 'published', unpublish: 'unpublished', approve: 'approved',
  reject: 'rejected', invite: 'invited', login: 'signed in',
  logout: 'signed out', pair: 'paired', archive: 'archived',
};

function dashEntityLabel(e) {
  for (const v of [e.new_value, e.old_value]) {
    if (!v) continue;
    let obj = v;
    if (typeof v === 'string') { try { obj = JSON.parse(v); } catch (_) { continue; } }
    const label = obj && (obj.name || obj.title || obj.common_name || obj.email);
    if (label) return label;
  }
  return null;
}

function dashActivityRows(logs) {
  const rows = logs.map((e) => {
    const actor = e.actor_name
      || (e.user_email ? e.user_email.split('@')[0] : null)
      || 'System';
    const verb = DASH_VERBS[e.action] || (e.action || 'touched').replace(/_/g, ' ');
    const entity = (e.entity_type || '').replace(/_/g, ' ');
    const label = dashEntityLabel(e);
    return {
      id: e.id,
      actor,
      isSystem: !e.actor_name && !e.user_email,
      sentence: `${verb} ${entity}`,
      label,
      time: e.created_at || e.time,
    };
  });
  /* Collapse consecutive identical sentences (the old feed showed the
     same "update creative" row six times). */
  const grouped = [];
  for (const r of rows) {
    const prev = grouped[grouped.length - 1];
    if (prev && prev.actor === r.actor && prev.sentence === r.sentence && prev.label === r.label) {
      prev.count += 1;
    } else {
      grouped.push({ ...r, count: 1 });
    }
  }
  return grouped.slice(0, 6);
}

function RecentActivityCard() {
  const [logs, setLogs] = useState(null);
  useEffect(() => {
    apiFetch('/api/audit?limit=12')
      .then((r) => setLogs(r.logs || []))
      .catch(() => setLogs([]));
  }, []);

  return (
    <section className="aq-section">
      <div className="aq-section-head">
        <div className="aq-section-title"><Icon name="clock" />Recent activity</div>
        <a className="aq-section-link" href="#audit" onClick={dashGo('#audit')}>
          Audit log <Icon name="chevron-right" size={11} />
        </a>
      </div>
      <div className="aq-card" style={{ padding: '6px 16px' }}>
        {logs === null && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10, padding: '10px 0' }}>
            {[70, 55, 80, 62].map((w, i) => (
              <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span className="aq-skeleton" style={{ width: 22, height: 22, borderRadius: '50%', flexShrink: 0 }} />
                <span className="aq-skeleton" style={{ flex: 1, height: 12, borderRadius: 3, maxWidth: `${w}%` }} />
                <span className="aq-skeleton" style={{ width: 38, height: 10, borderRadius: 3 }} />
              </div>
            ))}
          </div>
        )}
        {logs !== null && logs.length === 0 && (
          <div style={{ padding: '24px 0', textAlign: 'center' }}>
            <div style={{ fontWeight: 500, fontSize: 13, color: 'var(--aq-text-dim)', marginBottom: 4 }}>No activity yet</div>
            <div style={{ fontSize: 12, color: 'var(--aq-text-faint)' }}>This feed fills as your team edits species, campaigns, and screens.</div>
          </div>
        )}
        {logs !== null && logs.length > 0 && (
          <div className="aq-activity">
            {dashActivityRows(logs).map((r) => (
              <div className="aq-activity-row" key={r.id}>
                <span className={`aq-avatar-mini${r.isSystem ? ' is-sys' : ''}`}>
                  {r.isSystem ? '⟳' : r.actor.slice(0, 1).toUpperCase()}
                </span>
                <span>
                  <strong>{r.actor}</strong> {r.sentence}
                  {r.label ? <em> “{r.label}”</em> : null}
                  {r.count > 1 ? <span style={{ color: 'var(--aq-text-faint)' }}> ×{r.count}</span> : null}
                </span>
                <span className="aq-time">{dashDayTime(r.time)}</span>
              </div>
            ))}
          </div>
        )}
      </div>
    </section>
  );
}

/* ── Org dashboard — Mission Control assembly ──────────────────────── */

function OrgDashboard() {
  const { data } = useOrgData();
  const { canAnalytics, metrics, alertEvents } = useDashAnalytics();

  return (
    <div className="aq-content">
      <div className="aq-content-inner aq-dash-stagger" style={{ maxWidth: 1400 }}>
        <DashHero data={data} />
        <DashBrief data={data} metrics={metrics} canAnalytics={canAnalytics} />
        <DashKpis data={data} metrics={metrics} canAnalytics={canAnalytics} />
        <DashInbox data={data} alertEvents={alertEvents} />
        <DashFleet screens={data.screens} />
        <RecentActivityCard />
        {data.error && <DashError error={data.error} />}
      </div>
    </div>
  );
}

/* ── Platform dashboard (aquaos_admin) ─────────────────────────────── */

function usePlatformData() {
  const [data, setData] = useState({
    orgsTotal: null, sitesTotal: null, speciesTotal: null,
    usersTotal: null, pendingGlobal: null, error: null,
  });
  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        const stats = await apiFetch('/api/auth/platform/stats').catch(() => null);
        if (cancelled) return;
        if (!stats) { setData((d) => ({ ...d, error: 'Could not load platform stats' })); return; }
        setData((d) => ({
          ...d,
          orgsTotal:     Number(stats.orgsTotal)          || 0,
          sitesTotal:    Number(stats.sitesTotal)         || 0,
          usersTotal:    Number(stats.usersTotal)         || 0,
          speciesTotal:  Number(stats.globalSpeciesTotal) || 0,
          pendingGlobal: Number(stats.pendingGlobal)      || 0,
        }));
      } catch (err) { if (!cancelled) setData((d) => ({ ...d, error: err.message })); }
    }
    load();
    return () => { cancelled = true; };
  }, []);
  return data;
}

function PlatformDashboard() {
  const data = usePlatformData();
  const orgsLine = data.orgsTotal != null ? `${data.orgsTotal} organisation${data.orgsTotal === 1 ? '' : 's'}` : 'orgs loading';
  const sitesLine = data.sitesTotal != null ? `${data.sitesTotal} site${data.sitesTotal === 1 ? '' : 's'} provisioned` : '';
  const speciesLine = data.speciesTotal != null ? `${data.speciesTotal} species in the catalogue` : '';
  const summary = [orgsLine, sitesLine, speciesLine].filter(Boolean).join(' · ');
  const pending = data.pendingGlobal || 0;

  return (
    <div className="aq-content">
      <div className="aq-content-inner aq-dash-stagger" style={{ maxWidth: 1400 }}>
        {/* Hero changes shape with urgency, same rule as the org
            dashboard: pending review work promotes to an action card. */}
        {pending > 0 ? (
          <section className="aq-hero-card is-warn">
            <div className="aq-hero-card-row">
              <div className="aq-hero-card-body">
                <div className="aq-hero-card-title">
                  <span className="aq-hero-dot is-breathing" style={{ background: 'var(--aq-warn)', margin: 0, boxShadow: '0 0 14px var(--aq-warn)' }} />
                  {pending} species awaiting platform review
                </div>
                <div className="aq-hero-card-sub">
                  Submitted by member organisations for promotion to the global catalogue.
                </div>
              </div>
              <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
                <button type="button" className="aq-hero-card-cta" onClick={dashGo('#global-species')}>
                  Review queue <Icon name="chevron-right" size={13} />
                </button>
              </div>
            </div>
          </section>
        ) : (
          <section className="aq-hero">
            <h1>
              <span className="aq-hero-dot" style={{ background: 'var(--aq-success)', boxShadow: '0 0 14px var(--aq-success)' }} />
              {data.orgsTotal ?? '—'} organisation{data.orgsTotal === 1 ? '' : 's'} across the fleet
            </h1>
            <div className="aq-hero-meta"><span>Nothing pending — the platform is steady.</span></div>
          </section>
        )}

        <div className="aq-digest">
          <div className="aq-digest-head">
            <div className="aq-digest-icon"><Icon name="spark" size={12} /></div>
            <div className="aq-digest-title">Fleet Health Digest</div>
            <div className="aq-digest-time">live · {todayLong()}</div>
          </div>
          <div className="aq-digest-body">
            {summary || 'Loading platform totals…'}. {pending > 0
              ? `${pending} species awaiting platform review for global promotion.`
              : 'No emergency broadcasts, connector errors, or pending global species.'}
          </div>
        </div>

        <section>
          <div className="aq-stats">
            <DashKpi label="Organisations" value={data.orgsTotal}
              sub={`${data.orgsTotal ?? 0} active`} href="#orgs" />
            <DashKpi label="Global species" value={data.speciesTotal}
              sub={pending > 0 ? `${pending} to review` : 'all caught up'}
              subTone={pending > 0 ? 'warn' : null}
              href="#global-species" />
            <DashKpi label="Users" value={data.usersTotal}
              sub={`across ${data.orgsTotal ?? 0} organisation${data.orgsTotal === 1 ? '' : 's'}`}
              href="#users" />
            <DashKpi label="Total sites" value={data.sitesTotal}
              sub="aquarium locations worldwide" />
          </div>
        </section>

        <section className="aq-section">
          <PlatformOrgsCard />
        </section>

        {data.error && <DashError error={data.error} />}
      </div>
    </div>
  );
}

function PlatformOrgsCard() {
  const [orgs, setOrgs] = useState([]);
  useEffect(() => {
    apiFetch('/api/auth/platform/organisations')
      .then((r) => setOrgs((r.organisations || []).slice(0, 6)))
      .catch(() => {});
  }, []);
  return (
    <div className="aq-card">
      <div className="aq-section-head" style={{ marginBottom: 6 }}>
        <div className="aq-section-title">
          <Icon name="building" />Organisations Overview
        </div>
        <a href="#orgs" onClick={dashGo('#orgs')} className="aq-section-link">
          View all <Icon name="chevron-right" size={11} />
        </a>
      </div>
      {orgs.length === 0 && (
        <div style={{ padding: '28px 16px', textAlign: 'center' }}>
          <div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--aq-text-faint)', marginBottom: 10, fontSize: 18 }}>
            <Icon name="building" size={20} />
          </div>
          <div style={{ fontWeight: 500, fontSize: 13, color: 'var(--aq-text-dim)', marginBottom: 4 }}>No organisations yet</div>
          <div style={{ fontSize: 12, color: 'var(--aq-text-faint)', lineHeight: 1.5 }}>Create your first organisation to start onboarding sites and users.</div>
        </div>
      )}
      {orgs.map((o) => (
        <div key={o.id} className="aq-list-row" onClick={dashGo('#orgs')} style={{ cursor: 'pointer' }}>
          <div className="aq-list-mark">{(o.name || '?').slice(0, 1).toUpperCase()}</div>
          <div className="aq-list-body">
            <div className="aq-list-name">{o.name}</div>
            <div className="aq-list-meta">
              {o.site_count || 0} site{o.site_count === 1 ? '' : 's'} · {o.user_count || 0} users · {o.species_count || 0} species
            </div>
          </div>
          <div className="aq-list-action">Enter <Icon name="chevron-right" size={11} /></div>
        </div>
      ))}
    </div>
  );
}

/* ── Operator dashboard — read-only, gets the live fleet ───────────── */

function OperatorDashboard() {
  const me = Auth.getUser() || {};
  const siteName = (me.sites && me.sites[0] && me.sites[0].name) || 'your site';
  const [screens, setScreens] = useState(null);
  useEffect(() => {
    apiFetchSiteScoped('/api/screens')
      .then((r) => setScreens(r.screens || []))
      .catch(() => setScreens([]));
  }, []);
  const list = screens || [];
  const onlineCount = list.filter(screenIsOnline).length;

  return (
    <div className="aq-content">
      <div className="aq-content-inner aq-dash-stagger" style={{ maxWidth: 1400 }}>
        <section className="aq-hero">
          <DashEyebrow label={siteName} />
          <h1>
            <span className="aq-hero-dot" style={{
              background: list.length > 0 && onlineCount < list.length ? 'var(--aq-warn)' : 'var(--aq-success)',
              boxShadow: `0 0 14px ${list.length > 0 && onlineCount < list.length ? 'var(--aq-warn)' : 'var(--aq-success)'}`,
            }} />
            {screens == null ? 'Checking screens…' : `${onlineCount}/${list.length} screens online`}
          </h1>
          <div className="aq-hero-meta">
            <span>Read-only access</span>
            <span className="aq-sep">·</span>
            <span>Browse screens and the species library</span>
          </div>
        </section>

        <DashFleet screens={screens} />

        <section className="aq-section">
          <div className="aq-grid-2">
            <DashLinkCard icon="monitor" title="Screens" href="#displays"
              body="Live status, last heartbeat, and active species pool for every screen on site." />
            <DashLinkCard icon="library" title="Species library" href="#library"
              body="Read-only species catalogue. Use the editor's AI tools to suggest copy fixes — saves still need an editor's approval." />
          </div>
        </section>
      </div>
    </div>
  );
}

/* ── Designer / marketing landings ─────────────────────────────────── */

function DesignerDashboard() {
  const me = Auth.getUser() || {};
  return (
    <div className="aq-content">
      <div className="aq-content-inner aq-dash-stagger" style={{ maxWidth: 1400 }}>
        <section className="aq-hero">
          <DashEyebrow label={me.name || 'Designer'} />
          <h1>
            <span className="aq-hero-dot" style={{ background: 'var(--aq-accent)', boxShadow: '0 0 14px var(--aq-accent)' }} />
            Ready to design
          </h1>
          <div className="aq-hero-meta">
            <span>Campaigns + media at your fingertips</span>
          </div>
        </section>
        <section className="aq-section">
          <div className="aq-grid-2">
            <DashLinkCard icon="spark" title="Campaigns" href="#campaigns"
              body="Edit creative blocks, suggest motion clips, regenerate copy with AI." />
            <DashLinkCard icon="library" title="Media library" href="#media"
              body="Upload and tag photos and clips — used as backgrounds and overlays." />
          </div>
        </section>
      </div>
    </div>
  );
}

function MarketingDashboard() {
  /* Marketing admins want the org-shape dashboard so they see live
     campaigns + screen health alongside their templates and media. */
  return <OrgDashboard />;
}

function DashLinkCard({ icon, title, body, href }) {
  return (
    <a className="aq-card" href={href} onClick={dashGo(href)}
      style={{ display: 'block', textDecoration: 'none', color: 'inherit', cursor: 'pointer' }}>
      <div className="aq-section-head" style={{ marginBottom: 6 }}>
        <div className="aq-section-title"><Icon name={icon} />{title}</div>
        <span className="aq-section-link">Open <Icon name="chevron-right" size={11} /></span>
      </div>
      <div style={{ fontSize: 13, color: 'var(--aq-text-muted)' }}>{body}</div>
    </a>
  );
}

/* ── Top-level switch — picks the right variant per role. ─────────── */

function Dashboard() {
  const u = Auth.getUser() || {};
  const role = u.role;
  if (role === 'aquaos_admin') return <PlatformDashboard />;
  if (role === 'operator')     return <OperatorDashboard />;
  if (role === 'designer')     return <DesignerDashboard />;
  if (role === 'marketing_admin') return <MarketingDashboard />;
  /* org_admin, site_manager, legacy admin — same shape, role-scoped data. */
  return <OrgDashboard />;
}

window.Dashboard = Dashboard;
