// Update Notifier
// -----------------------------------------------------------------------------
// Surfaces LibrePortal's version + "out of date" state in one persistent pill
// in the global topbar (visible on every page). The pill stays calm by default —
// the installed version, or "Up to date" — and only escalates to an accented,
// pulsing "Update" when one is actually waiting. Clicking it opens a details
// modal carrying the full version readout and the update / check actions.
//
// Driven by /data/system/update_status.json (written host-side by
// webuiSystemUpdateCheck — see scripts/webui/data/generators/system/).
//
// Both actions 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;
    this._checking = false;        // a manual "check for updates" is in flight
  }

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

  async fetchStatus() {
    if (this.fetching) return this.fetching;
    this.fetching = (async () => {
      try {
        const [s, v] = await Promise.all([
          fetch('/data/system/update_status.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : null).catch(() => null),
          fetch('/data/system/verify_status.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : null).catch(() => null),
        ]);
        if (s !== null) this.status = s;   // keep last-good on a failed fetch
        if (v !== null) this.verify = v;
        return this.status;
      } catch {
        return null;
      } finally {
        this.fetching = null;
      }
    })();
    return this.fetching;
  }

  async refresh() {
    await this.fetchStatus();
    this.renderTopbarPill();
  }

  // ---- 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.renderTopbarPill(); 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 task finishes so the badge clears
    // (or the version updates) without waiting for the next poll. Registered
    // with the task-refresh coordinator (single source of truth); the debounce
    // gives the host a beat to finish writing update_status.json.
    window.taskRefresh?.register({
      id: 'update-pill',
      match: (d) => d.action === 'update' || d.action === 'system_update'
        || /^libreportal update\b/.test((d.task && d.task.command) || d.command || ''),
      run: () => this.refresh(),
      debounceMs: 1500,
    });
  }

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

  // ---- topbar pill ---------------------------------------------------------
  // One persistent status control in the global topbar, on every page. Calm by
  // default (the installed version, or "Up to date"); escalates to an accented,
  // pulsing "Update" only when one is actually waiting, and shows a spinner
  // while a manual check runs. Clicking it opens the details modal.
  renderTopbarPill() {
    const controls = document.querySelector('.topbar-controls');
    if (!controls) return;

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

    // Nothing known yet (fresh install before the first check) — show nothing.
    if (!s) { if (pill) pill.remove(); return; }

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

    const refreshIcon = `<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>`;
    const checkIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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>`;
    const boxIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`;

    const ver = (s.current_version && s.current_version !== 'unknown') ? `v${s.current_version}` : '';
    const local = s.source === 'local' || s.install_mode === 'local';

    let state, icon, text, title, dot = '';
    if (this._checking) {
      state = 'is-checking'; icon = refreshIcon; text = 'Checking…';
      title = 'Checking for updates…';
    } else if (s.update_available) {
      state = 'is-update'; icon = refreshIcon; text = 'Update';
      dot = '<span class="update-pill-dot" aria-hidden="true"></span>';
      const to = this._versionLabel();
      title = `Update available${to ? ' — ' + to : ''}`;
    } else if (local) {
      // Local/dev install: no remote to check against — show the version plainly.
      state = 'is-local'; icon = boxIcon; text = ver || 'Local';
      title = `Local installation — updates are managed manually${ver ? ' · ' + ver : ''}`;
    } else {
      state = 'is-ok'; icon = checkIcon; text = 'Up to date';
      title = `LibrePortal${ver ? ' ' + ver : ''} · up to date`;
    }

    pill.className = `update-pill ${state}`;
    pill.title = title;
    pill.setAttribute('aria-label', title);
    pill.innerHTML = `${dot}${icon}<span class="update-pill-text">${this._escape(text)}</span>`;
  }

  // ---- 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 integrity = this._integrityLabel();
    if (integrity) rows.splice(2, 0, ['Integrity', integrity]);

    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() {
    this._checking = true;
    this.renderTopbarPill();
    try {
      await this._createTask('libreportal update check');
      this._toast('Checking for updates…', 'info');
      // The check task rewrites update_status.json; clear the spinner and
      // re-read shortly after so the pill settles on the fresh state.
      setTimeout(() => { this._checking = false; this.refresh(); }, 4000);
    } catch (e) {
      this._checking = false;
      this.renderTopbarPill();
      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}`);
  }

  _integrityLabel() {
    switch (this.verify && this.verify.state) {
      case 'verified':     return 'Verified · matches signed release';
      case 'modified':     return `Modified (${this.verify.files_modified || 0} changed)`;
      case 'tampered':     return 'Signature invalid';
      case 'unsigned':     return 'Unsigned build';
      case 'unverifiable': return 'Can’t verify';
      case 'development':  return 'Development build';
      default:             return '';
    }
  }

  _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();
}
