// Network Notifier
// -----------------------------------------------------------------------------
// Surfaces docker-network drift across the WebUI — apps whose allocated static
// IP no longer falls inside the shared network's real subnet (the "network
// recreated with a different /24, app stranded" case). Two surfaces, shown ONLY
// when there's a real conflict to act on:
//   * a badge in the global topbar (after the update pill), and
//   * a banner on the dashboard.
// Both are driven by /data/system/network_status.json, written host-side by
// webuiSystemNetworkCheck (scripts/webui/data/generators/system/).
//
// Actions go through the normal task pipeline so the user can watch them stream
// on the Tasks page:
//   * "Heal now"  -> task `libreportal system network heal`  (re-IPs stranded
//                    apps from the corrected subnet; published ports preserved)
//   * "Re-check"  -> task `libreportal system network check`
//
// This file owns no detection logic — it only reads the status file.

class NetworkNotifier {
  constructor() {
    this.status = null;
    this.fetching = null;
    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 s = await fetch('/data/system/network_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
        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();

    // Topbar HTML and this script load independently; retry until
    // .topbar-controls exists so the badge appears regardless of 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 a network heal/check task finishes so the
    // badge clears without waiting for the next poll.
    window.taskRefresh?.register({
      id: 'network-badge',
      match: (d) => d.action === 'system_network_heal'
        || /^libreportal system network\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.renderTopbarBadge();
    this.refresh();
  }

  _hasConflicts() { return !!(this.status && this.status.conflicts_found === true); }

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

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

    let badge = document.getElementById('network-badge');

    if (!this._hasConflicts()) {
      if (badge) badge.remove();
      return;
    }

    if (!badge) {
      badge = document.createElement('button');
      badge.id = 'network-badge';
      badge.className = 'network-badge';
      badge.type = 'button';
      badge.addEventListener('click', () => this.openPanel());
      // Sit just after the update pill so the two coexist in a stable order —
      // update first, network second — rather than racing for firstChild.
      const anchor = document.getElementById('update-pill');
      if (anchor && anchor.parentNode === controls) controls.insertBefore(badge, anchor.nextSibling);
      else controls.insertBefore(badge, controls.firstChild);
    }

    const n = this.status.conflict_count || 0;
    badge.title = `Network attention needed${n ? ` — ${n} item${n === 1 ? '' : 's'}` : ''}`;
    badge.setAttribute('aria-label', badge.title);
    badge.innerHTML = `
      <span class="network-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">
        <rect x="2" y="2" width="8" height="8" rx="1"></rect>
        <rect x="14" y="14" width="8" height="8" rx="1"></rect>
        <path d="M6 10v4a2 2 0 0 0 2 2h6"></path>
      </svg>
      <span class="network-badge-text">Network</span>`;
  }

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

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

    let banner = document.getElementById('network-banner');

    // Attention-only: render nothing unless there's a real conflict.
    if (!this._hasConflicts()) {
      if (banner) banner.remove();
      return;
    }

    if (!banner) {
      banner = document.createElement('div');
      banner.id = 'network-banner';
      // Attention-only, so it leads the dashboard when a conflict is present.
      main.insertBefore(banner, main.firstChild);
    }

    const s = this.status;
    const n = s.conflict_count || 0;
    const net = s.network_name || 'docker';
    const sub = s.docker_subnet
      ? `${n} item${n === 1 ? '' : 's'} not on the ${this._escape(net)} network (${this._escape(s.docker_subnet)})`
      : (s.error ? this._escape(s.error) : `${n} network item${n === 1 ? '' : 's'} need attention`);

    const netIcon = `
      <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <rect x="2" y="2" width="8" height="8" rx="1"></rect>
        <rect x="14" y="14" width="8" height="8" rx="1"></rect>
        <path d="M6 10v4a2 2 0 0 0 2 2h6"></path>
      </svg>`;

    banner.className = 'network-banner';
    banner.innerHTML = `
      <div class="network-banner-icon" aria-hidden="true">${netIcon}</div>
      <div class="network-banner-text">
        <div class="network-banner-title">Network attention needed</div>
        <div class="network-banner-sub">${sub}</div>
      </div>
      <div class="network-banner-actions">
        <button type="button" class="network-btn-secondary" id="network-banner-details">Details</button>
        ${s.can_auto_heal ? '<button type="button" class="network-btn-primary" id="network-banner-heal">Heal now</button>' : ''}
      </div>`;

    const details = banner.querySelector('#network-banner-details');
    if (details) details.addEventListener('click', () => this.openPanel());
    const healBtn = banner.querySelector('#network-banner-heal');
    if (healBtn) healBtn.addEventListener('click', () => this.runHeal());
  }

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

  openPanel() {
    this.closePanel();
    const s = this.status || {};
    const apps = Array.isArray(s.apps) ? s.apps : [];

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

    const rows = [
      ['Network', s.network_name || '—'],
      ['Docker subnet', s.docker_subnet || '—'],
      ['Config subnet', s.config_subnet || '—'],
      ['Conflicts', (s.conflict_count ?? 0).toString()],
      ['Last checked', this._formatTime(s.checked_at)],
    ];

    const appList = apps.length
      ? `<div class="network-panel-apps">
           ${apps.map(a => `
             <div class="network-panel-app">
               <span class="network-panel-app-name">${this._escape(a.app)}${a.service && a.service !== a.app ? ` / ${this._escape(a.service)}` : ''}</span>
               <span class="network-panel-app-ip">${this._escape(a.stored_ip || '')}</span>
             </div>`).join('')}
         </div>`
      : '';

    const statusLine = this._hasConflicts()
      ? 'Some apps have a static IP outside the docker network and can’t be reached until re-IP’d.'
      : 'No network conflicts detected.';

    overlay.innerHTML = `
      <div class="network-panel" role="dialog" aria-modal="true" aria-label="Network status">
        <div class="network-panel-header">
          <h3>Network Status</h3>
          <button type="button" class="network-panel-close" id="network-panel-close" aria-label="Close">&times;</button>
        </div>
        <div class="network-panel-status ${this._hasConflicts() ? 'is-conflict' : ''}">${this._escape(statusLine)}</div>
        ${s.error ? `<div class="network-panel-error">${this._escape(s.error)}</div>` : ''}
        <dl class="network-panel-rows">
          ${rows.map(([k, v]) => `<div class="network-panel-row"><dt>${this._escape(k)}</dt><dd>${this._escape(v)}</dd></div>`).join('')}
        </dl>
        ${appList}
        ${s.can_auto_heal && this._hasConflicts() ? '<p class="network-panel-note">Healing re-assigns each stranded app a fresh IP from the current subnet and restarts it. Published host ports are preserved. Progress streams on the Tasks page.</p>' : ''}
        <div class="network-panel-actions">
          <button type="button" class="network-btn-secondary" id="network-panel-check">Re-check</button>
          ${s.can_auto_heal && this._hasConflicts() ? '<button type="button" class="network-btn-primary" id="network-panel-heal">Heal now</button>' : ''}
        </div>
      </div>`;

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

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

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

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

  async runHeal() {
    this.closePanel();
    try {
      await this._createTask('libreportal system network heal');
      this._toast('Network heal started — follow progress on the Tasks page.', 'info');
      this._goToTasks();
    } catch (e) {
      this._toast('Could not start the network heal: ' + e.message, 'error');
    }
  }

  async checkNow() {
    try {
      await this._createTask('libreportal system network check');
      this._toast('Re-checking the network…', 'info');
      setTimeout(() => this.refresh(), 4000);
    } catch (e) {
      this._toast('Could not re-check the network: ' + e.message, 'error');
    }
  }

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

  async _createTask(command) {
    if (window.tasksManager?.taskManager?.createTask) {
      return window.tasksManager.taskManager.createTask(command, 'system_network_heal', null, '');
    }
    if (typeof TaskManager !== 'undefined') {
      return new TaskManager().createTask(command, 'system_network_heal', null, '');
    }
    const res = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command, type: 'system_network_heal', 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(`[network] ${message}`);
  }

  _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.networkNotifier = window.networkNotifier || new NetworkNotifier();

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