'use strict';

// ── State ──────────────────────────────────────────────────────────────────
let currentUser   = null;
let editingUserId = null;
let users         = [];
let companyFilter = '';
let roleFilter    = new Set();   // empty = all roles
let statusFilter  = null;        // null | 'active' | 'inactive'
let groupBy       = 'company';   // 'company' | 'role' | 'status' | null
let syncroCustomers = [];  // [{ id, name }] loaded once from Syncro

// ── Init ───────────────────────────────────────────────────────────────────
(async function init() {
  try {
    const r = await fetch('/auth/me');
    if (!r.ok) { location.replace('/login.html?next=' + encodeURIComponent(location.pathname)); return; }
    const { user } = await r.json();
    currentUser = user;

    document.getElementById('user-name').textContent = user.name;
    const roleBadge = document.getElementById('user-role');
    roleBadge.textContent = user.role;
    roleBadge.dataset.role = user.role;

    // Show server section only for superduperadmin
    if (user.role === 'superduperadmin') {
      document.getElementById('section-server').style.display = '';
    }

    // Hide elevated role options for non-superduperadmin
    if (user.role !== 'superduperadmin') {
      document.querySelector('#f-role option[value="superduperadmin"]').remove();
      document.querySelector('#f-role option[value="admin"]').remove();
    }

    // Load users and Syncro customers in parallel
    await Promise.all([loadUsers(), loadSyncroCustomers()]);
    bindEvents();
    loadApiUsage();
    setInterval(loadApiUsage, 10_000);
  } catch (e) {
    console.error(e);
  }
})();

// ── API helpers ────────────────────────────────────────────────────────────
async function api(method, path, body) {
  const opts = { method, headers: { 'Content-Type': 'application/json' } };
  if (body !== undefined) opts.body = JSON.stringify(body);
  const r = await fetch(path, opts);
  const data = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
  return data;
}

// ── Syncro customers ───────────────────────────────────────────────────────
async function loadSyncroCustomers() {
  const hint    = document.getElementById('f-customer-hint');
  const listbox = document.getElementById('f-syncro-customer');
  try {
    let page = 1, all = [];
    while (true) {
      const r    = await fetch(`/syncro-api/customers?per_page=100&page=${page}`);
      const data = await r.json();
      const batch      = data.customers ?? [];
      const totalPages = data.meta?.total_pages ?? 1;
      all = all.concat(batch);
      if (page >= totalPages) break;
      page++;
    }
    syncroCustomers = all
      .map(c => ({ id: c.id, name: c.business_name ?? c.name ?? `Customer ${c.id}` }))
      .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));

    populateCustomerListbox('');
    hint.textContent = 'Links this user to a Syncro customer for access control.';
  } catch (e) {
    hint.textContent = 'Could not load Syncro customers.';
    console.error('Syncro customer load failed:', e);
  }
}

function populateCustomerListbox(filter) {
  const listbox = document.getElementById('f-syncro-customer');
  const current = listbox.value;
  const q = filter.toLowerCase();

  // Keep the "None" option then add matching customers
  const matches = q
    ? syncroCustomers.filter(c => c.name.toLowerCase().includes(q))
    : syncroCustomers;

  listbox.innerHTML = '<option value="">— None (internal user) —</option>';
  for (const c of matches) {
    const opt = document.createElement('option');
    opt.value = c.id;
    opt.textContent = c.name;
    listbox.appendChild(opt);
  }

  // Restore selection if still present
  if (current && listbox.querySelector(`option[value="${current}"]`)) {
    listbox.value = current;
  }
}

// ── Load & render users ────────────────────────────────────────────────────
async function loadUsers() {
  try {
    const { users: list } = await api('GET', '/admin/users');
    users = list;
    rebuildCompanyFilter();
    renderUsers();
  } catch (e) {
    document.getElementById('user-tbody').innerHTML =
      `<tr><td colspan="7" class="empty-state" style="color:var(--red);">Failed to load users: ${e.message}</td></tr>`;
  }
}

function rebuildCompanyFilter() {
  // Collect unique non-empty companies, sorted
  const companies = [...new Set(
    users.map(u => u.company).filter(Boolean)
  )].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));

  // Filter dropdown
  const select = document.getElementById('company-filter');
  const prev   = select.value;
  select.innerHTML = '<option value="">All Companies</option>';
  for (const c of companies) {
    const opt = document.createElement('option');
    opt.value = c;
    opt.textContent = c;
    select.appendChild(opt);
  }
  select.value = companies.includes(prev) ? prev : '';
  companyFilter = select.value;

}

function renderUsers() {
  const tbody = document.getElementById('user-tbody');

  const visible = users.filter(u => {
    if (companyFilter && u.company !== companyFilter) return false;
    if (roleFilter.size > 0 && !roleFilter.has(u.role)) return false;
    if (statusFilter === 'active'   && !u.active)  return false;
    if (statusFilter === 'inactive' &&  u.active)  return false;
    return true;
  });

  const hasFilter = companyFilter || roleFilter.size > 0 || statusFilter !== null;
  if (!visible.length) {
    tbody.innerHTML = `<tr><td colspan="7" class="empty-state">${hasFilter ? 'No users match the current filters.' : 'No users found.'}</td></tr>`;
    return;
  }

  const rows = [];

  if (groupBy) {
    const keyOf = u => {
      if (groupBy === 'company') return u.company || '';
      if (groupBy === 'role')    return u.role;
      if (groupBy === 'status')  return u.active ? 'active' : 'inactive';
      return '';
    };

    const labelOf = key => {
      if (groupBy === 'company') return key || 'No Company';
      if (groupBy === 'role')    return { client: 'Client', tech: 'Tech', admin: 'Admin', superduperadmin: 'Super Admin' }[key] ?? key;
      if (groupBy === 'status')  return key === 'active' ? 'Active' : 'Inactive';
      return key;
    };

    const sortKeys = keys => {
      if (groupBy === 'company') return keys.sort((a, b) => !a && b ? 1 : a && !b ? -1 : a.localeCompare(b, undefined, { sensitivity: 'base' }));
      if (groupBy === 'role')    return keys.sort((a, b) => (['superduperadmin','admin','tech','client'].indexOf(a) - ['superduperadmin','admin','tech','client'].indexOf(b)));
      if (groupBy === 'status')  return keys.sort((a, b) => a === 'active' ? -1 : 1);
      return keys;
    };

    const grouped = new Map();
    for (const u of visible) {
      const key = keyOf(u);
      if (!grouped.has(key)) grouped.set(key, []);
      grouped.get(key).push(u);
    }

    for (const key of sortKeys([...grouped.keys()])) {
      rows.push(`<tr class="group-row"><td colspan="7">${esc(labelOf(key))}</td></tr>`);
      for (const u of grouped.get(key)) rows.push(userRow(u));
    }
  } else {
    for (const u of visible) rows.push(userRow(u));
  }

  tbody.innerHTML = rows.join('');
}

function userRow(u) {
  const activeClass = u.active ? 'active' : 'inactive';
  const activeLabel = u.active ? 'Active' : 'Inactive';
  const created     = new Date(u.created_at + 'Z').toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
  const isSelf      = u.id === currentUser.id;

  const toggleBtn = u.active
    ? `<button class="btn btn-danger btn-sm" data-action="toggle" data-id="${u.id}" data-activate="false" ${isSelf ? 'disabled title="Cannot deactivate your own account"' : ''}>Deactivate</button>`
    : `<button class="btn btn-ghost btn-sm" data-action="toggle" data-id="${u.id}" data-activate="true">Activate</button>`;

  return `
    <tr>
      <td class="td-name">${esc(u.name)}${isSelf ? ' <span style="font-size:.72rem;color:var(--gray-400);font-weight:400;">(you)</span>' : ''}</td>
      <td class="td-username">${esc(u.username)}</td>
      <td style="color:var(--gray-600);font-size:.85rem;">${u.company ? esc(u.company) : '<span style="color:var(--gray-300);">—</span>'}</td>
      <td><span class="role-badge" data-role="${esc(u.role)}">${esc(u.role)}</span></td>
      <td><span class="status-badge ${activeClass}"><span class="status-dot"></span>${activeLabel}</span></td>
      <td style="color:var(--gray-500);font-size:.82rem;">${created}</td>
      <td class="td-actions">
        <button class="btn btn-ghost btn-sm" data-action="edit" data-id="${u.id}">Edit</button>
        ${toggleBtn}
      </td>
    </tr>`;
}

// ── App menu ───────────────────────────────────────────────────────────────
function initAdminMenu() {
  const toggle = document.getElementById('btn-menu-toggle');
  const menu   = document.getElementById('app-menu');
  if (!toggle || !menu) return;

  toggle.addEventListener('click', e => {
    e.stopPropagation();
    const opening = menu.hidden;
    menu.hidden = !opening;
    toggle.setAttribute('aria-expanded', String(opening));
  });

  document.addEventListener('click', e => {
    if (!menu.hidden && !menu.contains(e.target) && e.target !== toggle && !toggle.contains(e.target)) {
      menu.hidden = true;
      toggle.setAttribute('aria-expanded', 'false');
    }
  });

  document.addEventListener('keydown', e => {
    if (e.key === 'Escape' && !menu.hidden) {
      menu.hidden = true;
      toggle.setAttribute('aria-expanded', 'false');
    }
  });
}

// ── User filter panel ──────────────────────────────────────────────────────
function bindFilterPanel() {
  const panel     = document.getElementById('user-filter-panel');
  const filterBtn = document.getElementById('btn-user-filter');

  filterBtn?.addEventListener('click', () => {
    panel.hidden = !panel.hidden;
    filterBtn.classList.toggle('active', !panel.hidden);
  });

  panel?.addEventListener('click', e => {
    const chip = e.target.closest('.af-chip');
    if (!chip) return;
    const group = chip.closest('[data-filter-group]')?.dataset.filterGroup;

    if (group === 'role') {
      const val = chip.dataset.value;
      if (roleFilter.has(val)) { roleFilter.delete(val); chip.classList.remove('active'); }
      else                     { roleFilter.add(val);    chip.classList.add('active'); }
    } else if (group === 'status') {
      statusFilter = chip.dataset.value === '' ? null : chip.dataset.value;
      chip.closest('.af-chips').querySelectorAll('.af-chip').forEach(c =>
        c.classList.toggle('active', c.dataset.value === (chip.dataset.value))
      );
    } else if (group === 'groupby') {
      groupBy = chip.dataset.value === '' ? null : chip.dataset.value;
      chip.closest('.af-chips').querySelectorAll('.af-chip').forEach(c =>
        c.classList.toggle('active', c.dataset.value === chip.dataset.value)
      );
    }

    updateFilterBadge();
    renderUsers();
  });

  document.getElementById('af-clear')?.addEventListener('click', () => {
    roleFilter.clear();
    statusFilter  = null;
    companyFilter = '';
    document.getElementById('company-filter').value = '';
    panel?.querySelectorAll('[data-filter-group="role"] .af-chip').forEach(c => c.classList.remove('active'));
    panel?.querySelectorAll('[data-filter-group="status"] .af-chip').forEach(c =>
      c.classList.toggle('active', c.dataset.value === '')
    );
    groupBy = 'company';
    panel?.querySelectorAll('[data-filter-group="groupby"] .af-chip').forEach(c =>
      c.classList.toggle('active', c.dataset.value === 'company')
    );
    updateFilterBadge();
    renderUsers();
  });

  document.getElementById('company-filter')?.addEventListener('change', e => {
    companyFilter = e.target.value;
    updateFilterBadge();
    renderUsers();
  });
}

function updateFilterBadge() {
  const count = (companyFilter ? 1 : 0) + roleFilter.size + (statusFilter !== null ? 1 : 0);
  const badge = document.getElementById('user-filter-badge');
  if (badge) { badge.textContent = count; badge.hidden = count === 0; }
}

// ── Bind events ────────────────────────────────────────────────────────────
function bindEvents() {
  initAdminMenu();
  bindFilterPanel();

  document.getElementById('btn-new-user').addEventListener('click', openCreate);

  document.getElementById('btn-logout').addEventListener('click', async () => {
    await fetch('/auth/logout', { method: 'POST' });
    location.replace('/login.html');
  });

  // Table actions — delegated so they work after innerHTML re-renders
  document.getElementById('user-tbody').addEventListener('click', e => {
    const btn = e.target.closest('button[data-action]');
    if (!btn || btn.disabled) return;
    const id = Number(btn.dataset.id);
    if (btn.dataset.action === 'edit') {
      openEdit(id);
    } else if (btn.dataset.action === 'toggle') {
      confirmToggleActive(id, btn.dataset.activate === 'true');
    }
  });

  // Customer picker search
  document.getElementById('f-customer-search').addEventListener('input', e => {
    populateCustomerListbox(e.target.value);
  });

  // Modal close / cancel
  document.getElementById('modal-close').addEventListener('click',  closeUserModal);
  document.getElementById('modal-cancel').addEventListener('click', closeUserModal);
  document.getElementById('user-modal').addEventListener('click', e => {
    if (e.target === e.currentTarget) closeUserModal();
  });

  // Confirm modal
  document.getElementById('confirm-close').addEventListener('click',  closeConfirm);
  document.getElementById('confirm-cancel').addEventListener('click', closeConfirm);
  document.getElementById('confirm-modal').addEventListener('click', e => {
    if (e.target === e.currentTarget) closeConfirm();
  });

  document.getElementById('user-form').addEventListener('submit', handleUserFormSubmit);

  const btnRestart = document.getElementById('btn-restart');
  if (btnRestart) {
    btnRestart.addEventListener('click', () => {
      showConfirm(
        'Restart Server',
        'The server process will exit and PM2 will restart it automatically. This takes about 1–2 seconds. Proceed?',
        'Restart',
        'btn-accent',
        doRestart
      );
    });
  }

  document.getElementById('btn-refresh-usage')?.addEventListener('click', loadApiUsage);

  // History panel toggle
  document.getElementById('btn-history-toggle')?.addEventListener('click', () => {
    _historyPanelVisible = !_historyPanelVisible;
    const panel = document.getElementById('usage-history-panel');
    if (panel) panel.hidden = !_historyPanelVisible;
    document.getElementById('btn-history-toggle')?.classList.toggle('active', _historyPanelVisible);
    _setHistoryWindowDescription();
    loadApiUsage(); // immediate refresh so bar + chart update
  });

  // Timeframe chips
  document.querySelectorAll('.usage-tf-chip').forEach(chip => {
    chip.addEventListener('click', () => {
      _historyWindowMs = parseInt(chip.dataset.window, 10);
      document.querySelectorAll('.usage-tf-chip').forEach(c =>
        c.classList.toggle('active', c === chip)
      );
      _setHistoryWindowDescription();
      loadApiUsage();
    });
  });

  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') { closeUserModal(); closeConfirm(); }
  });
}

// ── User modal ─────────────────────────────────────────────────────────────
function openCreate() {
  editingUserId = null;
  document.getElementById('modal-title').textContent    = 'New User';
  document.getElementById('modal-submit').textContent   = 'Create User';
  document.getElementById('f-password-label').textContent = 'Password';
  document.getElementById('f-password-hint').textContent  = 'Minimum 12 characters.';
  document.getElementById('f-password').required = true;
  document.getElementById('user-form').reset();
  document.getElementById('modal-error').style.display = 'none';
  document.getElementById('f-username').disabled = false;
  document.getElementById('f-role').disabled     = false;
  document.getElementById('f-customer-search').value = '';
  populateCustomerListbox('');
  document.getElementById('f-syncro-customer').value = '';
  document.getElementById('user-modal').style.display = 'flex';
  document.getElementById('f-name').focus();
}

function openEdit(id) {
  const u = users.find(x => x.id === id);
  if (!u) return;
  editingUserId = id;

  document.getElementById('modal-title').textContent    = 'Edit User';
  document.getElementById('modal-submit').textContent   = 'Save Changes';
  document.getElementById('f-password-label').textContent = 'New Password';
  document.getElementById('f-password-hint').textContent  = 'Leave blank to keep current password. Minimum 12 characters if changing.';
  document.getElementById('f-password').required = false;
  document.getElementById('modal-error').style.display = 'none';

  const isSelf = u.id === currentUser.id;

  document.getElementById('f-name').value        = u.name;
  document.getElementById('f-username').value    = u.username;
  document.getElementById('f-username').disabled = true;
  document.getElementById('f-role').disabled     = isSelf;
  document.getElementById('f-password').value    = '';

  // Pre-select Syncro customer
  document.getElementById('f-customer-search').value = '';
  populateCustomerListbox('');
  document.getElementById('f-syncro-customer').value = u.syncro_customer_id ?? '';

  const roleSelect = document.getElementById('f-role');
  if (!roleSelect.querySelector(`option[value="${u.role}"]`)) {
    const opt = document.createElement('option');
    opt.value = u.role;
    opt.textContent = u.role;
    opt.dataset.temp = '1';
    roleSelect.insertBefore(opt, roleSelect.firstChild);
  }
  roleSelect.value = u.role;

  document.getElementById('user-modal').style.display = 'flex';
  document.getElementById('f-name').focus();
}

function closeUserModal() {
  document.getElementById('user-modal').style.display = 'none';
  document.getElementById('f-role').disabled = false;
  document.querySelectorAll('#f-role option[data-temp]').forEach(o => o.remove());
}

async function handleUserFormSubmit(e) {
  e.preventDefault();
  const errEl  = document.getElementById('modal-error');
  const submit = document.getElementById('modal-submit');
  errEl.style.display = 'none';
  submit.disabled = true;

  const name     = document.getElementById('f-name').value.trim();
  const username = document.getElementById('f-username').value.trim();
  const role     = document.getElementById('f-role').value;
  const password = document.getElementById('f-password').value;

  const customerSelect   = document.getElementById('f-syncro-customer');
  const syncroCustomerId = customerSelect.value ? Number(customerSelect.value) : null;
  const company          = syncroCustomerId
    ? (customerSelect.options[customerSelect.selectedIndex]?.text ?? '')
    : '';

  try {
    if (editingUserId === null) {
      await api('POST', '/admin/users', { name, username, company, syncro_customer_id: syncroCustomerId, role, password });
      toast('User created successfully.', 'success');
    } else {
      const body = { name, company, syncro_customer_id: syncroCustomerId, role };
      if (password) body.password = password;
      await api('PATCH', `/admin/users/${editingUserId}`, body);
      toast('User updated successfully.', 'success');
    }
    closeUserModal();
    await loadUsers();
  } catch (err) {
    errEl.textContent = err.message;
    errEl.style.display = '';
  } finally {
    submit.disabled = false;
  }
}

// ── Toggle active ──────────────────────────────────────────────────────────
function confirmToggleActive(id, activate) {
  const u = users.find(x => x.id === id);
  if (!u) return;
  const action = activate ? 'activate' : 'deactivate';
  showConfirm(
    `${activate ? 'Activate' : 'Deactivate'} User`,
    `Are you sure you want to ${action} <strong>${esc(u.name)}</strong> (${esc(u.username)})?${!activate ? ' They will be unable to log in.' : ''}`,
    activate ? 'Activate' : 'Deactivate',
    activate ? 'btn-primary' : 'btn-danger',
    async () => {
      try {
        await api('PATCH', `/admin/users/${id}`, { active: activate });
        toast(`User ${activate ? 'activated' : 'deactivated'}.`, activate ? 'success' : 'info');
        await loadUsers();
      } catch (err) {
        toast(err.message, 'error');
      }
    }
  );
}

// ── Restart ────────────────────────────────────────────────────────────────
async function doRestart() {
  const btn = document.getElementById('btn-restart');
  btn.disabled = true;
  btn.innerHTML = `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin .7s linear infinite">
      <polyline points="23 4 23 10 17 10"/>
      <path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/>
    </svg>
    Restarting…`;
  try {
    await api('POST', '/admin/restart');
  } catch {
    // Server closed before responding — expected
  }
  toast('Server is restarting… page will refresh in a few seconds.', 'info');
  setTimeout(() => location.reload(), 4000);
}

// ── Confirm modal ──────────────────────────────────────────────────────────
function showConfirm(title, message, okLabel, okClass, callback) {
  document.getElementById('confirm-title').textContent = title;
  document.getElementById('confirm-message').innerHTML = message;
  const okBtn = document.getElementById('confirm-ok');
  okBtn.textContent = okLabel;
  okBtn.className   = `btn btn-sm ${okClass}`;
  okBtn.onclick     = async () => { closeConfirm(); await callback(); };
  document.getElementById('confirm-modal').style.display = 'flex';
}

function closeConfirm() {
  document.getElementById('confirm-modal').style.display = 'none';
}

// ── Toast ──────────────────────────────────────────────────────────────────
function toast(msg, type = 'info') {
  const container = document.getElementById('toast-container');
  const el = document.createElement('div');
  el.className = `toast ${type}`;
  el.textContent = msg;
  container.appendChild(el);
  setTimeout(() => el.remove(), 4000);
}

// ── API usage ──────────────────────────────────────────────────────────────
let _historyWindowMs     = 3_600_000;  // 1h — active only when panel is visible
let _historyPanelVisible = false;

async function loadApiUsage() {
  const bar   = document.getElementById('api-usage-bar');
  const label = document.getElementById('api-usage-label');
  const sub   = document.getElementById('api-usage-sub');
  if (!bar) return;

  try {
    // Always fetch the current 60s count + 7-day limit-hit tally
    const { requests, limit, limitHits7d = 0 } = await api('GET', '/admin/syncro-usage');

    // Update limit-hit badge on the History button
    const badge = document.getElementById('history-badge');
    if (badge) {
      badge.textContent = limitHits7d > 999 ? '999+' : String(limitHits7d);
      badge.hidden = limitHits7d === 0;
    }

    let displayReq, displayLimit, pct;

    if (_historyPanelVisible) {
      // Fetch per-minute history for the selected window
      const { buckets } = await api('GET', `/admin/syncro-history?window=${_historyWindowMs}`);
      const windowTotal = (buckets ?? []).reduce((s, b) => s + b.count, 0);
      displayReq   = windowTotal;
      displayLimit = Math.round(_historyWindowMs / 60_000) * limit;  // minutes × 180
      pct          = Math.min(100, (windowTotal / displayLimit) * 100);
      drawUsageChart(buckets ?? []);
    } else {
      displayReq   = requests;
      displayLimit = limit;
      pct          = Math.min(100, (requests / limit) * 100);
    }

    bar.style.width = pct + '%';
    bar.className   = 'api-usage-bar' + (pct >= 80 ? ' crit' : pct >= 50 ? ' warn' : '');
    label.textContent = displayReq === 0
      ? `— / ${displayLimit.toLocaleString()}`
      : `${displayReq.toLocaleString()} / ${displayLimit.toLocaleString()}`;

    const timeStr = new Date().toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    sub.textContent = `Updated ${timeStr} · auto-refreshes every 10s`;
  } catch (e) {
    if (sub) sub.textContent = 'Failed to load usage data.';
  }
}

function _setHistoryWindowDescription() {
  const el = document.getElementById('api-usage-desc');
  if (!el) return;
  if (!_historyPanelVisible) {
    el.textContent = 'Rolling 60-second request window';
    return;
  }
  const labels = { 3600000: '1-hour', 86400000: '24-hour', 604800000: '7-day' };
  el.textContent = `Rolling ${labels[_historyWindowMs] ?? ''} request window`;
}

function drawUsageChart(buckets) {
  const canvas = document.getElementById('usage-chart');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');

  // Size the backing store to physical pixels for crispness
  const dpr = window.devicePixelRatio || 1;
  const displayW = canvas.offsetWidth;
  const displayH = canvas.offsetHeight;
  if (displayW === 0 || displayH === 0) return;
  canvas.width  = displayW * dpr;
  canvas.height = displayH * dpr;
  ctx.scale(dpr, dpr);

  const W = displayW, H = displayH;
  const now = Date.now();

  // Aggregation block size
  let blockMs;
  if      (_historyWindowMs <= 3_600_000)   blockMs = 60_000;        // 1h  → 1-min blocks (60 pts)
  else if (_historyWindowMs <= 86_400_000)  blockMs = 10 * 60_000;   // 24h → 10-min blocks (144 pts)
  else                                      blockMs = 60 * 60_000;   // 7d  → 1-hour blocks (168 pts)

  // Build lookup map from raw per-minute buckets
  const bucketMap = new Map(buckets.map(b => [b.ts, b.count]));

  // Generate aggregated points (avg req/min in each block)
  const windowStart    = now - _historyWindowMs;
  const firstBlockStart = Math.floor(windowStart / blockMs) * blockMs;
  const lastBlockStart  = Math.floor(now / blockMs) * blockMs;
  const points = [];
  for (let t = firstBlockStart; t <= lastBlockStart; t += blockMs) {
    let total = 0, mins = 0;
    for (let m = t; m < t + blockMs; m += 60_000) { total += bucketMap.get(m) ?? 0; mins++; }
    points.push({ t, value: mins > 0 ? total / mins : 0 });
  }
  if (points.length === 0) return;

  // Layout padding
  const padL = 28, padR = 8, padT = 10, padB = 20;
  const chartW = W - padL - padR;
  const chartH = H - padT - padB;
  const maxY   = 180;

  const toX = i => padL + (points.length > 1 ? i / (points.length - 1) : 0.5) * chartW;
  const toY = v => padT + chartH * (1 - Math.min(v, maxY) / maxY);

  ctx.clearRect(0, 0, W, H);

  // Grid lines + Y-axis labels
  [0, 60, 120, 180].forEach(v => {
    const y = toY(v);
    ctx.strokeStyle = v === 0 ? '#d1d5db' : '#e5e7eb';
    ctx.lineWidth   = 1;
    ctx.setLineDash(v === 0 ? [] : [3, 3]);
    ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(padL + chartW, y); ctx.stroke();
    ctx.setLineDash([]);

    ctx.fillStyle  = '#9ca3af';
    ctx.font       = '9px sans-serif';
    ctx.textAlign  = 'right';
    ctx.textBaseline = 'middle';
    ctx.fillText(String(v), padL - 4, y);
  });

  // Filled area gradient
  const gradient = ctx.createLinearGradient(0, padT, 0, padT + chartH);
  gradient.addColorStop(0, 'rgba(43,84,153,.22)');
  gradient.addColorStop(1, 'rgba(43,84,153,.02)');

  ctx.beginPath();
  points.forEach((p, i) => {
    if (i === 0) ctx.moveTo(toX(i), toY(p.value));
    else         ctx.lineTo(toX(i), toY(p.value));
  });
  ctx.lineTo(toX(points.length - 1), padT + chartH);
  ctx.lineTo(toX(0),                 padT + chartH);
  ctx.closePath();
  ctx.fillStyle = gradient;
  ctx.fill();

  // Line
  ctx.beginPath();
  points.forEach((p, i) => {
    if (i === 0) ctx.moveTo(toX(i), toY(p.value));
    else         ctx.lineTo(toX(i), toY(p.value));
  });
  ctx.strokeStyle = '#2b5499';
  ctx.lineWidth   = 1.5;
  ctx.lineJoin    = 'round';
  ctx.stroke();

  // X-axis labels (5 evenly spaced)
  const labelCount = Math.min(5, points.length);
  ctx.fillStyle    = '#9ca3af';
  ctx.font         = '9px sans-serif';
  ctx.textAlign    = 'center';
  ctx.textBaseline = 'alphabetic';
  for (let li = 0; li < labelCount; li++) {
    const idx = Math.round(li * (points.length - 1) / (labelCount - 1));
    const d   = new Date(points[idx].t);
    const label = blockMs >= 60 * 60_000
      ? d.toLocaleDateString([], { month: 'short', day: 'numeric' })
      : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    ctx.fillText(label, toX(idx), H - 3);
  }
}

// ── Utility ───────────────────────────────────────────────────────────────
function esc(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}
