// Tailscale device list panel for status.sami
//
// Polls /api/v1/tailscale/status (richer payload: online + offline peers with
// lastSeen, OS, user) every 30s and renders a device card. Clicking the card
// opens a panel that lists every device known to the tailnet — online ones
// first, with "last seen Xm ago" for offline ones.
//
// Self-contained so it doesn't need to be in dist/. Just needs secureFetch,
// escapeHtml, and timeAgo (all from dist/core.js, which loads first).

(function () {
  'use strict';

  const REFRESH_MS = 30 * 1000;          // status doesn't change fast — 30s is plenty
  const STATUS_URL = '/api/v1/tailscale/status';
  const DEVICES_URL = '/api/v1/tailscale/devices';

  const OS_ICON = {
    windows: '🪟',
    linux:   '🐧',
    darwin:  '🍎',
    android: '🤖',
    ios:     '📱',
    macos:   '🍎',
    synology:'💾',
    other:   '🖥️',
  };

  function osKey(raw) {
    if (!raw) return 'other';
    const s = String(raw).toLowerCase();
    if (s.includes('windows')) return 'windows';
    if (s.includes('darwin') || s.includes('macos') || s.includes('mac')) return 'darwin';
    if (s.includes('android')) return 'android';
    if (s.includes('ios')) return 'ios';
    if (s.includes('linux')) return 'linux';
    if (s.includes('synology') || s.includes('dsm')) return 'synology';
    return 'other';
  }

  // Module-local cache so the panel render doesn't refetch when the user toggles
  // between card and panel view.
  let cache = null;        // last successful /status payload
  let lastFetch = 0;       // ms timestamp of last successful fetch
  let pollTimer = null;
  let inFlight = null;     // promise of in-flight fetch (deduplicate overlapping calls)

  function $ (id) { return document.getElementById(id); }

  function setCard (onlineCount, totalCount, connected, installed) {
    const card = $('tailscale-card');
    const dot = $('tailscale-dot');
    const pill = $('tailscale-pill');
    const summary = $('tailscale-summary');
    if (!card || !dot || !pill || !summary) return;

    if (!installed) {
      card.setAttribute('data-status', 'off');
      dot.className = 'dot bad at-bl';
      pill.className = 'badge off';
      pill.textContent = 'N/A';
      summary.textContent = 'Tailscale not installed';
      return;
    }
    if (!connected) {
      card.setAttribute('data-status', 'off');
      dot.className = 'dot bad at-bl';
      pill.className = 'badge off';
      pill.textContent = 'OFF';
      summary.textContent = 'Daemon offline';
      return;
    }
    card.setAttribute('data-status', 'on');
    dot.className = 'dot ok at-bl';
    pill.className = 'badge on';
    pill.textContent = onlineCount + '/' + totalCount;
    summary.textContent = onlineCount === 1
      ? '1 device online'
      : onlineCount + ' devices online';
  }

  function deviceRow (dev) {
    const online = !!dev.online;
    const isSelf = !!dev.isSelf;
    const osIcon = OS_ICON[osKey(dev.os)] || OS_ICON.other;
    const host = escapeHtml(dev.hostname || dev.id || '(unknown)');
    const ip = dev.ip ? escapeHtml(dev.ip) : '';
    const user = dev.user ? escapeHtml(String(dev.user).split('@')[0]) : '';
    const lastSeen = (!online && dev.lastSeen)
      ? '<span style="color: var(--muted); font-size: 0.75rem; margin-left: 6px;">last seen ' + escapeHtml(timeAgo(dev.lastSeen)) + '</span>'
      : '';
    const selfBadge = isSelf
      ? '<span style="background: var(--accent); color: var(--bg); padding: 1px 6px; border-radius: 4px; font-size: 0.65rem; font-weight: 700; margin-left: 6px; letter-spacing: 0.5px;">THIS DEVICE</span>'
      : '';

    return `
      <div class="ts-device" data-online="${online ? '1' : '0'}" style="
        display: flex; align-items: center; gap: 10px;
        padding: 10px 12px; border-radius: 8px;
        background: var(--card-bg);
        border: 1px solid var(--border);
        opacity: ${online ? '1' : '0.55'};
      ">
        <span style="font-size: 1.4rem; line-height: 1;">${osIcon}</span>
        <div style="flex: 1; min-width: 0; overflow: hidden;">
          <div style="display: flex; align-items: center; min-width: 0;">
            <span style="font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${host}</span>${selfBadge}
          </div>
          <div style="display: flex; align-items: center; gap: 6px; margin-top: 2px; min-width: 0;">
            <span style="
              display: inline-block; width: 7px; height: 7px; border-radius: 50%;
              background: ${online ? 'var(--uptime)' : 'var(--bad-fg)'};
              box-shadow: 0 0 ${online ? '6px' : '0'} ${online ? 'var(--uptime)' : 'transparent'};
              flex-shrink: 0;
            "></span>
            <span style="font-size: 0.75rem; color: ${online ? 'var(--uptime)' : 'var(--bad-fg)'}; font-weight: 600;">
              ${online ? 'ONLINE' : 'OFFLINE'}
            </span>
            ${user ? '<span style="color: var(--muted); font-size: 0.75rem;">·</span><span style="color: var(--muted); font-size: 0.75rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + user + '</span>' : ''}
            ${ip ? '<span style="color: var(--muted); font-size: 0.75rem;">·</span><code style="color: var(--muted); font-size: 0.7rem; font-family: ui-monospace, monospace;">' + ip + '</code>' : ''}
            ${lastSeen}
          </div>
        </div>
      </div>
    `;
  }

  function renderPanel (status) {
    const list = $('tailscale-device-list');
    const count = $('tailscale-panel-count');
    const tailnet = $('tailscale-tailnet');
    if (!list) return;

    if (!status || !status.installed) {
      list.innerHTML = '<div style="color: var(--muted); padding: 20px; text-align: center;">Tailscale is not installed on this host.</div>';
      if (count) count.textContent = '';
      if (tailnet) tailnet.textContent = '';
      return;
    }
    if (!status.connected) {
      list.innerHTML = '<div style="color: var(--muted); padding: 20px; text-align: center;">Tailscale daemon is not running. Start it with <code>systemctl start tailscaled</code>.</div>';
      if (count) count.textContent = '';
      if (tailnet) tailnet.textContent = '';
      return;
    }

    // Build merged device list: self first, then online peers, then offline peers
    const devices = [];
    if (status.self && status.self.ip) {
      devices.push({
        id: 'self', hostname: status.self.hostname, ip: status.self.ip,
        os: status.self.os, online: !!status.self.online, isSelf: true,
        lastSeen: status.self.online ? null : null,
      });
    }
    const onlinePeers = [];
    const offlinePeers = [];
    for (const dev of (status.devices || [])) {
      if (dev.isSelf) continue; // already added above
      const entry = {
        id: dev.id, hostname: dev.hostname, ip: dev.ip, os: dev.os,
        online: !!dev.online, user: dev.user,
        lastSeen: dev.online ? null : dev.lastSeen,
      };
      if (entry.online) onlinePeers.push(entry); else offlinePeers.push(entry);
    }
    // Sort: hostname alpha within each bucket
    onlinePeers.sort((a, b) => (a.hostname || '').localeCompare(b.hostname || ''));
    offlinePeers.sort((a, b) => {
      // Most-recently-seen first for offline
      const ta = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
      const tb = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
      return tb - ta;
    });
    const sorted = devices.concat(onlinePeers, offlinePeers);

    if (sorted.length === 0) {
      list.innerHTML = '<div style="color: var(--muted); padding: 20px; text-align: center;">No devices found in this tailnet.</div>';
    } else {
      list.innerHTML = sorted.map(deviceRow).join('');
    }

    const onlineCount = sorted.filter(d => d.online).length;
    if (count) {
      count.textContent = sorted.length === 1
        ? '1 device total'
        : sorted.length + ' devices total · ' + onlineCount + ' online';
    }
    if (tailnet) {
      const tn = status.self && status.self.tailnetName;
      tailnet.textContent = tn ? '(' + tn + ')' : '';
    }
  }

  async function fetchStatus (force) {
    // Dedupe overlapping calls + skip if we have a fresh cache
    const now = Date.now();
    if (!force && cache && (now - lastFetch) < REFRESH_MS) return cache;
    if (inFlight) return inFlight;

    inFlight = (async () => {
      try {
        const res = await secureFetch(STATUS_URL, { cache: 'no-store' });
        if (!res.ok) throw new Error('status ' + res.status);
        const data = await res.json();
        cache = data;
        lastFetch = Date.now();
        return data;
      } catch (e) {
        // If /status fails, the container might be on an older build without it.
        // Fall back to /devices (smaller payload, online only).
        try {
          const res2 = await secureFetch(DEVICES_URL, { cache: 'no-store' });
          if (!res2.ok) throw new Error('devices ' + res2.status);
          const d = await res2.json();
          cache = {
            installed: true,
            connected: true,
            self: d.devices && d.devices.find(x => x.isSelf),
            devices: (d.devices || []).filter(x => !x.isSelf),
          };
          lastFetch = Date.now();
          return cache;
        } catch (e2) {
          // Surface a graceful "unknown" state rather than throwing
          if (typeof errorHandler !== 'undefined') {
            errorHandler.logError('[Tailscale] Fetch', e2, { function: 'fetchStatus' });
          }
          return null;
        }
      } finally {
        inFlight = null;
      }
    })();
    return inFlight;
  }

  async function refresh () {
    const status = await fetchStatus(false);
    if (!status) {
      setCard(0, 0, false, false);
      return;
    }
    const installed = !!status.installed;
    const connected = !!status.connected;
    const total = (status.devices ? status.devices.length : 0) + (status.self ? 1 : 0);
    const online = (status.devices ? status.devices.filter(d => d.online).length : 0)
                 + (status.self && status.self.online ? 1 : 0);
    setCard(online, total, connected, installed);
    // Only re-render the panel if it's currently open (saves DOM thrash)
    const panel = $('tailscale-panel');
    if (panel && panel.style.display !== 'none') {
      renderPanel(status);
    }
  }

  function togglePanel () {
    const panel = $('tailscale-panel');
    if (!panel) return;
    const isOpen = panel.style.display !== 'none';
    if (isOpen) {
      panel.style.display = 'none';
    } else {
      panel.style.display = '';
      // Force a refresh when opening so the user always sees fresh data
      fetchStatus(true).then(renderPanel);
    }
  }

  function init () {
    // Card click → toggle panel (skip if click was on the refresh button)
    const card = $('tailscale-card');
    if (card) {
      card.addEventListener('click', (e) => {
        if (e.target && e.target.id === 'tailscale-refresh-btn') return;
        togglePanel();
      });
    }
    // Cursor hint
    if (card) card.style.cursor = 'pointer';

    // Refresh button → force fetch
    const btn = $('tailscale-refresh-btn');
    if (btn) {
      btn.addEventListener('click', async (e) => {
        e.stopPropagation();
        btn.disabled = true;
        const orig = btn.innerHTML;
        btn.innerHTML = '…';
        try {
          const status = await fetchStatus(true);
          if (status) {
            renderPanel(status);
            // Update card too
            const total = (status.devices ? status.devices.length : 0) + (status.self ? 1 : 0);
            const online = (status.devices ? status.devices.filter(d => d.online).length : 0)
                         + (status.self && status.self.online ? 1 : 0);
            setCard(online, total, status.connected, status.installed);
          }
        } finally {
          btn.innerHTML = orig;
          btn.disabled = false;
        }
      });
    }

    // Close button on panel
    const closeBtn = $('tailscale-panel-close');
    if (closeBtn) {
      closeBtn.addEventListener('click', () => {
        const panel = $('tailscale-panel');
        if (panel) panel.style.display = 'none';
      });
    }

    // Initial fetch + periodic poll
    refresh();
    if (pollTimer) clearInterval(pollTimer);
    pollTimer = setInterval(refresh, REFRESH_MS);
  }

  // Re-init if the card is later rendered (e.g. after TOTP auth completes)
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    // Defer slightly to let dist/core.js's secureFetch be defined
    setTimeout(init, 0);
  }
})();
