import { FastifyInstance } from 'fastify';
import { prisma } from '../db/client';
import { flushProductStats } from '../stats/productStats';
import { countryLabel, formatUserAgent } from '../geo';
import { getRedis, REDIS_KEYS } from '../redis';
import { getSessionWithMessages, listOpenSessions } from '../support/service';

export async function adminRoutes(app: FastifyInstance): Promise<void> {
  const CSS = `
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f0f0f0;color:#222;font-size:14px}
    .topbar{background:#222;color:#fff;padding:14px 24px;display:flex;align-items:center;gap:16px}
    .topbar h1{font-size:16px;font-weight:700;letter-spacing:-.2px}
    .topbar a{color:#aaa;text-decoration:none;font-size:13px}
    .topbar a:hover{color:#fff}
    .container{max-width:1100px;margin:24px auto;padding:0 16px}
    .card{background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.1);overflow:hidden;margin-bottom:20px}
    .card-header{padding:16px 20px;border-bottom:1px solid #eee;display:flex;align-items:center;justify-content:space-between}
    .card-header h2{font-size:15px;font-weight:700}
    table{width:100%;border-collapse:collapse}
    th{text-align:left;padding:10px 16px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:#888;background:#fafafa;border-bottom:1px solid #eee}
    td{padding:12px 16px;border-bottom:1px solid #f5f5f5;vertical-align:top}
    tr:last-child td{border:none}
    .badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
    .badge-new{background:#fef3c7;color:#b45309}
    .badge-done{background:#d1fae5;color:#065f46}
    .badge-paid{background:#d1fae5;color:#065f46}
    .badge-cancelled{background:#fee2e2;color:#991b1b}
    .badge-failed{background:#fee2e2;color:#991b1b}
    .badge-processing{background:#dbeafe;color:#1e40af}
    a.detail-link{color:#222;text-decoration:none;font-weight:600}
    a.detail-link:hover{text-decoration:underline}
    .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:20px}
    .stat{background:#fff;border-radius:8px;padding:20px;box-shadow:0 1px 4px rgba(0,0,0,.1);text-align:center}
    .stat-val{font-size:32px;font-weight:700;line-height:1.1}
    .stat-lbl{font-size:12px;color:#888;margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
    .items-list{list-style:none;padding:0;font-size:13px}
    .items-list li{padding:4px 0;border-bottom:1px solid #f0f0f0}
    .items-list li:last-child{border:none}
    .back{display:inline-block;margin-bottom:16px;color:#888;text-decoration:none;font-size:13px}
    .back:hover{color:#222}
    .muted{color:#888;font-size:12px}
    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word}
    .pill{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:999px;background:#f3f4f6;color:#374151;font-size:12px;margin:2px 4px 2px 0}
    .event-list{display:flex;flex-direction:column;gap:4px}
    .event-row{display:flex;gap:8px;align-items:flex-start}
    .event-time{color:#888;font-size:11px;min-width:92px}
    .event-name{font-weight:700;font-size:12px}
    .event-detail{font-size:12px;color:#555}
    .url{max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .security-row code{font-size:11px}
  `;

  // ---- Order list ----
  app.get('/', async (_req, reply) => {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const day = new Date().toISOString().slice(0, 10);
    const [todayVisitors, todayOrders, todayRevenue, orders, blockedCount, blockedSamples] = await Promise.all([
      prisma.visitor.count({ where: { createdAt: { gte: today } } }),
      prisma.order.count({ where: { createdAt: { gte: today } } }),
      prisma.order.aggregate({
        where: { createdAt: { gte: today }, status: { not: 'cancelled' } },
        _sum: { total: true },
      }),
      prisma.order.findMany({
        orderBy: { createdAt: 'desc' },
        take: 100,
        include: { _count: { select: { items: true } } },
      }),
      getRedis().get(REDIS_KEYS.securityBlockedCount(day)),
      getRedis().lrange(REDIS_KEYS.securityBlockedSamples(day), 0, 9),
    ]);

    const rows = orders
      .map(
        (o) =>
          `<tr>
          <td><a class="detail-link" href="/_admin/orders/${o.id}">#${o.id}</a></td>
          <td>${esc(o.name)}</td>
          <td>${esc(o.phone)}</td>
          <td>${esc(o.city)}</td>
          <td>${o._count.items} шт.</td>
          <td><b>${o.total.toFixed(2)}</b> ${o.currency}</td>
          <td>${esc(o.paymentMethod)}</td>
          <td><code style="font-size:11px">${esc(o.reference)}</code></td>
          <td><span class="badge badge-${o.status}">${o.status}</span></td>
          <td>${fmtDate(o.createdAt)}</td>
        </tr>`,
      )
      .join('');
    const securityRows = blockedSamples
      .map(parseSecuritySample)
      .filter((sample): sample is SecuritySample => sample !== null)
      .map(
        (sample) =>
          `<tr class="security-row">
            <td>${fmtDate(new Date(sample.at))}</td>
            <td><code>${esc(sample.ip)}</code></td>
            <td>${esc(sample.method)}</td>
            <td><code>${esc(sample.path)}</code></td>
            <td><span class="muted">${esc(sample.ua || '—')}</span></td>
          </tr>`,
      )
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Admin – Заказы</title>
<style>${CSS}</style></head><body>
<div class="topbar">
  <h1>Sinsay Proxy Admin</h1>
  <a href="/_admin/">Заказы</a>
  <a href="/_admin/visitors">Визиторы</a>
  <a href="/_admin/products">Товары</a>
  <a href="/analytics">Analytics</a>
</div>
<div class="container">
  <div class="stat-grid">
    <div class="stat"><div class="stat-val">${todayVisitors}</div><div class="stat-lbl">Визиторов сегодня</div></div>
    <div class="stat"><div class="stat-val">${todayOrders}</div><div class="stat-lbl">Заказов сегодня</div></div>
    <div class="stat"><div class="stat-val">${(todayRevenue._sum.total ?? 0).toFixed(0)}</div><div class="stat-lbl">PLN выручка сегодня</div></div>
    <div class="stat"><div class="stat-val">${orders.length}</div><div class="stat-lbl">Всего заказов</div></div>
    <div class="stat"><div class="stat-val">${parseInt(blockedCount ?? '0', 10)}</div><div class="stat-lbl">Сканеров заблокировано</div></div>
  </div>
  <div class="card">
    <div class="card-header"><h2>Последние заблокированные сканеры</h2><span class="muted">без DB/Telegram шума</span></div>
    <table>
      <thead><tr><th>Время</th><th>IP</th><th>Метод</th><th>Путь</th><th>User-Agent</th></tr></thead>
      <tbody>${securityRows || '<tr><td colspan="5" style="text-align:center;color:#888;padding:18px">Сегодня блокировок нет</td></tr>'}</tbody>
    </table>
  </div>
  <div class="card">
    <div class="card-header"><h2>Заказы</h2></div>
    <table>
      <thead><tr><th>#</th><th>Имя</th><th>Телефон</th><th>Город</th><th>Товары</th><th>Сумма</th><th>Оплата</th><th>Ref</th><th>Статус</th><th>Дата</th></tr></thead>
      <tbody>${rows || '<tr><td colspan="10" style="text-align:center;color:#888;padding:24px">Заказов пока нет</td></tr>'}</tbody>
    </table>
  </div>
</div>
</body></html>`);
  });

  // ---- Order detail ----
  app.get<{ Params: { id: string } }>('/orders/:id', async (req, reply) => {
    const id = parseInt(req.params.id);
    if (isNaN(id)) { reply.status(404).send('Not found'); return; }

    const order = await prisma.order.findUnique({ where: { id }, include: { items: true } });
    if (!order) { reply.status(404).send('Order not found'); return; }

    const itemRows = order.items
      .map(
        (i) =>
          `<li>${esc(i.name)}${i.size ? ` (${i.size})` : ''} × ${i.qty} — <b>${(i.price * i.qty).toFixed(2)} ${order.currency}</b></li>`,
      )
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Заказ #${order.id}</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/">← Все заказы</a></div>
<div class="container">
  <a class="back" href="/_admin/">← Назад</a>
  <div class="card">
    <div class="card-header">
      <h2>Заказ #${order.id}</h2>
      <span class="badge badge-${order.status}">${order.status}</span>
    </div>
    <table>
      <tr><th>Имя</th><td>${esc(order.name)}</td><th>Телефон</th><td>${esc(order.phone)}</td></tr>
      ${order.email ? `<tr><th>E-mail</th><td colspan="3">${esc(order.email)}</td></tr>` : ''}
      <tr><th>Адрес</th><td colspan="3">${esc(order.address)}, ${esc(order.city)}, ${esc(order.zip)}</td></tr>
      <tr><th>Сумма</th><td><b>${order.total.toFixed(2)} ${order.currency}</b></td><th>Оплата</th><td>${esc(order.paymentMethod)}</td></tr>
      <tr><th>Ref</th><td><code>${esc(order.reference)}</code></td><th>Дата</th><td>${fmtDate(order.createdAt)}</td></tr>
      ${order.notes ? `<tr><th>Примечания</th><td colspan="3">${esc(order.notes)}</td></tr>` : ''}
    </table>
    <div style="padding:16px 20px;border-top:1px solid #eee">
      <b>Товары:</b>
      <ul class="items-list" style="margin-top:8px">${itemRows}</ul>
    </div>
  </div>
</div>
</body></html>`);
  });

  // ---- Visitors list ----
  app.get('/visitors', async (_req, reply) => {
    const visitors = await prisma.visitor.findMany({
      orderBy: { createdAt: 'desc' },
      take: 200,
      include: {
        _count: { select: { events: true, orders: true } },
        events: { orderBy: { createdAt: 'desc' }, take: 3 },
      },
    });

    const rows = visitors
      .map(
        (v) =>
          `<tr>
          <td><a class="detail-link" href="/_admin/visitors/${v.uuid.slice(0, 8)}"><code>${v.uuid.slice(0, 8)}</code></a></td>
          <td><code>${esc(v.ip)}</code></td>
          <td>${esc(countryLabel(v.country, v.city))}</td>
          <td><div class="url">${esc(v.landingUrl)}</div>${v.referer ? `<div class="muted url">ref: ${esc(v.referer)}</div>` : ''}</td>
          <td><span class="muted">${esc(formatUserAgent(v.userAgent))}</span></td>
          <td><div class="event-list">${renderRecentEvents(v.events)}</div></td>
          <td>${v._count.events}</td>
          <td>${v._count.orders}</td>
          <td>${fmtDate(v.createdAt)}</td>
        </tr>`,
      )
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Admin – Визиторы</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/">Заказы</a><a href="/_admin/visitors">Визиторы</a><a href="/_admin/products">Товары</a></div>
<div class="container">
  <div class="card">
    <div class="card-header"><h2>Визиторы (последние 200)</h2></div>
    <table>
      <thead><tr><th>ID</th><th>IP</th><th>Страна</th><th>Вход / referer</th><th>Устройство</th><th>Последние действия</th><th>События</th><th>Заказы</th><th>Первый визит</th></tr></thead>
      <tbody>${rows || '<tr><td colspan="9" style="text-align:center;color:#888;padding:24px">Нет данных</td></tr>'}</tbody>
    </table>
  </div>
</div>
</body></html>`);
  });

  app.get<{ Params: { id: string } }>('/visitors/:id', async (req, reply) => {
    const visitor = await prisma.visitor.findFirst({
      where: { uuid: { startsWith: req.params.id } },
      include: {
        events: { orderBy: { createdAt: 'desc' }, take: 100 },
        orders: { orderBy: { createdAt: 'desc' }, take: 20, include: { items: true } },
      },
    });
    if (!visitor) { reply.status(404).send('Visitor not found'); return; }

    const eventRows = visitor.events.map((e) => `
      <tr>
        <td>${fmtDate(e.createdAt)}</td>
        <td>${eventBadge(e.type)}</td>
        <td>${renderEventSummary(e.type, e.data)}<details><summary class="muted">raw JSON</summary><div class="mono">${esc(JSON.stringify(e.data ?? {}, null, 2))}</div></details></td>
      </tr>`).join('');

    const orderRows = visitor.orders.map((o) => `
      <tr>
        <td><a class="detail-link" href="/_admin/orders/${o.id}">#${o.id}</a></td>
        <td>${fmtDate(o.createdAt)}</td>
        <td>${o.items.length} шт.</td>
        <td><b>${o.total.toFixed(2)} ${o.currency}</b></td>
        <td><span class="badge badge-${o.status}">${o.status}</span></td>
      </tr>`).join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Визитор #${visitor.uuid.slice(0, 8)}</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/visitors">← Визиторы</a></div>
<div class="container">
  <a class="back" href="/_admin/visitors">← Назад</a>
  <div class="card">
    <div class="card-header"><h2>Визитор <code>#${visitor.uuid.slice(0, 8)}</code></h2></div>
    <table>
      <tr><th>IP</th><td><code>${esc(visitor.ip)}</code></td><th>Страна</th><td>${esc(countryLabel(visitor.country, visitor.city))}</td></tr>
      <tr><th>Первый URL</th><td colspan="3"><div class="url">${esc(visitor.landingUrl)}</div></td></tr>
      ${visitor.referer ? `<tr><th>Referer</th><td colspan="3">${esc(visitor.referer)}</td></tr>` : ''}
      <tr><th>Устройство</th><td colspan="3">${esc(formatUserAgent(visitor.userAgent))}<br><span class="muted">${esc(visitor.userAgent)}</span></td></tr>
      <tr><th>Первый визит</th><td colspan="3">${fmtDate(visitor.createdAt)}</td></tr>
    </table>
  </div>
  <div class="card">
    <div class="card-header"><h2>События</h2></div>
    <table><thead><tr><th>Время</th><th>Тип</th><th>Детали</th></tr></thead><tbody>${eventRows || '<tr><td colspan="3" style="text-align:center;color:#888;padding:24px">Нет событий</td></tr>'}</tbody></table>
  </div>
  <div class="card">
    <div class="card-header"><h2>Заказы</h2></div>
    <table><thead><tr><th>#</th><th>Дата</th><th>Товары</th><th>Сумма</th><th>Статус</th></tr></thead><tbody>${orderRows || '<tr><td colspan="5" style="text-align:center;color:#888;padding:24px">Нет заказов</td></tr>'}</tbody></table>
  </div>
</div>
</body></html>`);
  });

  // ---- Product stats ----
  app.get('/products', async (_req, reply) => {
    await flushProductStats();
    const products = await prisma.productStat.findMany({
      orderBy: [{ totalViews: 'desc' }, { lastSeenAt: 'desc' }],
      take: 200,
    });

    const rows = products
      .map(
        (p, idx) =>
          `<tr>
          <td>${idx + 1}</td>
          <td><a class="detail-link" href="${esc(p.url)}" target="_blank" rel="noreferrer">${esc(p.name)}</a><br><code>${esc(p.productId)}</code></td>
          <td><b>${p.totalViews}</b></td>
          <td>~${p.uniqueVisitors}</td>
          <td>${fmtDate(p.lastSeenAt)}</td>
        </tr>`,
      )
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Admin – Товары</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/">Заказы</a><a href="/_admin/visitors">Визиторы</a><a href="/_admin/products">Товары</a></div>
<div class="container">
  <div class="card">
    <div class="card-header">
      <h2>Самые просматриваемые товары за всё время</h2>
      <span style="font-size:12px;color:#888">Сырые просмотры не хранятся: Redis-buffer → агрегат ProductStat</span>
    </div>
    <table>
      <thead><tr><th>#</th><th>Товар</th><th>Просмотры</th><th>Уникальные</th><th>Последний просмотр</th></tr></thead>
      <tbody>${rows || '<tr><td colspan="5" style="text-align:center;color:#888;padding:24px">Статистики пока нет</td></tr>'}</tbody>
    </table>
  </div>
</div>
</body></html>`);
  });

  // ---- Support chat sessions ----
  app.get('/support', async (_req, reply) => {
    const sessions = await listOpenSessions(100);
    const rows = sessions
      .map((s) => {
        const last = s.messages[0];
        const preview = last ? esc(last.text.slice(0, 80)) : '—';
        return `<tr>
          <td><a class="detail-link" href="/_admin/support/${esc(s.id)}">#${esc(s.id.slice(0, 8))}</a></td>
          <td><code>${esc(s.visitorUuid?.slice(0, 8) ?? '—')}</code></td>
          <td class="url">${esc(s.pageUrl ?? '—')}</td>
          <td>${s._count.messages}</td>
          <td>${preview}</td>
          <td>${fmtDate(s.updatedAt)}</td>
        </tr>`;
      })
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Admin – Поддержка</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/">Заказы</a><a href="/_admin/visitors">Визиторы</a><a href="/_admin/products">Товары</a><a href="/_admin/support">Поддержка</a><a href="/analytics">Analytics</a></div>
<div class="container">
  <div class="card">
    <div class="card-header"><h2>Открытые чаты поддержки</h2></div>
    <table>
      <thead><tr><th>ID</th><th>Визитор</th><th>Страница</th><th>Сообщений</th><th>Последнее</th><th>Обновлён</th></tr></thead>
      <tbody>${rows || '<tr><td colspan="6" style="text-align:center;color:#888;padding:24px">Чатов пока нет</td></tr>'}</tbody>
    </table>
  </div>
</div>
</body></html>`);
  });

  app.get<{ Params: { id: string } }>('/support/:id', async (req, reply) => {
    const session = await getSessionWithMessages(req.params.id);
    if (!session) {
      reply.status(404).send('Session not found');
      return;
    }

    const messages = session.messages
      .map((m) => {
        const who = m.role === 'agent' ? 'Менеджер' : 'Клиент';
        return `<div style="margin-bottom:12px;padding:10px 12px;border-radius:8px;background:${m.role === 'agent' ? '#f3f4f6' : '#fff'};border:1px solid #eee">
          <div style="font-size:11px;color:#888;margin-bottom:4px">${esc(who)} · ${fmtDate(m.createdAt)}</div>
          <div>${esc(m.text)}</div>
        </div>`;
      })
      .join('');

    reply.header('content-type', 'text/html; charset=utf-8').send(`<!DOCTYPE html>
<html lang="ru"><head><meta charset="UTF-8"/><title>Чат ${esc(session.id.slice(0, 8))}</title>
<style>${CSS}</style></head><body>
<div class="topbar"><h1>Sinsay Proxy Admin</h1><a href="/_admin/support">← Поддержка</a></div>
<div class="container">
  <div class="card">
    <div class="card-header">
      <h2>Чат #${esc(session.id.slice(0, 8))}</h2>
      <span class="muted">${esc(session.pageUrl ?? '')}</span>
    </div>
    <div style="padding:16px 20px">${messages || '<span class="muted">Нет сообщений</span>'}</div>
  </div>
</div>
</body></html>`);
  });
}

function esc(s?: string | null): string {
  if (!s) return '';
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function fmtDate(d: Date): string {
  return d.toLocaleString('ru-RU', { timeZone: 'Europe/Warsaw' });
}

function summarizeEvent(data: unknown): string {
  if (!data || typeof data !== 'object') return '';
  const obj = data as Record<string, unknown>;
  const parts = [
    obj.name || obj.productName,
    obj.productId ? `#${obj.productId}` : undefined,
    obj.size ? `size ${obj.size}` : undefined,
    obj.quantity ? `qty ${obj.quantity}` : undefined,
    obj.price ? `${obj.price} PLN` : undefined,
    obj.ip ? `IP ${obj.ip}` : undefined,
    obj.sourceUrl || obj.landingUrl,
  ].filter(Boolean);
  return parts.join(' · ');
}

function renderRecentEvents(events: Array<{ type: string; data: unknown; createdAt: Date }>): string {
  if (events.length === 0) return '<span class="muted">Нет действий</span>';
  return events
    .map(
      (event) =>
        `<div class="event-row"><span class="event-time">${fmtDate(event.createdAt).slice(0, 16)}</span><span>${eventBadge(event.type)}</span><span class="event-detail">${esc(summarizeEvent(event.data) || event.type)}</span></div>`,
    )
    .join('');
}

function eventBadge(type: string): string {
  const labels: Record<string, string> = {
    'visit.new': 'Новый визит',
    'cart.add': 'В корзину',
    'cart.remove': 'Удалил',
    'order.new': 'Заказ',
    'payment.update': 'Оплата',
  };
  const cls = type.includes('payment') || type.includes('order')
    ? 'badge-processing'
    : type.includes('cart')
    ? 'badge-new'
    : 'badge-done';
  return `<span class="badge ${cls}">${esc(labels[type] || type)}</span>`;
}

function renderEventSummary(type: string, data: unknown): string {
  if (!data || typeof data !== 'object') return '<span class="muted">Нет деталей</span>';
  const obj = data as Record<string, unknown>;
  const rows: string[] = [];
  const geo = countryLabel(asString(obj.countryCode || obj.country), asString(obj.city), asString(obj.countryName));
  if (obj.ip) rows.push(`<span class="pill">IP <code>${esc(asString(obj.ip))}</code></span>`);
  if (obj.country || obj.countryCode || obj.city) rows.push(`<span class="pill">${esc(geo)}</span>`);
  if (obj.device) rows.push(`<span class="pill">${esc(asString(obj.device))}</span>`);

  if (type === 'cart.add' || type === 'cart.remove') {
    rows.push(`<div><b>${esc(asString(obj.name || obj.productName) || 'Товар')}</b>${obj.quantity ? ` ×${esc(asString(obj.quantity))}` : ''}${obj.size ? ` · size ${esc(asString(obj.size))}` : ''}${obj.price ? ` · ${esc(asString(obj.price))} PLN` : ''}</div>`);
  } else if (type === 'order.new') {
    rows.push(`<div><b>Заказ #${esc(asString(obj.orderId))}</b>${obj.total ? ` · ${esc(asString(obj.total))} ${esc(asString(obj.currency || 'PLN'))}` : ''}${obj.paymentMethod ? ` · ${esc(asString(obj.paymentMethod))}` : ''}</div>`);
  } else if (type === 'visit.new') {
    rows.push('<div><b>Первое посещение сайта</b></div>');
  } else {
    rows.push(`<div>${esc(summarizeEvent(data) || type)}</div>`);
  }

  const url = asString(obj.currentUrl || obj.sourceUrl || obj.landingUrl);
  if (obj.referer) rows.push(`<div class="muted url">Source: ${esc(asString(obj.referer))}</div>`);
  if (url) rows.push(`<div class="muted url">URL: ${esc(url)}</div>`);
  if (obj.at) rows.push(`<div class="muted">At: ${esc(asString(obj.at))}</div>`);
  return rows.join('');
}

function asString(value: unknown): string {
  if (value == null) return '';
  return String(value);
}

interface SecuritySample {
  at: string;
  ip: string;
  method: string;
  path: string;
  ua?: string;
}

function parseSecuritySample(raw: string): SecuritySample | null {
  try {
    const sample = JSON.parse(raw) as Partial<SecuritySample>;
    if (!sample.at || !sample.ip || !sample.method || !sample.path) return null;
    return {
      at: String(sample.at),
      ip: String(sample.ip),
      method: String(sample.method),
      path: String(sample.path),
      ua: sample.ua ? String(sample.ua) : '',
    };
  } catch {
    return null;
  }
}
