/* MediCore Smart Top Status Bar
 * File: assets/topbar.js
 *
 * Owns:
 *   - Connection state on the .topbar itself (green/red/orange/blue),
 *     layered on top of the existing browser online/offline events and
 *     MediOffline's IndexedDB sync queue (assets/offline.js) — this file
 *     does NOT duplicate that queueing logic, it just visualises it plus
 *     adds a separate SERVER/DB health check (api/status.php) so a
 *     "server down but browser still thinks it's online" case shows up
 *     distinctly (orange) from a real internet outage (red).
 *   - Notification bell / Push message / Task panels + badge counts.
 *   - Real-time-ish updates via SSE (api/status.php?action=stream) with
 *     an automatic polling fallback — no WebSocket process required.
 */
(function (w, d) {
  'use strict';

  const API_STATUS = () => (w.MEDI_API_BASE || '../api/') + 'status.php';
  const API_NOTIF   = () => (w.MEDI_API_BASE || '../api/') + 'notifications.php';
  const API_PUSH    = () => (w.MEDI_API_BASE || '../api/') + 'push-messages.php';
  const API_TASKS   = () => (w.MEDI_API_BASE || '../api/') + 'tasks.php';

  let sse           = null;
  let pollTimer     = null;
  let serverState   = 'online';   // 'online' | 'server-error'
  let browserState  = navigator.onLine ? 'online' : 'offline';
  let syncing       = false;
  let lastCounts    = { unread_notifications: 0, unread_push: 0, pending_tasks: 0 };
  let openPanelName = null;

  /* ── tiny helpers ─────────────────────────────────────────── */
  function qs(id) { return d.getElementById(id); }
  function esc(s) {
    return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
                          .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }
  function relTime(iso) {
    if (!iso) return '';
    const t = new Date(iso.replace(' ', 'T')).getTime();
    if (isNaN(t)) return iso;
    const diff = Math.max(0, Math.floor((Date.now() - t) / 1000));
    if (diff < 60)    return diff + 's ago';
    if (diff < 3600)  return Math.floor(diff / 60) + 'm ago';
    if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
    return Math.floor(diff / 86400) + 'd ago';
  }

  /* ── optional sound (off by default) ─────────────────────────
   * Synthesised with the Web Audio API so no external audio file
   * is needed. Toggle stored in localStorage. */
  function soundEnabled() { return localStorage.getItem('mc_notif_sound') === '1'; }
  function setSoundEnabled(on) { localStorage.setItem('mc_notif_sound', on ? '1' : '0'); }
  function beep() {
    if (!soundEnabled()) return;
    try {
      const ctx = new (w.AudioContext || w.webkitAudioContext)();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = 'sine';
      osc.frequency.value = 880;
      gain.gain.setValueAtTime(0.001, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.15, ctx.currentTime + 0.01);
      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.28);
      osc.connect(gain).connect(ctx.destination);
      osc.start();
      osc.stop(ctx.currentTime + 0.3);
    } catch (e) { /* Web Audio not available — silently skip */ }
  }

  /* ── connection state (browser + server, combined) ───────────
   * Priority: offline (red) > syncing (blue) > server-error (orange)
   * > online (green). Drives the .topbar background class and the
   * existing #offline-badge text (left in place from assets/offline.js —
   * this only adds the full-bar colour + a ticker message on top). */
  function effectiveState() {
    if (browserState === 'offline') return 'offline';
    if (syncing)                    return 'syncing';
    if (serverState === 'server-error') return 'server-error';
    return 'online';
  }

  function applyBarState() {
    const bar = d.querySelector('.topbar');
    if (!bar) return;
    const state = effectiveState();
    bar.classList.remove('state-online', 'state-offline', 'state-syncing', 'state-server-error');
    bar.classList.add('state-' + state);
    renderTicker(state);
  }

  function resetMarquee(el) {
    el.classList.remove('tt-has-marquee');
    tickerSig = ''; // force a full rebuild once we're back to the online state
  }

  function renderTicker(state) {
    const el = qs('topbar-ticker');
    if (!el) return;

    if (state === 'offline') {
      resetMarquee(el);
      MediOffline?.getPendingCount().then(n => {
        el.innerHTML = `<span class="tt-icon">⚠</span> Offline | Internet Connection Lost. Working in Offline Mode.`
          + (n > 0 ? ` <span class="tt-pill">Pending Sync: ${n} Record${n === 1 ? '' : 's'}</span>` : '');
      }).catch(() => {
        el.innerHTML = `<span class="tt-icon">⚠</span> Offline | Internet Connection Lost. Working in Offline Mode.`;
      });
      return;
    }
    if (state === 'syncing') {
      resetMarquee(el);
      el.innerHTML = `<span class="tt-icon tt-spin">🔄</span> Connection Restored. Synchronizing Data…`;
      return;
    }
    if (state === 'server-error') {
      resetMarquee(el);
      el.innerHTML = `<span class="tt-icon">🟠</span> Server Connection Problem Detected.`;
      return;
    }
    renderImportantTicker();
  }

  /* Center ticker in the healthy/online state strings every pressing
   * unread item — notifications, push messages, AND pending tasks —
   * together and scrolls the whole lane right-to-left, stock-ticker
   * style, taking advantage of the topbar's spare width. The queue is
   * only re-rendered (which restarts the scroll) when its contents
   * actually change, so a routine poll that finds nothing new doesn't
   * visibly reset/jump the animation. Clicking a specific item still
   * opens the right panel via event delegation. */
  let tickerItems = [];
  let tickerSig   = '';

  function renderImportantTicker() {
    const sig = tickerItems.map(it => (it._panel || '') + ':' + it.title).join('|');
    if (sig === tickerSig && qs('topbar-ticker')?.querySelector('.tt-marquee-track')) return;
    tickerSig = sig;
    renderTickerFrame();
  }

  function renderTickerFrame() {
    const el = qs('topbar-ticker');
    if (!el) return;
    el.onclick = null;
    if (!tickerItems.length) { el.classList.remove('tt-has-marquee'); el.innerHTML = ''; return; }

    const pieces = tickerItems.map((item, i) =>
      `<span class="tt-marquee-item" data-idx="${i}"><span class="tt-icon">${esc(item.icon || '🔔')}</span> ${esc(item.title)}</span><span class="tt-marquee-sep">•</span>`
    ).join('');

    el.classList.add('tt-has-marquee');
    // Render exactly ONE copy up front. Which animation it gets depends
    // on whether that one copy already fills the visible width (measured
    // below): short queues slide that single copy fully across and loop
    // (see the `singleWidth <= viewWidth` branch) so there is only ever
    // ONE instance of any item on screen — never two sitting side by
    // side. Only once content is confirmed to overflow do we duplicate
    // it back-to-back for the classic seamless wrap loop, where a second
    // copy reads as "the ticker continuing," not as a duplicate, because
    // there's enough distinct content filling the lane either way.
    el.innerHTML = `<span class="tt-marquee"><span class="tt-marquee-track">${pieces}</span></span>`;

    const track = el.querySelector('.tt-marquee-track');
    track.addEventListener('click', (e) => {
      const itemEl = e.target.closest('.tt-marquee-item');
      if (!itemEl) return;
      e.stopPropagation();
      const idx = parseInt(itemEl.dataset.idx, 10) % tickerItems.length;
      openPanel(tickerItems[idx]._panel || 'notif');
    });

    requestAnimationFrame(() => {
      const singleWidth = track.scrollWidth;
      const viewWidth   = el.clientWidth;

      if (singleWidth <= viewWidth) {
        // Short content (as little as one item): disable the CSS wrap-
        // loop keyframe (which assumes duplicated content) and instead
        // slide this single copy in from just past the right edge,
        // fully across, and out past the left edge, looping forever —
        // exactly one instance of the text is ever visible at a time.
        track.style.animation = 'none';
        const distance = viewWidth + singleWidth;
        const duration = Math.max(6000, distance / 60 * 1000);
        track.animate(
          [
            { transform: `translateX(${viewWidth}px)` },
            { transform: `translateX(-${singleWidth}px)` },
          ],
          { duration, iterations: Infinity, easing: 'linear' }
        );
        return;
      }

      // Overflows — NOW duplicate for a seamless right-to-left loop.
      track.insertAdjacentHTML('beforeend', pieces);
      // Scroll speed proportional to content width so pace feels constant
      // whether the queue is short or long (~45px/sec, floor of 10s/loop).
      const duration = Math.max(10, singleWidth / 45);
      track.style.animationDuration = duration + 's';
    });
  }

  /* ═══════════════════════════════════════════════════════════
     Panels (Notification bell / Push messages / Tasks)
     ═══════════════════════════════════════════════════════════ */
  function closeAllPanels() {
    ['notif', 'push', 'task'].forEach(name => {
      const p = qs('tb-' + name + '-panel');
      if (p) p.style.display = 'none';
    });
    openPanelName = null;
  }

  function openPanel(name) {
    if (openPanelName === name) { closeAllPanels(); return; }
    closeAllPanels();
    const p = qs('tb-' + name + '-panel');
    if (!p) return;
    p.style.display = 'block';
    openPanelName = name;
    if (name === 'notif') loadNotifications();
    if (name === 'push')  loadPush();
    if (name === 'task')  loadTasks();
  }

  d.addEventListener('click', e => {
    if (!openPanelName) return;
    const panel = qs('tb-' + openPanelName + '-panel');
    const btn   = qs('tb-' + openPanelName + '-btn');
    if (panel && (panel.contains(e.target) || (btn && btn.contains(e.target)))) return;
    closeAllPanels();
  });

  /* ── Notifications ────────────────────────────────────────── */
  async function loadNotifications() {
    const body = qs('tb-notif-body');
    if (body) body.innerHTML = `<div class="tb-empty">Loading…</div>`;
    const r = await apiPost(API_NOTIF(), { action: 'list', page_size: 20 }).catch(() => null);
    if (!r || !r.success) { if (body) body.innerHTML = `<div class="tb-empty">Could not load notifications.</div>`; return; }
    renderNotifPanel(r.data);
  }

  function renderNotifPanel(rows) {
    const body = qs('tb-notif-body');
    if (!body) return;
    if (!rows.length) { body.innerHTML = `<div class="tb-empty">🔔 No notifications yet.</div>`; return; }
    body.innerHTML = rows.map(n => `
      <div class="tb-item${n.is_read == 0 ? ' unread' : ''}" data-id="${n.id}" onclick="MediTopbar._notifClick(${n.id},'${esc(n.link || '')}','${esc(n.type || '')}')">
        <span class="tb-item-icon">${esc(n.icon || '🔔')}</span>
        <div class="tb-item-body">
          <div class="tb-item-title">${esc(n.title)}</div>
          ${n.message ? `<div class="tb-item-msg">${esc(n.message)}</div>` : ''}
          <div class="tb-item-time">${relTime(n.created_at)}</div>
        </div>
        ${n.is_read == 0 ? '<span class="tb-dot"></span>' : ''}
      </div>`).join('');
  }

  // Notification types with no dedicated page to send the user to (e.g.
  // "New Task Assigned" previously linked to plain dashboard.php, which
  // just reloaded the dashboard with no sign of the actual task) open the
  // relevant topbar panel in place instead of navigating away.
  const NOTIF_PANEL_TYPES = { task_assigned: 'task' };

  async function notifClick(id, link, type) {
    await apiPost(API_NOTIF(), { action: 'mark_read', id }).catch(() => null);
    refreshCounts();
    const panel = NOTIF_PANEL_TYPES[type];
    if (panel) {
      closeAllPanels();
      openPanel(panel);
      return;
    }
    if (link) w.location.href = link;
    else loadNotifications();
  }

  async function markAllNotifRead() {
    await apiPost(API_NOTIF(), { action: 'mark_all_read' }).catch(() => null);
    loadNotifications();
    refreshCounts();
  }

  /* ── Push messages ────────────────────────────────────────── */
  async function loadPush() {
    const body = qs('tb-push-body');
    if (body) body.innerHTML = `<div class="tb-empty">Loading…</div>`;
    const r = await apiPost(API_PUSH(), { action: 'list', page_size: 20 }).catch(() => null);
    if (!r || !r.success) { if (body) body.innerHTML = `<div class="tb-empty">Could not load messages.</div>`; return; }
    renderPushPanel(r.data);
  }

  function renderPushPanel(rows) {
    const body = qs('tb-push-body');
    if (!body) return;
    if (!rows.length) { body.innerHTML = `<div class="tb-empty">📨 No messages yet.</div>`; return; }
    body.innerHTML = rows.map(m => `
      <div class="tb-item${m.is_read == 0 ? ' unread' : ''}" data-id="${m.id}" onclick="MediTopbar._pushClick(${m.id})">
        <span class="tb-item-icon">📨</span>
        <div class="tb-item-body">
          <div class="tb-item-title">${esc(m.title)}</div>
          <div class="tb-item-msg">${esc(m.message)}</div>
          <div class="tb-item-time">${esc(m.sender_name || 'Admin')} · ${relTime(m.created_at)}</div>
        </div>
        ${m.is_read == 0 ? '<span class="tb-dot"></span>' : ''}
      </div>`).join('');
  }

  async function pushClick(id) {
    await apiPost(API_PUSH(), { action: 'mark_read', id }).catch(() => null);
    refreshCounts();
    loadPush();
  }

  function openComposePush() {
    apiPost(API_PUSH(), { action: 'branches' }).then(br => {
      apiPost(API_PUSH(), { action: 'users' }).then(us => {
        const branches = (br && br.data) || [];
        const users    = (us && us.data) || [];
        showModal(`
          <div style="display:flex;flex-direction:column;gap:10px">
            <div class="fg"><label>Title *</label><input id="pm-title" placeholder="e.g. System Maintenance Tonight"></div>
            <div class="fg"><label>Message *</label>
              <textarea id="pm-message" rows="3" style="padding:6px 9px;border:1px solid var(--border);border-radius:var(--rsm);font-family:inherit;font-size:12px;resize:vertical"></textarea>
            </div>
            <div class="fg"><label>Target</label>
              <select id="pm-target" onchange="MediTopbar._toggleComposeTarget(this.value)">
                <option value="all">Everyone</option>
                <option value="branch">A Branch</option>
                <option value="user">A Specific User</option>
              </select>
            </div>
            <div class="fg" id="pm-branch-wrap" style="display:none">
              <label>Branch</label>
              <select id="pm-branch">${branches.map(b => `<option value="${b.id}">${esc(b.name)}</option>`).join('')}</select>
            </div>
            <div class="fg" id="pm-user-wrap" style="display:none">
              <label>User</label>
              <select id="pm-user">${users.map(u => `<option value="${u.id}">${esc(u.name)} (${esc(u.role)})</option>`).join('')}</select>
            </div>
            <div id="pm-err" style="color:var(--danger);font-size:11px;display:none"></div>
          </div>
          <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
            <button class="btn" onclick="closeModal()">Cancel</button>
            <button class="btn btn-primary" onclick="MediTopbar._sendPush()">📨 Send</button>
          </div>`, '📨 New Push Message');
      });
    });
  }
  function toggleComposeTarget(val) {
    qs('pm-branch-wrap').style.display = val === 'branch' ? 'block' : 'none';
    qs('pm-user-wrap').style.display   = val === 'user'   ? 'block' : 'none';
  }
  async function sendPush() {
    const title  = qs('pm-title').value.trim();
    const msg    = qs('pm-message').value.trim();
    const target = qs('pm-target').value;
    const errEl  = qs('pm-err');
    const payload = { action: 'send', title, message: msg, target_type: target };
    if (target === 'branch') payload.target_branch_id = qs('pm-branch').value;
    if (target === 'user')   payload.target_user_id   = qs('pm-user').value;

    const r = await apiPost(API_PUSH(), payload);
    if (r.success) {
      closeModal();
      MediOffline?.toast?.(`✅ Sent to ${r.recipient_count} recipient(s)`, 'success');
    } else {
      errEl.textContent = '⚠ ' + (r.error || 'Send failed.');
      errEl.style.display = 'block';
    }
  }

  /* ── Tasks ─────────────────────────────────────────────────── */
  async function loadTasks() {
    const body = qs('tb-task-body');
    if (body) body.innerHTML = `<div class="tb-empty">Loading…</div>`;
    const r = await apiPost(API_TASKS(), { action: 'list', page_size: 30 }).catch(() => null);
    if (!r || !r.success) { if (body) body.innerHTML = `<div class="tb-empty">Could not load tasks.</div>`; return; }
    renderTaskPanel(r.data);
  }

  const PRI_COLOR = { high: 'var(--danger)', medium: 'var(--warn)', low: 'var(--text3)' };
  function renderTaskPanel(rows) {
    const body = qs('tb-task-body');
    if (!body) return;
    if (!rows.length) { body.innerHTML = `<div class="tb-empty">✅ No tasks right now.</div>`; return; }
    body.innerHTML = rows.map(t => `
      <div class="tb-item${t.status !== 'completed' ? ' unread' : ''}" data-id="${t.id}">
        <span class="tb-item-icon" style="color:${PRI_COLOR[t.priority] || 'var(--text3)'}">●</span>
        <div class="tb-item-body">
          <div class="tb-item-title" style="${t.status === 'completed' ? 'text-decoration:line-through;opacity:.6' : ''}">${esc(t.title)}</div>
          ${t.description ? `<div class="tb-item-msg">${esc(t.description)}</div>` : ''}
          <div class="tb-item-time">
            ${esc(t.assignee_name || '—')} · ${t.priority} priority
            ${t.due_date ? ' · due ' + esc(t.due_date) : ''}
          </div>
        </div>
        ${t.status !== 'completed'
          ? `<button class="btn btn-sm" onclick="event.stopPropagation();MediTopbar._completeTask(${t.id})" title="Mark complete">✓</button>`
          : ''}
      </div>`).join('');
  }

  async function completeTask(id) {
    await apiPost(API_TASKS(), { action: 'mark_complete', id }).catch(() => null);
    loadTasks();
    refreshCounts();
  }

  function openComposeTask() {
    apiPost(API_TASKS(), { action: 'users' }).then(us => {
      const users = (us && us.data) || [];
      showModal(`
        <div style="display:flex;flex-direction:column;gap:10px">
          <div class="fg"><label>Title *</label><input id="tk-title" placeholder="e.g. Verify Purchase Invoice"></div>
          <div class="fg"><label>Description</label>
            <textarea id="tk-desc" rows="2" style="padding:6px 9px;border:1px solid var(--border);border-radius:var(--rsm);font-family:inherit;font-size:12px;resize:vertical"></textarea>
          </div>
          <div class="fg2">
            <div class="fg"><label>Assign To *</label>
              <select id="tk-assignee">${users.map(u => `<option value="${u.id}">${esc(u.name)} (${esc(u.role)})</option>`).join('')}</select>
            </div>
            <div class="fg"><label>Priority</label>
              <select id="tk-priority">
                <option value="high">High</option>
                <option value="medium" selected>Medium</option>
                <option value="low">Low</option>
              </select>
            </div>
          </div>
          <div class="fg"><label>Due Date</label><input id="tk-due" type="date"></div>
          <div id="tk-err" style="color:var(--danger);font-size:11px;display:none"></div>
        </div>
        <div style="display:flex;gap:8px;justify-content:flex-end;margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
          <button class="btn" onclick="closeModal()">Cancel</button>
          <button class="btn btn-primary" onclick="MediTopbar._createTask()">✅ Assign Task</button>
        </div>`, '✅ New Task');
    });
  }
  async function createTask() {
    const payload = {
      action: 'create',
      title:       qs('tk-title').value.trim(),
      description: qs('tk-desc').value.trim(),
      assigned_to: qs('tk-assignee').value,
      priority:    qs('tk-priority').value,
      due_date:    qs('tk-due').value,
    };
    const errEl = qs('tk-err');
    const r = await apiPost(API_TASKS(), payload);
    if (r.success) {
      closeModal();
      MediOffline?.toast?.('✅ Task assigned', 'success');
    } else {
      errEl.textContent = '⚠ ' + (r.error || 'Could not create task.');
      errEl.style.display = 'block';
    }
  }

  /* ═══════════════════════════════════════════════════════════
     Badge counts + health monitoring
     ═══════════════════════════════════════════════════════════ */
  function updateBadgeDot(id, count) {
    const el = qs(id);
    if (!el) return;
    el.textContent = count > 99 ? '99+' : (count > 0 ? count : '');
    el.style.display = count > 0 ? 'inline-flex' : 'none';
  }

  function applyCounts(payload) {
    const notif = payload.unread_notifications || 0;
    const push  = payload.unread_push || 0;
    const task  = payload.pending_tasks || 0;

    if (notif > lastCounts.unread_notifications || push > lastCounts.unread_push) {
      beep();
    }
    lastCounts = { unread_notifications: notif, unread_push: push, pending_tasks: task };

    updateBadgeDot('tb-notif-count', notif);
    updateBadgeDot('tb-push-count',  push);
    updateBadgeDot('tb-task-count',  task);

    // If a panel is currently open, refresh it live too
    if (openPanelName === 'notif') loadNotifications();
    if (openPanelName === 'push')  loadPush();
    if (openPanelName === 'task')  loadTasks();

    refreshTickerItems();
  }

  function refreshTickerItems() {
    // Build the center-ticker flip queue from whichever unread items exist
    // across ALL three sources — notifications, push messages, pending
    // tasks — each gated behind its own count so a quiet tab with nothing
    // unread anywhere issues zero extra requests per poll.
    // 'task_assigned' notifications are deliberately excluded here — the
    // SAME assignment already shows up below via the pending-tasks feed
    // (taskP), with the task's own real title instead of the generic
    // "New Task Assigned" label. Showing both meant one new task looked
    // like two duplicate ticker entries. The notification itself still
    // exists normally for the bell badge/panel — only the ticker skips it.
    const notifP = lastCounts.unread_notifications > 0
      ? apiPost(API_NOTIF(), { action: 'list', unread_only: true, page_size: 5 })
          .then(r => (r && r.success)
            ? r.data.filter(n => n.type !== 'task_assigned').map(n => ({ icon: n.icon || '🔔', title: n.title, _panel: 'notif' }))
            : [])
          .catch(() => [])
      : Promise.resolve([]);

    const pushP = lastCounts.unread_push > 0
      ? apiPost(API_PUSH(), { action: 'list', page_size: 5 })
          .then(r => (r && r.success)
            ? r.data.filter(m => m.is_read == 0).map(m => ({ icon: '📨', title: m.title, _panel: 'push' }))
            : [])
          .catch(() => [])
      : Promise.resolve([]);

    const taskP = lastCounts.pending_tasks > 0
      ? apiPost(API_TASKS(), { action: 'list', page_size: 5 })
          .then(r => (r && r.success)
            ? r.data.filter(t => t.status !== 'completed').map(t => ({ icon: '✅', title: t.title, _panel: 'task' }))
            : [])
          .catch(() => [])
      : Promise.resolve([]);

    Promise.all([notifP, pushP, taskP]).then(([notifs, pushes, tasks]) => {
      tickerItems = [...notifs, ...pushes, ...tasks];
      if (effectiveState() === 'online') renderImportantTicker();
    });
  }

  async function refreshCounts() {
    const r = await apiPost(API_STATUS(), { action: 'ping' }).catch(() => null);
    handlePingResult(r);
  }

  function handlePingResult(r) {
    if (!r || !r.success) {
      if (serverState !== 'server-error') {
        serverState = 'server-error';
        applyBarState();
      }
      return;
    }
    if (serverState !== 'online') {
      serverState = 'online';
    }
    applyCounts(r);
    applyBarState();
  }

  /* ── Health monitor — plain polling ──────────────────────────
   * Previously used SSE (api/status.php?action=stream): each open tab
   * held a PHP request open for up to 55s and EventSource reconnected
   * immediately on close, so every browser tab consumed one Apache/PHP
   * worker essentially continuously. XAMPP's default Windows Apache
   * build has a small worker-thread pool — with several tabs open
   * across a long session, that pool can run out, and *unrelated*
   * requests (e.g. a normal AJAX call on another page) then queue for
   * a free worker and appear to hang indefinitely with no error, since
   * the server never even gets to run the PHP that would respond.
   * Plain polling issues one short request every few seconds per tab
   * instead of holding a worker open continuously — far cheaper on a
   * small dev server and avoids that whole class of bug. 10s keeps it
   * feeling reasonably current without repeating the earlier SSE
   * issue; only pause while the tab is hidden, matching the same
   * backgrounded-tab courtesy used by assets/stock-sync.js. */
  function startHealthMonitor() {
    startPolling();
  }
  function startPolling() {
    refreshCounts();
    pollTimer = setInterval(() => { if (!d.hidden) refreshCounts(); }, 10000);
    d.addEventListener('visibilitychange', () => { if (!d.hidden) refreshCounts(); });
  }

  /* ── Browser online/offline + MediOffline sync hooks ─────────
   * assets/offline.js already listens for these events to run its
   * IndexedDB sync engine; this just mirrors the resulting state onto
   * the top bar's background colour and center ticker. */
  function wireBrowserEvents() {
    w.addEventListener('online', () => {
      browserState = 'online';
      syncing = true;
      applyBarState();
      // MediOffline.syncAll() runs on its own 'online' listener; give it
      // a moment, then drop the "syncing" banner back to normal.
      setTimeout(() => { syncing = false; applyBarState(); refreshCounts(); }, 3000);
    });
    w.addEventListener('offline', () => {
      browserState = 'offline';
      syncing = false;
      applyBarState();
    });
    // Poll the pending-sync count while offline so "Pending Sync: N" stays live
    setInterval(() => { if (browserState === 'offline') renderTicker('offline'); }, 5000);
  }

  /* ═══════════════════════════════════════════════════════════
     Public API + init
     ═══════════════════════════════════════════════════════════ */
  const MediTopbar = {
    init() {
      wireBrowserEvents();
      applyBarState();
      startHealthMonitor();

      qs('tb-notif-btn')?.addEventListener('click', () => openPanel('notif'));
      qs('tb-push-btn') ?.addEventListener('click', () => openPanel('push'));
      qs('tb-task-btn') ?.addEventListener('click', () => openPanel('task'));

      qs('tb-notif-markall')?.addEventListener('click', markAllNotifRead);
      qs('tb-push-compose')?.addEventListener('click', openComposePush);
      qs('tb-task-compose') ?.addEventListener('click', openComposeTask);

      const soundToggle = qs('tb-sound-toggle');
      if (soundToggle) {
        soundToggle.checked = soundEnabled();
        soundToggle.addEventListener('change', () => setSoundEnabled(soundToggle.checked));
      }

      refreshCounts();
    },
    _notifClick: notifClick,
    _pushClick: pushClick,
    _toggleComposeTarget: toggleComposeTarget,
    _sendPush: sendPush,
    _completeTask: completeTask,
    _createTask: createTask,
  };

  w.MediTopbar = MediTopbar;
  d.addEventListener('DOMContentLoaded', () => MediTopbar.init());
})(window, document);
