/**
 * Cyber-Assistant — Presence Manager
 *
 * Affichage des utilisateurs connectés (indicateur header + badges sur les onglets).
 * Ping toutes les 30s, TTL présence 90s côté serveur.
 *
 * Coordination supplémentaire : ce module propage l'état du verrou d'édition global
 * (champ `editLock` retourné par presence-ping) vers `CSI_EditLockManager` — voir
 * modules/edit-lock-manager.js pour l'arbitrage de l'édition concurrente.
 *
 * Inerte en mode local (file://).
 *
 * Dépend de : auth-manager.js, edit-lock-manager.js (optionnel mais recommandé).
 */
(function() {
  'use strict';

  // ── Tab ↔ appData keys mapping (inchangé — utilisé par bundle.js pour la sync) ──
  var TAB_DATA_KEYS = {
    'controls':          ['controls'],
    'nis2-controls':     ['nis2Controls'],
    'recyf-controls':    ['recyfControls'],
    'nis2-program':      ['nis2Plan'],
    // FIX-ACTIONID-SYNC (audit 2026-06-10) : nextActionId fait partie des données de
    // l'onglet actions — sans lui, le compteur n'était jamais poussé au serveur et les
    // nouvelles actions réutilisaient des ids existants après rechargement (édition du
    // mauvais élément, suppression double).
    'actions':           ['actions', 'actionsHistory', 'nextActionId'],
    'risks':             ['risks'],
    'threats-section':   ['threats'],
    'nonconformities':   ['nonconformities'],
    'soa':               ['soa'],
    'docReview':         ['documentReview'],
    'audits':            ['audits'],
    'managementReviews': ['reviews'],
    'stakeholders':      ['stakeholders'],
    'documents':         ['documents'],
    'kpis':              ['kpis'],
    'critical-assets':   ['criticalAssets', 'criticalAssetsMeta'],
    'project-risks':     ['projectRisks'],
    'nis2-socle':        ['soclePillars'],
    'risk-analysis':     ['riskAnalyses', 'riskThresholds'],
    'impact-matrices':   ['impactMatrices'],
    // FIX-CONFIG-CHANNEL (Lot 3, 2026-07-17) : miroir STRICT de CSI_SECTION_KEYS['app-config']
    // (entities-core.php). Le serveur accepte depuis toujours 'app-config' comme section de
    // la grappe 'config' (data-push/-multi, fusion au GET) — le client ne l'utilisait jamais
    // et poussait la configuration par un POST import complet (4 grappes exigées, bump
    // d'époque qui déclenchait des 409 chez les collègues). Ce canal ferme ce trou.
    // 'app-config' n'est PAS un onglet d'interface : jamais _currentActiveTab, seulement
    // une cible de _tabsForKeys/_csiPersistAffectedTabs et du canal de configuration.
    'app-config':        ['settings', 'title', 'targetMaturity', 'frameworkMapper',
                          'cyfunLevel', 'cyfunLang', 'nis2Referential', 'recyfLevel',
                          '_cyfunEvalArchive', '_cyfunLevelChosen', '_cyfunLevelLocked',
                          '_nis2ReferentialLocked']
  };

  // === CSI-BUILD === Estampille de build du CLIENT. À BUMPER À CHAQUE LIVRAISON, en même
  // temps que les cache-busters d'index.html et que CSI_SERVER_BUILD (sync.php) — parité
  // verrouillée par TOOLS/tests/build-parity.test.js. Comparée au champ serverBuild du
  // presence-ping : un onglet resté ouvert pendant une mise à jour serveur est invité à
  // recharger (bandeau, jamais de reload forcé) en ≤ 30 s.
  var CSI_CLIENT_BUILD = '20260717e';

  var PRESENCE_INTERVAL = 30000; // 30s (vs 7s avant)
  var _pingTimer = null;
  var _connectedUsers = [];
  var _activeTab = '';
  var _listeners = { visChange: null, online: null, offline: null, unload: null };

  // ── Helpers ──

  function _getAuthHeaders() {
    var AM = window.CSI_AuthManager;
    if (!AM || !AM.getToken) return null;
    var token = AM.getToken();
    if (!token) return null;
    return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
  }

  function _isServerMode() {
    var S = window.CSI_Storage;
    return S && S.isServerMode && S.isServerMode();
  }

  // ── Presence Ping ──

  // === CSI-ME === (audit co-édition 2026-07-03, CRITIQUE) presence scopée au périmètre
  // actif : sans entity=, le bandeau « Édité par X » et l'état du verrou provenaient de la
  // partition PRINCIPALE quel que soit le périmètre affiché (édition d'une partition libre
  // bloquée, co-éditeurs invisibles). appendEntityParam est un no-op garanti hors mode
  // multi-entités (même contrat que dans edit-lock-manager).
  function _scopedUrl(base) {
    var EM = window.CSI_EntityManager;
    if (EM && typeof EM.appendEntityParam === 'function') {
      try { return EM.appendEntityParam(base); } catch (e) { /* no-op */ }
    }
    return base;
  }
  function _scopeStamp() {
    var EM = window.CSI_EntityManager;
    try { return (EM && typeof EM.activeScope === 'function') ? (EM.activeScope() || '') : ''; } catch (e) { return ''; }
  }

  function _ping() {
    if (!_isServerMode()) return;
    var headers = _getAuthHeaders();
    if (!headers) return;

    var _scopeAtSend = _scopeStamp();
    fetch(_scopedUrl('./data/sync.php?action=presence-ping'), {
      method: 'POST',
      headers: headers,
      body: JSON.stringify({ activeTab: _activeTab })
    })
    .then(function(r) { return r.ok ? r.json() : null; })
    .then(function(data) {
      if (!data) return;
      // Réponse d'un périmètre déjà quitté (bascule pendant le vol) — ignorer : l'état
      // verrou/présence d'une AUTRE partition ne doit jamais s'appliquer à l'écran courant.
      if (_scopeStamp() !== _scopeAtSend) return;
      if (data.users) {
        _connectedUsers = data.users;
        _updateUI();
      }
      // Propagation de l'état du verrou d'édition global à EditLockManager
      // (le backend inclut le champ editLock dans la réponse depuis EDIT_LOCK_*)
      // === CSI-CO === editLocks (détenteurs PAR GRAPPE, présent sous COEDIT_CLUSTERS)
      // transmis en 2e argument — ignoré sans bruit par un EditLockManager antérieur.
      if (window.CSI_EditLockManager && typeof window.CSI_EditLockManager.setRemoteState === 'function') {
        try {
          window.CSI_EditLockManager.setRemoteState(data.editLock || null, data.editLocks || null);
        } catch (e) { /* ignore */ }
      }
      // === CSI-BUILD === le serveur annonce son build : s'il diffère du nôtre, cet onglet
      // tourne sur un client périmé (resté ouvert pendant la mise à jour) → déléguer au
      // bundle l'affichage du bandeau « Mettre à jour » (une seule fois par build ; jamais
      // de reload forcé). Vieux serveur sans le champ → no-op.
      if (data.serverBuild && data.serverBuild !== CSI_CLIENT_BUILD
          && typeof window._csiNotifyServerBuild === 'function') {
        try { window._csiNotifyServerBuild(String(data.serverBuild), CSI_CLIENT_BUILD); } catch (e) { /* ignore */ }
      }
      // === CSI-CO === (Lot 4, 2026-07-17) « Demander la main » : le serveur ne renvoie ici
      // QUE les demandes visant des grappes que JE détiens — déléguer l'affichage au bundle
      // (toast anti-spam). Vieux serveur sans le champ → no-op.
      if (data.handRequests && data.handRequests.length
          && typeof window._csiOnHandRequests === 'function') {
        try { window._csiOnHandRequests(data.handRequests); } catch (e) { /* ignore */ }
      }
    })
    .catch(function() { /* silencieux — la présence est informative */ });
  }

  function _leave() {
    if (!_isServerMode()) return;
    var headers = _getAuthHeaders();
    if (!headers) return;

    try {
      fetch(_scopedUrl('./data/sync.php?action=presence-leave'), {
        method: 'POST',
        headers: headers,
        body: JSON.stringify({}),
        keepalive: true
      });
    } catch (e) { /* best effort */ }
  }

  // ── UI Updates ──

  function _updateUI() {
    _updateConnectedUsersIndicator();
    _updateTabBadges();
  }

  function _updateConnectedUsersIndicator() {
    var el = document.getElementById('connectedUsersIndicator');
    if (!el) return;
    var currentUserId = (window.CSI_AuthManager && window.CSI_AuthManager.getCurrentUser)
      ? (window.CSI_AuthManager.getCurrentUser() || {}).id : null;
    var others = _connectedUsers.filter(function(u) { return u.userId !== currentUserId; });

    if (others.length === 0) {
      // Masqu\u00e9 : la zone statut du header r\u00e9serve sa largeur (flex-basis), rien ne saute.
      el.style.display = 'none';
      return;
    }
    el.style.display = '';
    var names = others.map(function(u) { return u.userName; }).join(', ');
    var label = others.length + ' connect\u00e9' + (others.length > 1 ? 's' : '');
    // Refonte header 2026-07 : l'indicateur est un <button> ic\u00f4ne + compteur ;
    // on pr\u00e9serve son balisage (span .connected-users__count) au lieu d'\u00e9craser
    // innerHTML, et la liste des noms est expos\u00e9e en aria-label (accessible),
    // plus title pour la souris.
    var countEl = el.querySelector('.connected-users__count');
    if (countEl) countEl.textContent = String(others.length);
    else el.textContent = label;
    el.setAttribute('aria-label', label + ' : ' + names);
    el.title = names;
  }

  function _updateTabBadges() {
    var currentUserId = (window.CSI_AuthManager && window.CSI_AuthManager.getCurrentUser)
      ? (window.CSI_AuthManager.getCurrentUser() || {}).id : null;

    // Retirer les anciens badges
    document.querySelectorAll('.lock-badge').forEach(function(b) { b.remove(); });

    // Ajouter des badges de présence pour les autres utilisateurs
    _connectedUsers.forEach(function(u) {
      if (u.userId === currentUserId || !u.activeTab) return;
      var navBtn = document.querySelector('[data-tab="' + u.activeTab + '"]');
      if (!navBtn) return;
      navBtn.style.position = 'relative';
      var badge = document.createElement('span');
      badge.className = 'lock-badge lock-badge--other';
      badge.textContent = (u.userName || '?').charAt(0).toUpperCase();
      badge.title = u.userName + ' consulte cet onglet';
      navBtn.appendChild(badge);
    });
  }

  // ── API publique ──

  var PresenceManager = {
    start: function() {
      if (_pingTimer) clearInterval(_pingTimer);

      // Retirer les anciens listeners
      if (_listeners.visChange) document.removeEventListener('visibilitychange', _listeners.visChange);
      if (_listeners.online) window.removeEventListener('online', _listeners.online);
      if (_listeners.offline) window.removeEventListener('offline', _listeners.offline);
      if (_listeners.unload) window.removeEventListener('beforeunload', _listeners.unload);

      _listeners.visChange = function() {
        if (!document.hidden) _ping();
      };
      _listeners.online = function() { _ping(); };
      _listeners.offline = function() {};
      _listeners.unload = function() { _leave(); };

      document.addEventListener('visibilitychange', _listeners.visChange);
      window.addEventListener('online', _listeners.online);
      window.addEventListener('offline', _listeners.offline);
      window.addEventListener('beforeunload', _listeners.unload);

      _ping(); // ping initial
      _pingTimer = setInterval(_ping, PRESENCE_INTERVAL);
    },

    stop: function() {
      if (_pingTimer) { clearInterval(_pingTimer); _pingTimer = null; }
      if (_listeners.visChange) document.removeEventListener('visibilitychange', _listeners.visChange);
      if (_listeners.online) window.removeEventListener('online', _listeners.online);
      if (_listeners.offline) window.removeEventListener('offline', _listeners.offline);
      if (_listeners.unload) window.removeEventListener('beforeunload', _listeners.unload);
      _connectedUsers = [];
    },

    notifyTabChange: function(tabId) {
      _activeTab = tabId || '';
      _ping(); // ping immédiat au changement d'onglet
    },

    getConnectedUsers: function() {
      return _connectedUsers.slice();
    },

    getTabDataKeys: function() {
      return TAB_DATA_KEYS;
    }
  };

  // ── Export ──
  window.CSI_PresenceManager = PresenceManager;

  // Shim de compatibilité — l'ancien code référence CSI_LockManager.
  // Les méthodes lock per-tab (acquireLock/releaseLock) sont obsolètes : le verrouillage est
  // désormais GLOBAL et géré par CSI_EditLockManager. Les stubs ci-dessous restent en place
  // uniquement pour ne pas casser les anciens appels (notamment auth-manager.js::logout).
  // releaseAll() délègue à EditLockManager pour libérer le verrou global au logout.
  window.CSI_LockManager = {
    start: PresenceManager.start,
    stop: PresenceManager.stop,
    getConnectedUsers: PresenceManager.getConnectedUsers,
    getTabDataKeys: PresenceManager.getTabDataKeys,
    notifyTabChange: PresenceManager.notifyTabChange,
    // @deprecated — verrouillage per-tab supprimé, voir CSI_EditLockManager pour le verrou global
    acquireLock: function() { return Promise.resolve({ granted: true }); },
    releaseLock: function() { return Promise.resolve(); },
    releaseAll: function() {
      // Libération du verrou d'édition global (déclenché au logout par auth-manager.js)
      if (window.CSI_EditLockManager && typeof window.CSI_EditLockManager.release === 'function') {
        try { return window.CSI_EditLockManager.release(); } catch (e) { /* ignore */ }
      }
      return Promise.resolve();
    },
    isLockedByMe: function() { return true; },
    getLockHolder: function() { return null; },
    flushBeforeRelease: null
  };

  // ── Stub mode local (file://) ──
  if (window.location.protocol === 'file:') {
    var stub = {
      start: function() {},
      stop: function() {},
      notifyTabChange: function() {},
      getConnectedUsers: function() { return []; },
      getTabDataKeys: function() { return TAB_DATA_KEYS; }
    };
    window.CSI_PresenceManager = stub;
    window.CSI_LockManager = {
      start: function() {},
      stop: function() {},
      notifyTabChange: function() {},
      getConnectedUsers: function() { return []; },
      getTabDataKeys: function() { return TAB_DATA_KEYS; },
      acquireLock: function() { return Promise.resolve({ granted: true }); },
      releaseLock: function() { return Promise.resolve(); },
      releaseAll: function() { return Promise.resolve(); },
      isLockedByMe: function() { return true; },
      getLockHolder: function() { return null; },
      flushBeforeRelease: null
    };
  }

  void 0;

})();
