/* Larm: Incidenter (redesign 2026-09).
 *
 * Bara läsning. Källor:
 *   GET /v1/admin/alerts/catalog            src/routes/admin.alerts.ts (regler, 30-dagarsstatistik, kanaler)
 *   GET /v1/admin/alerts/history?limit=500  src/routes/admin.alerts.ts (utskicksloggen, alert_log)
 *   GET /v1/admin/alerts/history?type=X     samma, per larmtyp (loggen ger högst 500 rader per anrop)
 *   GET /v1/admin/opter/deliveries?overdue=true&limit=1  src/routes/admin.opter.ts (summary)
 *
 * Hur larm skickas (src/lib/alerting.ts, src/jobs/alertSweeps.ts):
 *   - Spärrtiden gäller per larmnyckel (order, artikel, kund), inte per typ.
 *   - Ändrat innehåll (payload) skickar om larmet även inom spärrtiden.
 *   - Typer som saknas i alert_config får 60 min spärrtid och kan inte stängas av.
 *   - Allvarlighet styr bara symbolen i Slack; allt skickas direkt.
 *   - Bara atp_risk markeras som åtgärdad (resolveAlert). sweepStaleAlerts körs inte.
 */

const RX_ALR_LABELS = {
  missed_delivery:      'Missat leveransdatum',
  unbilled_delivery:    'Levererat utan faktura',
  atp_risk:             'Leveransrisk i lager',
  dso_overdue:          'Lång kredittid hos kund',
  sync_fail:            'Uppdatering misslyckades',
  admin_lockout:        'Adminkonto låst',
  ip_flood:             'Många misslyckade inloggningar',
  big_order:            'Stor order',
  inventory_fall:       'Lagervärdet har fallit',
  new_lead:             'Ny lead',
  customer_forecast:    'Ny kundprognos',
  market_sentiment_neg: 'Negativ marknadspuls',
};

// Vad som utlöser respektive typ, enligt koden.
const RX_ALR_TRIGGER = {
  missed_delivery:      'En gång per dygn: ordrar vars utlovade leveransdatum passerat utan leverans från Opter, högst 30 dagar bakåt.',
  unbilled_delivery:    'En gång per dygn: ordrar levererade för mer än 48 timmar sedan utan lyckad fakturaöverföring till Fortnox.',
  atp_risk:             'En gång per dygn: artiklar där lagda ordrar inom 14 dagar överstiger saldo och inleveranser.',
  dso_overdue:          'Måndagar: kunder med genomsnittlig betaltid över 45 dagar senaste 180 dagarna.',
  inventory_fall:       'Måndagar: lagervärdet har fallit mer än 10 procent jämfört med förra veckan.',
  sync_fail:            'När uppdateringen från Shopify eller Fortnox misslyckas.',
  big_order:            'När en order över 500 tkr kommer in från Shopify.',
  customer_forecast:    'När en kund lämnar en prognos i partnerportalen.',
  market_sentiment_neg: 'När en kund anger negativ marknadspuls i partnerportalen.',
};

// Typer i regelkonfigurationen som inget i koden skickar via larmsystemet.
const RX_ALR_NO_SENDER = {
  admin_lockout: 'Inloggningslarm skickas direkt till Slack utanför larmsystemet, så regeln påverkar inget.',
  ip_flood:      'Inloggningslarm skickas direkt till Slack utanför larmsystemet, så regeln påverkar inget.',
  new_lead:      'Inget i systemet skickar den här typen i dag.',
};

const RX_ALR_ROUTE = {
  missed_delivery:   { route: 'deliveries',        label: 'Öppna Leveranser' },
  unbilled_delivery: { route: 'finance/invoices',  label: 'Öppna Fakturering' },
  atp_risk:          { route: 'stock',             label: 'Öppna Leveransrisk' },
  dso_overdue:       { route: 'finance/reskontra', label: 'Öppna Reskontra' },
  sync_fail:         { route: 'system-status',     label: 'Öppna Uppdateringar' },
  inventory_fall:    { route: 'reports/inventory', label: 'Öppna Saldo' },
  big_order:         { route: 'orders',            label: 'Öppna Ordrar' },
};

const RX_ALR_SEV = {
  critical: { rank: 3, label: 'Kritisk', one: 'kritisk', many: 'kritiska', fg: '#FFFFFF', bg: '#A12C1E', border: '#EFC4BE' },
  warning:  { rank: 2, label: 'Varning', one: 'varning', many: 'varningar', fg: '#8A4B00', bg: '#FBEBD0', border: 'var(--rx-border)' },
  info:     { rank: 1, label: 'Info',    one: 'info',    many: 'info',      fg: '#45434E', bg: '#ECEAE4', border: 'var(--rx-border)' },
};

const RX_ALR_HISTORY_CAP = 500;
const RX_ALR_DEFAULT_COOLDOWN = 60; // src/lib/alerting.ts: cfg?.cooldown_minutes ?? 60

function rxAlrLabel(type, catalogLabel) {
  return RX_ALR_LABELS[type] || catalogLabel || type;
}

/** alert_log.fired_at är SQLite datetime('now'), alltså UTC utan zon. */
function rxAlrIso(v) {
  if (!v) return null;
  const s = String(v);
  if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) return s;
  return s.replace(' ', 'T') + 'Z';
}

function rxAlrMs(v) {
  const iso = rxAlrIso(v);
  const t = iso ? Date.parse(iso) : NaN;
  return isNaN(t) ? null : t;
}

function rxAlrWhen(v) {
  const iso = rxAlrIso(v);
  if (!iso) return '';
  return `${rxDay(iso, { relative: true })} ${rxTime(iso)}`;
}

function rxAlrLocalDay(v) {
  const t = rxAlrMs(v);
  if (t == null) return '';
  const d = new Date(t);
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

function rxAlrCooldown(min) {
  const m = Number(min) || 0;
  if (m <= 0) return 'ingen spärrtid';
  if (m % 10080 === 0) return m === 10080 ? '1 vecka' : `${m / 10080} veckor`;
  if (m % 1440 === 0) return m === 1440 ? '1 dygn' : `${m / 1440} dygn`;
  if (m % 60 === 0) return `${m / 60} h`;
  return `${m} min`;
}

/** Slack-markdown till läsbar text, utan emoji-koder, länkar och tankstreck. */
function rxAlrClean(s) {
  return String(s || '')
    .replace(/:[a-z0-9_+-]+:/g, '')
    .replace(/https?:\/\/\S+/g, '')
    .replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '')
    .replace(/PO\s*[\u2014\u2013]/g, 'PO saknas')
    .replace(/\s*[\u2014\u2013]\s*/g, ': ')
    .replace(/[*_`]/g, '')
    .replace(/^\s*>\s*/gm, '')
    .replace(/[ \t]+/g, ' ')
    .trim();
}

function rxAlrError(err) {
  const s = String(err || '');
  if (s === 'no_webhook_url_configured') return 'ingen webhook konfigurerad';
  const m = /^slack_(\d+)$/.exec(s);
  if (m) return `Slack svarade ${m[1]}`;
  return s || 'okänt fel';
}

function rxAlrMatch(text, re) {
  const m = re.exec(String(text || ''));
  return m ? m[1].trim() : null;
}

/** Vad ett enskilt utskick handlar om: order, artikel, kund eller rubrik. */
function rxAlrSubject(e) {
  const t = e.text || '';
  const keyTail = String(e.alertKey || '').split(':').slice(1).join(':');
  switch (e.type) {
    case 'missed_delivery':
    case 'unbilled_delivery':
    case 'big_order':
      return rxAlrMatch(t, /order `([^`]+)`/) || keyTail;
    case 'atp_risk': {
      const sku = rxAlrMatch(t, /ATP-risk\*[^`]*`([^`]+)`/) || keyTail;
      const title = rxAlrMatch(t, /`[^`]+` \(([^)\n]+)\)/);
      return title && title !== 'okänd' ? `${sku} (${title})` : sku;
    }
    case 'dso_overdue': {
      const name = rxAlrMatch(t, /Sen betalare\*\s*[\u2014\u2013-]\s*(.+?) \(`#/);
      const days = rxAlrMatch(t, /DSO[^*]*\*(\d+) dgr\*/);
      return name ? (days ? `${name} (${days} dagar)` : name) : keyTail;
    }
    default:
      return rxAlrClean(t.split('\n')[0]).slice(0, 90) || keyTail;
  }
}

function rxAlrRange(dates) {
  const ds = dates.filter(Boolean).sort();
  if (!ds.length) return null;
  const a = ds[0], b = ds[ds.length - 1];
  return a === b ? rxDay(a) : `${rxDay(a)} till ${rxDay(b)}`;
}

function rxAlrExamples(list, n = 3) {
  const ex = list.filter(Boolean).slice(0, n);
  return ex.length ? `Exempel: ${ex.join(', ')}.` : '';
}

/** Beskrivning av en incident, byggd av loggens innehåll. */
function rxAlrBody(g, opter) {
  const latest = g.latestByKey; // senaste utskicket per larmnyckel, nyast först
  const n = latest.length;
  const subjects = latest.map(rxAlrSubject);
  const parts = [];
  if (g.type === 'missed_delivery') {
    const range = rxAlrRange(latest.map(e => rxAlrMatch(e.text, /Utlovat: (\d{4}-\d{2}-\d{2})/)));
    parts.push(`${rxPlural(n, 'order', 'ordrar')} har passerat förväntat leveransdatum utan rapporterad leverans${range ? ` (förväntat ${range})` : ''}.`);
    const byStatus = {};
    latest.forEach(e => {
      const s = rxAlrMatch(e.text, /Status: \*([^*]+)\*/);
      if (s) byStatus[s] = (byStatus[s] || 0) + 1;
    });
    const st = Object.entries(byStatus).sort((a, b) => b[1] - a[1]);
    if (st.length) parts.push(`Status i senaste utskicket: ${st.map(([s, c]) => `${s.toLowerCase()} ${fmtInt(c)}`).join(', ')}.`);
    if (opter && opter.overdue > 0) {
      const share = opter.awaitingStatus / opter.overdue;
      if (share >= 0.8) {
        parts.push(`Enligt Opter-datan inväntar ${fmtInt(opter.awaitingStatus)} av ${fmtInt(opter.overdue)} ordrar med passerat datum fortfarande status. Larmet speglar därför främst saknad status från Opter, inte nödvändigtvis sena leveranser.`);
      }
    }
  } else if (g.type === 'unbilled_delivery') {
    const range = rxAlrRange(latest.map(e => rxAlrMatch(e.text, /Levererad: (\d{4}-\d{2}-\d{2})/)));
    parts.push(`${rxPlural(n, 'order', 'ordrar')} levererades för mer än 48 timmar sedan utan lyckad fakturaöverföring till Fortnox${range ? ` (levererade ${range})` : ''}.`);
  } else if (g.type === 'atp_risk') {
    parts.push(`${rxPlural(n, 'artikel', 'artiklar')} räcker inte till lagda ordrar inom 14 dagar.`);
  } else if (g.type === 'dso_overdue') {
    parts.push(`${rxPlural(n, 'kund', 'kunder')} har genomsnittlig betaltid över 45 dagar senaste 180 dagarna.`);
  } else if (g.type === 'sync_fail') {
    parts.push(`${rxPlural(g.entries.length, 'misslyckad uppdatering', 'misslyckade uppdateringar')} rapporterades.`);
  } else {
    parts.push(`${rxPlural(n, 'ärende', 'olika ärenden')} i gruppen.`);
  }
  const ex = rxAlrExamples(subjects);
  if (ex) parts.push(ex);
  return parts.join(' ');
}

/** Hämtar regler, logg per larmtyp och Opter-summering. Bara GET. */
function rxAlrUseData() {
  const [state, setState] = React.useState({ loading: true, error: null, catalog: null, entries: [], capped: {} });
  const [nonce, setNonce] = React.useState(0);
  React.useEffect(() => {
    let cancelled = false;
    setState(s => ({ ...s, loading: true, error: null }));
    (async () => {
      try {
        const [catalog, recent] = await Promise.all([
          apiFetch('/v1/admin/alerts/catalog'),
          apiFetch(`/v1/admin/alerts/history?limit=${RX_ALR_HISTORY_CAP}`),
        ]);
        const known = new Set((catalog.alerts || []).map(a => a.type));
        const types = new Set();
        (catalog.alerts || []).forEach(a => { if (a.last30dFired > 0) types.add(a.type); });
        (recent.entries || []).forEach(e => { if (!known.has(e.type)) types.add(e.type); });
        const perType = await Promise.all([...types].map(t =>
          apiFetch(`/v1/admin/alerts/history?type=${encodeURIComponent(t)}&limit=${RX_ALR_HISTORY_CAP}`)
            .then(r => ({ type: t, entries: r.entries || [] }))));
        const byId = new Map();
        (recent.entries || []).forEach(e => byId.set(e.id, e));
        const capped = {};
        perType.forEach(p => {
          p.entries.forEach(e => byId.set(e.id, e));
          if (p.entries.length >= RX_ALR_HISTORY_CAP) {
            capped[p.type] = rxAlrMs(p.entries[p.entries.length - 1].firedAt);
          }
        });
        const entries = [...byId.values()].sort((a, b) => (rxAlrMs(b.firedAt) || 0) - (rxAlrMs(a.firedAt) || 0));
        if (!cancelled) setState({ loading: false, error: null, catalog, entries, capped });
      } catch (error) {
        if (!cancelled) setState({ loading: false, error, catalog: null, entries: [], capped: {} });
      }
    })();
    return () => { cancelled = true; };
  }, [nonce]);
  return { ...state, reload: () => setNonce(n => n + 1) };
}

function RxAlrSevBadge({ sev }) {
  const s = RX_ALR_SEV[sev] || RX_ALR_SEV.info;
  return <span style={{ flexShrink: 0, width: 72, textAlign: 'center', fontSize: 11, fontWeight: 700, letterSpacing: '.04em', textTransform: 'uppercase', color: s.fg, background: s.bg, borderRadius: 6, padding: '5px 0' }}>{s.label}</span>;
}

function RxAlrIncident({ g, opter, onNavigate }) {
  const [open, setOpen] = React.useState(false);
  const sev = RX_ALR_SEV[g.severity] || RX_ALR_SEV.info;
  const link = RX_ALR_ROUTE[g.type];
  const sevMix = Object.entries(g.sevCounts).filter(([, c]) => c > 0)
    .sort((a, b) => (RX_ALR_SEV[b[0]]?.rank || 0) - (RX_ALR_SEV[a[0]]?.rank || 0));
  const days = Object.entries(g.byDay).sort((a, b) => (a[0] < b[0] ? 1 : -1));
  const failed = g.entries.filter(e => !e.ok).length;
  return <article style={{ background: 'var(--rx-surface)', border: `1px solid ${sev.border}`, borderRadius: 12, padding: '16px 18px', display: 'flex', gap: 14, alignItems: 'flex-start' }}>
    <RxAlrSevBadge sev={g.severity} />
    <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
        <h3 style={{ fontSize: 15, fontWeight: 600, color: 'var(--rx-ink)', margin: 0 }}>{g.label}</h3>
        <span className="rx-mono rx-faint" style={{ fontSize: 11.5 }}>{g.type}</span>
        {!g.configured && <RxPill tone="warn" title="Typen finns inte i regelkonfigurationen">Saknar regel</RxPill>}
      </div>
      <p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.5, color: 'var(--rx-ink-2)' }}>{rxAlrBody(g, opter)}</p>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 16px', fontSize: 12.5, color: 'var(--rx-ink-3)' }}>
        <span><strong style={{ color: 'var(--rx-ink)', fontWeight: 600 }}>{g.capped ? 'Minst ' : ''}{fmtInt(g.entries.length)}</strong> utskick grupperade till en incident</span>
        {sevMix.length > 1 && <span>{sevMix.map(([k, c]) => `${fmtInt(c)} ${c === 1 ? RX_ALR_SEV[k]?.one : RX_ALR_SEV[k]?.many}`).join(', ')}</span>}
        <span>Slack-kanal {g.channels.join(', ') || 'okänd'}{g.fallback ? ' (via standardwebhooken)' : ''}</span>
        <span>Senast utlöst {rxAlrWhen(g.lastAt)}</span>
        {g.activeUnresolved > 0 && <span title="Antal ärenden som systemet fortfarande räknar som aktiva. Bara Leveransrisk markeras som åtgärdad automatiskt.">{fmtInt(g.activeUnresolved)} aktiva i systemet</span>}
        {failed > 0 && <span style={{ color: 'var(--rx-danger)' }}>{rxPlural(failed, 'misslyckat utskick', 'misslyckade utskick')}</span>}
      </div>
      {days.length > 1 && <div>
        <button type="button" className="rx-link" aria-expanded={open} onClick={() => setOpen(o => !o)} style={{ fontSize: 12.5 }}>
          {open ? 'Dölj utskick per dag' : 'Visa utskick per dag'}
        </button>
        {open && <ul style={{ listStyle: 'none', margin: '6px 0 0', padding: 0, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {days.map(([d, c]) => <li key={d}><RxPill>{rxDay(d, { relative: true })}: {fmtInt(c)}</RxPill></li>)}
        </ul>}
      </div>}
    </div>
    {link && <button type="button" className="btn" onClick={() => onNavigate && onNavigate(link.route)} style={{ flexShrink: 0 }}>{link.label}</button>}
  </article>;
}

function RxAlrSide({ title, tone, children }) {
  return <RxPanel title={title} tone={tone}>{children}</RxPanel>;
}

function RxAlertsPage({ onNavigate }) {
  const { loading, error, catalog, entries, capped, reload } = rxAlrUseData();
  const opterRes = useRxFetch('/v1/admin/opter/deliveries?overdue=true&limit=1');
  const opter = opterRes.data?.summary || null;

  const model = React.useMemo(() => {
    if (!catalog) return null;
    const now = Date.now();
    const d1 = now - 864e5, d7 = now - 7 * 864e5, d30 = now - 30 * 864e5;
    const rules = catalog.alerts || [];
    const ruleBy = new Map(rules.map(r => [r.type, r]));
    const chanOk = new Map((catalog.channels || []).map(c => [c.channel, c.configured]));

    // Utskick senaste 24 h, från loggen.
    const last24 = entries.filter(e => (rxAlrMs(e.firedAt) || 0) >= d1);
    const cap24 = Object.values(capped).some(t => t != null && t >= d1);

    // 30 dagar: regelstatistiken för kända typer, loggen för typer utan regel.
    const unknownTypes = [...new Set(entries.map(e => e.type))].filter(t => !ruleBy.has(t));
    let sent30 = rules.reduce((s, r) => s + (r.last30dFired || 0), 0);
    let failed30 = rules.reduce((s, r) => s + Math.max(0, (r.last30dFired || 0) - (r.last30dOk || 0)), 0);
    let cap30 = false;
    const unknown = unknownTypes.map(t => {
      const list = entries.filter(e => e.type === t && (rxAlrMs(e.firedAt) || 0) >= d30);
      const isCapped = capped[t] != null && capped[t] >= d30;
      if (isCapped) cap30 = true;
      sent30 += list.length;
      failed30 += list.filter(e => !e.ok).length;
      const sevs = {};
      list.forEach(e => { sevs[e.severity] = (sevs[e.severity] || 0) + 1; });
      return { type: t, count: list.length, capped: isCapped, sevs, channel: list[0]?.channel || null, lastAt: list[0]?.firedAt || null };
    });
    const errCounts = {};
    entries.filter(e => !e.ok && (rxAlrMs(e.firedAt) || 0) >= d30).forEach(e => {
      const k = e.error || 'okänt fel';
      errCounts[k] = (errCounts[k] || 0) + 1;
    });
    const topErr = Object.entries(errCounts).sort((a, b) => b[1] - a[1])[0] || null;

    // Incidenter: en grupp per larmtyp med utskick senaste 7 dagarna. Testutskick räknas inte.
    const groupsBy = new Map();
    entries.forEach(e => {
      const t = rxAlrMs(e.firedAt) || 0;
      if (t < d7) return;
      if (String(e.alertKey || '').startsWith('test:')) return;
      let g = groupsBy.get(e.type);
      if (!g) {
        const r = ruleBy.get(e.type);
        g = { type: e.type, label: rxAlrLabel(e.type, r?.label), configured: !!r, activeUnresolved: r?.activeUnresolved || 0,
          entries: [], sevCounts: {}, byDay: {}, keys: new Map(), channelSet: new Set(), lastAt: e.firedAt };
        groupsBy.set(e.type, g);
      }
      g.entries.push(e);
      g.sevCounts[e.severity] = (g.sevCounts[e.severity] || 0) + 1;
      const day = rxAlrLocalDay(e.firedAt);
      g.byDay[day] = (g.byDay[day] || 0) + 1;
      if (!g.keys.has(e.alertKey)) g.keys.set(e.alertKey, e); // entries är nyast först
      if (e.channel) g.channelSet.add(e.channel);
    });
    const groups = [...groupsBy.values()].map(g => {
      const severity = Object.keys(g.sevCounts).sort((a, b) => (RX_ALR_SEV[b]?.rank || 0) - (RX_ALR_SEV[a]?.rank || 0))[0] || 'info';
      const channels = [...g.channelSet];
      return { ...g, severity, channels, latestByKey: [...g.keys.values()],
        fallback: channels.some(c => chanOk.get(c) === false),
        capped: capped[g.type] != null && capped[g.type] >= d7 };
    }).sort((a, b) => ((RX_ALR_SEV[b.severity]?.rank || 0) - (RX_ALR_SEV[a.severity]?.rank || 0))
      || ((rxAlrMs(b.lastAt) || 0) - (rxAlrMs(a.lastAt) || 0)));

    // Förslagsunderlag: hur många meddelanden senaste 24 h om varje typ gav ett per dygn.
    const grouped24 = new Set(last24.filter(e => !String(e.alertKey || '').startsWith('test:')).map(e => `${e.type}|${rxAlrLocalDay(e.firedAt)}`)).size;

    const quiet = rules.filter(r => !r.last30dFired);
    return { rules, groups, unknown, quiet, last24, cap24, sent30, cap30, failed30, topErr, grouped24,
      types24: new Set(last24.map(e => e.type)).size };
  }, [catalog, entries, capped]);

  if (loading && !catalog) return <RxLoading label="Hämtar larm…" />;
  if (error) return <RxError error={error} onRetry={reload} />;
  if (!model) return null;

  const { rules, groups, unknown, quiet, last24, cap24, sent30, cap30, failed30, topErr, grouped24, types24 } = model;
  const hasCritical = groups.some(g => g.severity === 'critical');

  return <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
    <div className="rx-grid rx-grid--4">
      <RxKpi label="Incidenter" value={fmtInt(groups.length)} tone={hasCritical ? 'warn' : undefined}
        sub="larmtyper med utskick senaste 7 dagarna" />
      <RxKpi label="Utskick senaste 24 h" value={`${cap24 ? 'minst ' : ''}${fmtInt(last24.length)}`}
        sub={last24.length ? `fördelat på ${rxPlural(types24, 'larmtyp', 'larmtyper')}` : 'inga utskick'} />
      <RxKpi label="Utskick senaste 30 dagarna" value={`${cap30 ? 'minst ' : ''}${fmtInt(sent30)}`}
        sub={cap30 ? 'loggen visar högst 500 per larmtyp' : 'alla larmtyper, inklusive testutskick'} />
      <RxKpi label="Misslyckade utskick" value={fmtInt(failed30)} tone={failed30 > 0 ? 'warn' : 'ok'}
        sub={failed30 > 0 && topErr ? `30 dagar, vanligast: ${rxAlrError(topErr[0])}` : 'senaste 30 dagarna'} />
    </div>

    <div className="rx-split">
      <div className="rx-split__main">
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
          <h2 className="rx-section-title" style={{ margin: 0 }}>Incidenter senaste 7 dagarna</h2>
          <span className="rx-panel__sub">Utskicken grupperade per larmtyp här i admin. I Slack skickas de fortfarande ett och ett.</span>
          <div style={{ marginLeft: 'auto' }}>
            <button type="button" className="rx-link" onClick={reload} disabled={loading}>{loading ? 'Hämtar…' : 'Uppdatera'}</button>
          </div>
        </div>
        {opterRes.error && <RxCallout title="Opter-summeringen kunde inte hämtas.">Beskrivningen av Missat leveransdatum visas utan jämförelse mot Opter.</RxCallout>}
        {groups.length === 0
          ? <RxPanel><RxEmpty title="Inga larm senaste 7 dagarna">Utskicksloggen har inga larm från den senaste veckan.</RxEmpty></RxPanel>
          : groups.map(g => <RxAlrIncident key={g.type} g={g} opter={opter} onNavigate={onNavigate} />)}
      </div>

      <aside className="rx-split__side" aria-label="Om larmen">
        <RxAlrSide title="Så skickas larm i dag">
          <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13, lineHeight: 1.5, color: 'var(--rx-ink-2)', display: 'flex', flexDirection: 'column', gap: 4 }}>
            <li>Alla larm skickas direkt till Slack, ett meddelande per order, artikel eller kund. Allvarligheten styr bara symbolen i meddelandet.</li>
            <li>Spärrtiden gäller per ärende, inte per larmtyp. Ändras innehållet, till exempel status eller antal dagar sen, skickas larmet igen trots spärrtid.</li>
            <li>Bara Leveransrisk markeras som åtgärdad när villkoret upphör. Övriga typer räknas som aktiva tills vidare.</li>
          </ul>
          <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column' }}>
            {rules.map(r => <li key={r.type} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '8px 0', borderTop: '1px solid var(--rx-divider)' }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: r.enabled ? 'var(--rx-ink)' : 'var(--rx-ink-3)' }}>{rxAlrLabel(r.type, r.label)}</div>
                <div style={{ fontSize: 12, color: 'var(--rx-ink-3)' }}>
                  Kanal {r.channel}{r.channelConfigured ? '' : ' (standardwebhook)'}, spärrtid {rxAlrCooldown(r.cooldownMinutes)}
                </div>
                <div className="rx-mono rx-faint" style={{ fontSize: 11 }}>{r.type}</div>
              </div>
              {r.enabled ? <RxPill tone="ok">Aktiv</RxPill> : <RxPill>Avstängd</RxPill>}
            </li>)}
          </ul>
          <button type="button" className="rx-link" style={{ alignSelf: 'flex-start' }} onClick={() => onNavigate && onNavigate('alerts/regler')}>Ändra i Regler och utskick</button>
        </RxAlrSide>

        {unknown.length > 0 && <RxAlrSide title="Larmtyper utan regel" tone="warn">
          <p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: 'var(--rx-ink-2)' }}>
            De här typerna finns i utskicksloggen men saknas i regelkonfigurationen. De kan inte stängas av eller få egen spärrtid här, och skickas med standardspärrtiden {RX_ALR_DEFAULT_COOLDOWN} minuter.
          </p>
          {unknown.map(u => {
            const sev = Object.entries(u.sevs).sort((a, b) => b[1] - a[1])[0];
            return <div key={u.type} style={{ borderTop: '1px solid var(--rx-divider)', paddingTop: 10, display: 'flex', flexDirection: 'column', gap: 4 }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                <span style={{ fontSize: 13, fontWeight: 600 }}>{rxAlrLabel(u.type)}</span>
                <span className="rx-mono rx-faint" style={{ fontSize: 11 }}>{u.type}</span>
              </div>
              <div style={{ fontSize: 12.5, color: 'var(--rx-ink-3)' }}>
                {u.capped ? 'Minst ' : ''}{rxPlural(u.count, 'utskick', 'utskick')} senaste 30 dagarna
                {sev ? `, oftast som ${RX_ALR_SEV[sev[0]]?.one || sev[0]}` : ''}
                {u.channel ? `, kanal ${u.channel}` : ''}.
              </div>
              {RX_ALR_TRIGGER[u.type] && <div style={{ fontSize: 12.5, color: 'var(--rx-ink-3)' }}>{RX_ALR_TRIGGER[u.type]}</div>}
              {u.type === 'missed_delivery' && <div style={{ fontSize: 12.5, color: 'var(--rx-ink-2)' }}>
                Svepet körs en gång per dygn, så 60 minuters spärrtid hindrar aldrig nästa dags utskick. Antal dagar sen ingår i innehållet och ändras varje dag, vilket också bryter spärrtiden.
              </div>}
            </div>;
          })}
        </RxAlrSide>}

        <RxAlrSide title="Tysta regler">
          {quiet.length === 0
            ? <p style={{ margin: 0, fontSize: 13, color: 'var(--rx-ink-3)' }}>Alla regler har utlösts minst en gång senaste 30 dagarna.</p>
            : <>
              <p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: 'var(--rx-ink-2)' }}>
                {fmtInt(quiet.length)} av {rxPlural(rules.length, 'regel', 'regler')} har inga utskick senaste 30 dagarna.
              </p>
              <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
                {quiet.map(r => <li key={r.type} style={{ fontSize: 12.5, color: 'var(--rx-ink-3)', lineHeight: 1.45 }}>
                  <span style={{ fontWeight: 600, color: 'var(--rx-ink)' }}>{rxAlrLabel(r.type, r.label)}</span>
                  {!r.enabled && ' (avstängd)'}
                  <div>{RX_ALR_NO_SENDER[r.type] || RX_ALR_TRIGGER[r.type] || ''}</div>
                </li>)}
              </ul>
            </>}
        </RxAlrSide>

        <section className="rx-panel" style={{ borderStyle: 'dashed', background: 'var(--rx-surface-alt)' }} aria-label="Förslag, inte byggt">
          <div className="rx-panel__head">
            <h2 className="rx-panel__title">Förslag</h2>
            <RxPill tone="info">Inte byggt</RxPill>
          </div>
          <p style={{ margin: 0, fontSize: 12.5, color: 'var(--rx-ink-3)' }}>Förbättringar som systemet inte gör i dag.</p>
          <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13, lineHeight: 1.5, color: 'var(--rx-ink-2)', display: 'flex', flexDirection: 'column', gap: 6 }}>
            <li>
              Gruppera per incident i Slack: ett meddelande per larmtyp och dygn, med antal och exempel.
              {last24.length > grouped24 && grouped24 > 0 && <> Senaste 24 timmarna hade det blivit {rxPlural(grouped24, 'meddelande', 'meddelanden')} i stället för {fmtInt(last24.length)}.</>}
            </li>
            <li>Dagligt sammandrag för varningar, så att bara kritiska larm skickas direkt.</li>
            {unknown.some(u => u.type === 'missed_delivery') && <li>Lägg till Missat leveransdatum som regel, så att den kan stängas av och få spärrtid tills Opter-kopplingen är klar.</li>}
            <li>Markera larm som åtgärdade när villkoret upphör, även för Levererat utan faktura.</li>
          </ul>
        </section>
      </aside>
    </div>
  </div>;
}
