// Update Notifier
// -----------------------------------------------------------------------------
// Surfaces LibrePortal's "out of date" state across the WebUI:
//   * a persistent badge in the global topbar (visible on every page), and
//   * a banner on the dashboard,
// both driven by /data/system/update_status.json (written host-side by
// webuiSystemUpdateCheck — see scripts/webui/data/generators/system/).
//
// The two actions both go through the normal task pipeline so the user can
// watch them stream on the Tasks page, exactly like an app install:
//   * "Update now"        -> task `libreportal update apply`
//   * "Check for updates"  -> task `libreportal update check`
//
// This file owns no detection logic of its own. When get.libreportal.org is
// wired up, only the host-side generator changes; this stays as-is.

class UpdateNotifier {
  constructor() {
    this.status = null;
    this.fetching = null;          // de-dupe concurrent fetches
    this.pollMs = 5 * 60 * 1000;   // re-read the status file every 5 min
    this.pollTimer = null;
    this.started = false;
  }

  // ---- data ----------------------------------------------------------------

  async fetchStatus() {
    if (this.fetching) return this.fetching;
    this.fetching = (async () => {
      try {
        const res = await fetch('/data/system/update_status.json', { cache: 'no-store' });
        if (!res.ok) return null;
        this.status = await res.json();
        return this.status;
      } catch {
        return null;
      } finally {
        this.fetching = null;
      }
    })();
    return this.fetching;
  }

  async refresh() {
    await this.fetchStatus();
    this.renderTopbarBadge();
    this.renderDashboardBanner();
  }

  // ---- lifecycle -----------------------------------------------------------

  start() {
    if (this.started) return;
    this.started = true;

    this.refresh();

    // The topbar HTML and this script load independently; if the topbar
    // mounted first, the authoritative onTopbarReady() call already no-op'd.
    // Briefly retry until .topbar-controls exists so the badge appears on
    // first load regardless of which won the race.
    let tries = 0;
    const ensure = setInterval(() => {
      if (document.querySelector('.topbar-controls')) { this.renderTopbarBadge(); clearInterval(ensure); }
      else if (++tries > 30) clearInterval(ensure); // ~15s ceiling
    }, 500);

    if (this.pollTimer) clearInterval(this.pollTimer);
    this.pollTimer = setInterval(() => this.refresh(), this.pollMs);

    // Re-read the status as soon as an update/check task finishes so the badge
    // clears (or the version updates) without waiting for the next poll.
    const onTask = (event) => {
      const cmd = event?.detail?.command || event?.detail?.task?.command || '';
      const action = event?.detail?.action;
      if (/^libreportal update\b/.test(cmd) || action === 'update') {
        // Give the host a beat to finish writing update_status.json.
        setTimeout(() => this.refresh(), 1500);
      }
    };
    window.addEventListener('taskCompleted', onTask);
    window.addEventListener('taskUpdated', onTask);
  }

  // Called by TopbarComponent.init() once the topbar DOM exists.
  onTopbarReady() {
    this.renderTopbarBadge();
    this.refresh();
  }

  // ---- topbar badge --------------------------------------------------------

  renderTopbarBadge() {
    const controls = document.querySelector('.topbar-controls');
    if (!controls) return;

    let badge = document.getElementById('update-badge');
    const show = this.status && this.status.update_available === true;

    if (!show) {
      if (badge) badge.remove();
      return;
    }

    if (!badge) {
      badge = document.createElement('button');
      badge.id = 'update-badge';
      badge.className = 'update-badge';
      badge.type = 'button';
      badge.addEventListener('click', () => this.openPanel());
      controls.insertBefore(badge, controls.firstChild);
    }

    const to = this._versionLabel();
    badge.title = `Update available${to ? ' — ' + to : ''}`;
    badge.setAttribute('aria-label', badge.title);
    badge.innerHTML = `
      <span class="update-badge-dot" aria-hidden="true"></span>
      <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
        <path d="M21 2v6h-6"></path>
        <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
        <path d="M3 22v-6h6"></path>
        <path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
      </svg>
      <span class="update-badge-text">Update</span>`;
  }

  // ---- dashboard banner ----------------------------------------------------

  renderDashboardBanner() {
    const main = document.querySelector('.dashboard-main');
    if (!main) return; // not on the dashboard

    let banner = document.getElementById('update-banner');
    const s = this.status;

    // No status file yet (fresh install before the first check) — show nothing.
    if (!s) {
      if (banner) banner.remove();
      return;
    }

    if (!banner) {
      banner = document.createElement('div');
      banner.id = 'update-banner';
      main.insertBefore(banner, main.firstChild);
    }

    const refreshIcon = `
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M21 2v6h-6"></path>
        <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
        <path d="M3 22v-6h6"></path>
        <path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
      </svg>`;
    const checkIcon = `
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
        <polyline points="22 4 12 14.01 9 11.01"></polyline>
      </svg>`;

    if (s.update_available) {
      // Prominent: an update is waiting.
      const versionLine = this._versionLabel() || `${s.behind} update${s.behind === 1 ? '' : 's'} behind`;
      const behindLine = s.behind > 0 ? `${s.behind} commit${s.behind === 1 ? '' : 's'} behind` : '';
      banner.className = 'update-banner';
      banner.innerHTML = `
        <div class="update-banner-icon" aria-hidden="true">${refreshIcon}</div>
        <div class="update-banner-text">
          <div class="update-banner-title">A LibrePortal update is available</div>
          <div class="update-banner-sub">${this._escape(versionLine)}${behindLine ? ` &middot; ${this._escape(behindLine)}` : ''}</div>
        </div>
        <div class="update-banner-actions">
          <button type="button" class="update-btn-secondary" id="update-banner-details">Details</button>
          ${s.can_update ? '<button type="button" class="update-btn-primary" id="update-banner-update">Update now</button>' : ''}
        </div>`;
    } else {
      // Subtle: up to date (or a local install). Still gives a version readout
      // and a manual "Check for updates" entry point.
      const local = s.source === 'local' || s.install_mode === 'local';
      const verText = (s.current_version && s.current_version !== 'unknown') ? `LibrePortal v${s.current_version}` : 'LibrePortal';
      const subText = local ? 'Local installation — updates are managed manually' : 'Up to date';
      banner.className = 'update-banner update-banner-ok';
      banner.innerHTML = `
        <div class="update-banner-icon" aria-hidden="true">${checkIcon}</div>
        <div class="update-banner-text">
          <div class="update-banner-title">${this._escape(verText)}</div>
          <div class="update-banner-sub">${this._escape(subText)}</div>
        </div>
        <div class="update-banner-actions">
          <button type="button" class="update-btn-secondary" id="update-banner-details">Details</button>
          ${local ? '' : '<button type="button" class="update-btn-secondary" id="update-banner-check">Check for updates</button>'}
        </div>`;
    }

    const details = banner.querySelector('#update-banner-details');
    if (details) details.addEventListener('click', () => this.openPanel());
    const updateBtn = banner.querySelector('#update-banner-update');
    if (updateBtn) updateBtn.addEventListener('click', () => this.runUpdate());
    const checkBtn = banner.querySelector('#update-banner-check');
    if (checkBtn) checkBtn.addEventListener('click', () => this.checkNow());
  }

  // ---- details panel (self-contained modal) --------------------------------

  openPanel() {
    this.closePanel();
    const s = this.status || {};

    const overlay = document.createElement('div');
    overlay.id = 'update-panel-overlay';
    overlay.className = 'update-panel-overlay';
    overlay.addEventListener('click', (e) => { if (e.target === overlay) this.closePanel(); });

    const local = s.source === 'local' || s.install_mode === 'local';
    const rows = [
      ['Installed version', s.current_version || 'unknown'],
      ['Latest version', s.latest_version || 'unknown'],
      ['Commits behind', (s.behind ?? 0).toString()],
      ['Branch', s.branch || '—'],
      ['Last checked', this._formatTime(s.checked_at)],
    ];

    const statusLine = local
      ? 'This is a local installation — updates are managed manually on the host.'
      : (s.update_available
          ? 'An update is available.'
          : 'LibrePortal is up to date.');

    overlay.innerHTML = `
      <div class="update-panel" role="dialog" aria-modal="true" aria-label="LibrePortal updates">
        <div class="update-panel-header">
          <h3>LibrePortal Updates</h3>
          <button type="button" class="update-panel-close" id="update-panel-close" aria-label="Close">&times;</button>
        </div>
        <div class="update-panel-status ${s.update_available ? 'is-outdated' : ''}">${this._escape(statusLine)}</div>
        ${s.error ? `<div class="update-panel-error">${this._escape(s.error)}</div>` : ''}
        <dl class="update-panel-rows">
          ${rows.map(([k, v]) => `<div class="update-panel-row"><dt>${this._escape(k)}</dt><dd>${this._escape(v)}</dd></div>`).join('')}
        </dl>
        ${s.can_update && s.update_available ? '<p class="update-panel-note">Updating backs up your configuration, pulls the latest version, and restarts LibrePortal. Progress streams on the Tasks page.</p>' : ''}
        <div class="update-panel-actions">
          <button type="button" class="update-btn-secondary" id="update-panel-check">Check for updates</button>
          ${s.can_update && s.update_available ? '<button type="button" class="update-btn-primary" id="update-panel-update">Update now</button>' : ''}
        </div>
      </div>`;

    document.body.appendChild(overlay);
    overlay.querySelector('#update-panel-close').addEventListener('click', () => this.closePanel());
    overlay.querySelector('#update-panel-check').addEventListener('click', () => this.checkNow());
    const upd = overlay.querySelector('#update-panel-update');
    if (upd) upd.addEventListener('click', () => this.runUpdate());

    this._escHandler = (e) => { if (e.key === 'Escape') this.closePanel(); };
    document.addEventListener('keydown', this._escHandler);
  }

  closePanel() {
    const overlay = document.getElementById('update-panel-overlay');
    if (overlay) overlay.remove();
    if (this._escHandler) {
      document.removeEventListener('keydown', this._escHandler);
      this._escHandler = null;
    }
  }

  // ---- actions -------------------------------------------------------------

  async runUpdate() {
    this.closePanel();
    try {
      await this._createTask('libreportal update apply');
      this._toast('LibrePortal update started — follow progress on the Tasks page.', 'info');
      this._goToTasks();
    } catch (e) {
      this._toast('Could not start the update: ' + e.message, 'error');
    }
  }

  async checkNow() {
    try {
      await this._createTask('libreportal update check');
      this._toast('Checking for updates…', 'info');
      // The check task rewrites update_status.json; refresh shortly after.
      setTimeout(() => this.refresh(), 4000);
    } catch (e) {
      this._toast('Could not check for updates: ' + e.message, 'error');
    }
  }

  // ---- helpers -------------------------------------------------------------

  async _createTask(command) {
    if (window.tasksManager?.taskManager?.createTask) {
      return window.tasksManager.taskManager.createTask(command, 'update', null, '');
    }
    if (typeof TaskManager !== 'undefined') {
      return new TaskManager().createTask(command, 'update', null, '');
    }
    const res = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command, type: 'update', app: null, config: '' })
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }

  _goToTasks() {
    if (window.librePortalSPA?.navigate) window.librePortalSPA.navigate('/tasks');
    else if (typeof navigateToRoute === 'function') navigateToRoute('/tasks');
    else window.location.href = '/tasks';
  }

  _toast(message, type = 'info') {
    const ns = window.notificationSystem || window.ensureNotificationSystem?.();
    if (ns?.show) ns.show(message, type);
    else console.log(`[update] ${message}`);
  }

  _versionLabel() {
    const s = this.status;
    if (!s) return '';
    const cur = s.current_version, latest = s.latest_version;
    if (cur && latest && cur !== latest && latest !== 'unknown') return `v${cur} → v${latest}`;
    if (latest && latest !== 'unknown') return `v${latest}`;
    return '';
  }

  _formatTime(iso) {
    if (!iso) return '—';
    const d = new Date(iso);
    if (isNaN(d.getTime())) return '—';
    return d.toLocaleString();
  }

  _escape(str) {
    return String(str).replace(/[&<>"']/g, (c) => (
      { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
    ));
  }
}

window.updateNotifier = window.updateNotifier || new UpdateNotifier();

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => window.updateNotifier.start());
} else {
  window.updateNotifier.start();
}
