/**
 * Cyber-Assistant - Auth Manager
 * Gestion de l'authentification utilisateurs en mode serveur.
 * Login/logout/register, JWT token en sessionStorage, modales UI.
 * Inerte en mode local (CSI_LicenseManager.isServerMode() === false).
 *
 * Max 5 utilisateurs nommés. Premier inscrit = admin.
 */
(function() {
  'use strict';

  var API_BASE = './data/users.php';
  // === CSI-MCP === endpoint distinct de users.php (routes admin de l'interface MCP).
  var MCP_API_BASE = './data/mcp.php';
  // === CSI-UPD === endpoint distinct (mise à jour du serveur par paquet ZIP signé).
  var UPDATE_API_BASE = './data/update.php';
  var TOKEN_KEY = 'csi_auth_token';
  var USER_KEY = 'csi_auth_user';
  // === CSI-CO === (audit co-édition 2026-08-05) L'identifiant d'onglet est PERSISTÉ en
  // sessionStorage : sessionStorage est propre à l'onglet ET survit à un rechargement (F5).
  // Sans cela, la garde « un seul onglet éditeur » (décision PO du 05/08) prendrait un
  // rechargement pour un second onglet et bloquerait l'utilisateur sur son propre écran.
  var TAB_ID_STABLE = false;   // vrai seulement si l'identifiant survit à un rechargement
  var TAB_ID = (function () {
    var gen = function () {
      return crypto.randomUUID ? crypto.randomUUID() :
        Array.from(crypto.getRandomValues(new Uint8Array(16)))
          .map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
    };
    try {
      var k = 'csi_tab_id';
      var v = sessionStorage.getItem(k);
      if (!v) { v = gen(); sessionStorage.setItem(k, v); }
      TAB_ID_STABLE = true;
      return v;
    } catch (e) {
      // sessionStorage indisponible (navigation privée stricte, politique d'entreprise) :
      // l'identifiant serait REGÉNÉRÉ à chaque rechargement. Le signaler est essentiel —
      // un identifiant volatil ferait passer un simple F5 pour un second onglet et
      // refuserait l'édition à l'utilisateur sur son propre écran. Les consommateurs
      // (verrou d'édition) doivent alors s'abstenir et retomber sur le comportement
      // historique : mieux vaut ne pas distinguer les onglets que bloquer quelqu'un.
      TAB_ID_STABLE = false;
      return gen();
    }
  })();

  function _t(key, fallback) {
    // FIX-I18N-AUTH (audit 2026-06-10) : transmettre le fallback à CSI_i18n.t —
    // sans lui, une clé absente s'affichait brute ('auth.xxx') au lieu du texte FR.
    return window.CSI_i18n && window.CSI_i18n.t ? window.CSI_i18n.t(key, fallback) : (fallback || key);
  }

  var _currentUser = null;
  var _token = null;
  var _authStatus = null; // cached server status
  var _wasSetupFlag = false; // true when first admin was just created (this session)

  // BUG C9 fix: auth-ready promise to avoid race condition on init
  var _authReadyResolve = null;
  var _authReadyPromise = new Promise(function(resolve) { _authReadyResolve = resolve; });

  // Account lockout — client-side UX (real protection is server-side)
  var _loginFailCount = 0;
  var _loginLockUntil = 0;
  var _loginCountdownTimer = null;
  var CLIENT_LOCKOUT_THRESHOLD = 3;
  var CLIENT_LOCKOUT_SCHEDULE = [30, 60, 120]; // seconds per tier

  function _ingestEntityContext(data) {
    if (!data) return;
    if (window.CSI_EntityManager && typeof window.CSI_EntityManager.ingest === 'function') {
      window.CSI_EntityManager.ingest(data);
      try { delete window._csiPendingEntityAuthData; } catch (e) { window._csiPendingEntityAuthData = null; }
    } else {
      window._csiPendingEntityAuthData = data;
    }
  }

  // FEAT-USER-LANGUAGE (2026-07-10) : en mode serveur, la langue d'interface suit le
  // compte (user.language, whitelist fr/en/nl côté serveur). Appliquée à l'authentification
  // (login/setup/register/SSO/me) ; le mode local reste piloté par localStorage uniquement.
  // Si un choix local n'a pas pu être poussé au serveur (échec réseau lors de
  // l'enregistrement des paramètres), le flag LANG_PUSH_PENDING_KEY est posé : dans ce
  // cas on REJOUE le push au lieu d'écraser le choix explicite de l'utilisateur.
  var LANG_PUSH_PENDING_KEY = 'smsi_language_push_pending';
  function _applyUserLanguage(user) {
    if (!user || !user.language) return;
    var i18n = window.CSI_i18n;
    if (!i18n || !i18n.getAvailableLanguages()[user.language]) return;
    var localLang = i18n.getLanguage();
    if (localLang === user.language) {
      try { localStorage.removeItem(LANG_PUSH_PENDING_KEY); } catch (e) {}
      return;
    }
    var pushPending = false;
    try { pushPending = localStorage.getItem(LANG_PUSH_PENDING_KEY) === '1'; } catch (e) {}
    if (pushPending) {
      // Choix local explicite non encore synchronisé : le rejouer (best-effort).
      AuthManager.setUserLanguage(localLang).catch(function () {});
      return;
    }
    i18n.setLanguage(user.language);
    if (typeof i18n.translatePage === 'function') i18n.translatePage();
  }

  // Cybersecurity quotes — displayed randomly on the login screen
  var CSI_CYBER_QUOTES = [
    { text: "Security is a process, not a product.", author: "Bruce Schneier" },
    { text: "Connais ton ennemi et connais-toi toi-m\u00eame ; en cent batailles tu ne courras aucun danger.", author: "Sun Tzu" },
    { text: "The weakest link in the security chain is the human element.", author: "Kevin Mitnick" },
    { text: "An ounce of prevention is worth a pound of cure.", author: "Benjamin Franklin" },
    { text: "The only truly secure system is one that is powered off, cast in a block of concrete and sealed in a lead-lined room.", author: "Gene Spafford" },
    { text: "You can\u2019t secure what you don\u2019t understand.", author: "Dan Kaminsky" },
    { text: "The Internet is the first thing that humanity has built that humanity doesn\u2019t understand.", author: "Mikko Hypp\u00f6nen" },
    { text: "There\u2019s no silver bullet with cybersecurity; a layered defense is the only viable defense.", author: "James Scott" },
    { text: "It takes 20 years to build a reputation and few minutes of cyber-incident to ruin it.", author: "Stephane Nappo" },
    { text: "There are only two types of companies: those that have been hacked, and those that will be.", author: "Robert Mueller" },
    { text: "If you put a key under the mat for the cops, a burglar can find it, too.", author: "Tim Cook" },
    { text: "If you spend more on coffee than on IT security, you will be hacked.", author: "Richard Clarke" },
    { text: "Cybersecurity is a shared responsibility.", author: "Theresa Payton" },
    { text: "If you think technology can solve your security problems, then you don\u2019t understand the problems and you don\u2019t understand the technology.", author: "Bruce Schneier" },
    { text: "Privacy is not something that I\u2019m merely entitled to, it\u2019s an absolute prerequisite.", author: "Marlon Brando" }
  ];

  function _getClientLockDuration(failCount) {
    var tier = Math.floor((failCount - CLIENT_LOCKOUT_THRESHOLD) / CLIENT_LOCKOUT_THRESHOLD);
    tier = Math.max(0, Math.min(tier, CLIENT_LOCKOUT_SCHEDULE.length - 1));
    return CLIENT_LOCKOUT_SCHEDULE[tier] * 1000; // ms
  }

  var AuthManager = {

    // ============================================================
    // API calls
    // ============================================================

    _fetch: function(endpoint, options) {
      options = options || {};
      var headers = options.headers || {};
      if (!options.method || options.method.toUpperCase() !== 'GET') {
        headers['Content-Type'] = 'application/json';
      }
      if (_token) headers['Authorization'] = 'Bearer ' + _token;
      options.headers = headers;
      return (window.fetchWithTimeout || fetch)(API_BASE + '?action=' + endpoint, options)
        .then(function(r) {
          // PASS2-006: Catch JSON parse errors when server returns HTML (502/503)
          // FIX-AUTH-REFRESH (audit 2026-06-10) : propager le code HTTP (status) pour que les
          // appelants distinguent 401/403 (session invalide) des 5xx (indisponibilité transitoire)
          return r.json()
            .then(function(d) { return { ok: r.ok, status: r.status, data: d }; })
            .catch(function() { return { ok: false, status: r.status, data: { error: _t('auth.serverUnavailable', 'Server unavailable') + ' (error ' + r.status + ')' } }; });
        });
    },

    /**
     * Récupère le statut public (nombre d'utilisateurs, configuré ou non)
     */
    getStatus: function() {
      return this._fetch('status', { method: 'GET' }).then(function(r) {
        if (r.ok) _authStatus = r.data;
        return r.data;
      });
    },

    /**
     * Setup initial (premier utilisateur = admin)
     */
    setup: function(name, password) {
      var self = this;
      return this._fetch('setup', {
        method: 'POST',
        body: JSON.stringify({ name: name, password: password })
      }).then(function(r) {
        if (r.ok && r.data.token) {
          _token = r.data.token;
          _currentUser = r.data.user;
          _ingestEntityContext(r.data); /* === CSI-ME === */
          sessionStorage.setItem(TOKEN_KEY, _token);
          sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
          self._scheduleTokenRefresh();
          _wasSetupFlag = true; // First admin just created → wizard should show
        }
        return r;
      });
    },

    /**
     * Returns true if the current session created the first admin (setup, not login).
     * Used by the wizard to distinguish first-time setup from regular logins.
     */
    wasSetup: function() { return _wasSetupFlag; },

    /**
     * Inscription d'un nouvel utilisateur
     */
    register: function(name, password) {
      var self = this;
      return this._fetch('register', {
        method: 'POST',
        body: JSON.stringify({ name: name, password: password })
      }).then(function(r) {
        if (r.ok && r.data.token) {
          _token = r.data.token;
          _currentUser = r.data.user;
          _ingestEntityContext(r.data); /* === CSI-ME === */
          sessionStorage.setItem(TOKEN_KEY, _token);
          sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
          self._scheduleTokenRefresh();
        }
        return r;
      });
    },

    /**
     * Connexion
     */
    login: function(name, password) {
      var self = this;
      return this._fetch('login', {
        method: 'POST',
        body: JSON.stringify({ name: name, password: password })
      }).then(function(r) {
        if (r.ok && r.data.token) {
          _token = r.data.token;
          _currentUser = r.data.user;
          _ingestEntityContext(r.data); /* === CSI-ME === */
          sessionStorage.setItem(TOKEN_KEY, _token);
          sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
          self._scheduleTokenRefresh();
          _applyUserLanguage(_currentUser); // FEAT-USER-LANGUAGE
          // Marquer le wizard comme vu (évite le re-affichage en navigation privée)
          try { localStorage.setItem('smsi_welcome_seen', '1'); } catch(e) {}
        }
        return r;
      });
    },

    /**
     * Déconnexion
     */
    logout: function() {
      var self = this;
      // (audit 2026-07-11, finding 2.4) Capturer AVANT purge : un utilisateur SSO est ensuite
      // redirigé vers l'endpoint de déconnexion IdP (le serveur décide s'il rebondit vers l'IdP).
      var wasSso = !!(_currentUser && _currentUser.sso);
      // === CSI-CO === (audit co-édition 2026-08-05, MAJEUR) Libérer les verrous AVANT tout le
      // reste. Deux raisons cumulées faisaient que `edit-lock-release` ne partait JAMAIS à la
      // déconnexion : (1) l'appel était placé après `_token = null`, or `_getAuthHeaders()`
      // retourne null sans jeton → la requête était rejetée et le rejet avalé par le .catch de
      // release() ; (2) même bien ordonné côté client, l'endpoint `logout` invalide le jeton
      // côté SERVEUR — une libération postée après lui repart en 401. Résultat : les grappes
      // restaient verrouillées jusqu'au TTL (90 s) après chaque déconnexion propre, et les
      // collègues lisaient « Verrouillé par … » alors que la personne était partie.
      // (vérifié au banc : verrous présents après logout avant ce correctif, libérés après)
      var _relLocks = Promise.resolve();
      if (window.CSI_LockManager) {
        try { _relLocks = Promise.resolve(window.CSI_LockManager.releaseAll()); } catch (e) { /* best-effort */ }
      }
      return _relLocks.catch(function () { /* la déconnexion ne doit jamais être bloquée */ })
        .then(function () { return self._fetch('logout', { method: 'POST' }); })
        .finally(function() {
        if (self._refreshTimerId) { clearTimeout(self._refreshTimerId); self._refreshTimerId = null; }
        _token = null;
        _currentUser = null;
        if (window.CSI_EntityManager) window.CSI_EntityManager.reset(); /* === CSI-ME === */
        sessionStorage.removeItem(TOKEN_KEY);
        sessionStorage.removeItem(USER_KEY);
        // Session locale purgée : rediriger vers la déconnexion IdP UNIQUEMENT si le serveur
        // a signalé idpLogout actif (status.ssoIdpLogout). Sinon, comportement historique
        // inchangé (login in-SPA) → zéro régression pour les déploiements sans logout IdP.
        if (wasSso && _authStatus && _authStatus.ssoIdpLogout
            && typeof window !== 'undefined' && window.location && window.location.replace) {
          try { window.location.replace(self.getSsoLogoutUrl()); } catch (e) {}
        }
      });
    },

    /**
     * Valider le token courant et récupérer l'utilisateur
     */
    validateToken: function() {
      if (!_token) return Promise.resolve(null);
      return this._fetch('me', { method: 'GET' }).then(function(r) {
        if (r.ok && r.data.user) {
          _currentUser = r.data.user;
          _ingestEntityContext(r.data); /* === CSI-ME === */
          _applyUserLanguage(_currentUser); // FEAT-USER-LANGUAGE
          return _currentUser;
        }
        // Token invalide
        _token = null;
        _currentUser = null;
        sessionStorage.removeItem(TOKEN_KEY);
        sessionStorage.removeItem(USER_KEY);
        return null;
      });
    },

    // ============================================================
    // Admin endpoints
    // ============================================================

    // FEAT-USER-LANGUAGE (2026-07-10) : pousse la langue d'interface choisie vers le
    // profil serveur (self-service). Best-effort côté appelant — localStorage fait foi localement.
    setUserLanguage: function(lang) {
      return this._fetch('set-language', {
        method: 'POST',
        body: JSON.stringify({ language: lang })
      }).then(function(r) {
        if (!r.ok) throw new Error((r.data && r.data.error) || 'set-language failed');
        if (_currentUser) {
          _currentUser.language = lang;
          try { sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser)); } catch(e) {}
        }
        try { localStorage.removeItem(LANG_PUSH_PENDING_KEY); } catch(e) {}
        return r.data;
      }).catch(function(e) {
        // Échec du push : mémoriser que le choix local prime, pour le rejouer au
        // prochain contact serveur au lieu de laisser /me l'écraser.
        try { localStorage.setItem(LANG_PUSH_PENDING_KEY, '1'); } catch(e2) {}
        throw e;
      });
    },

    listUsers: function() {
      return this._fetch('users', { method: 'GET' }).then(function(r) { return r.data && r.data.users ? r.data.users : []; });
    },

    deleteUser: function(userId) {
      return this._fetch('delete-user', {
        method: 'POST',
        body: JSON.stringify({ id: userId })
      });
    },

    resetPassword: function(userId, newPassword) {
      return this._fetch('reset-password', {
        method: 'POST',
        body: JSON.stringify({ id: userId, newPassword: newPassword })
      });
    },

    suspendUser: function(userId) {
      return this._fetch('suspend-user', {
        method: 'POST',
        body: JSON.stringify({ id: userId })
      });
    },

    // FIX-SSO-EMAIL-LINK (audit 2026-06-10, complément client) : email optionnel à la
    // création (omis si vide) — permet la liaison « match by email » du SSO entreprise.
    // === CSI-ME === 5e paramètre entOpts (facultatif) : { entityIds, canEditSubEntities }.
    createUser: function(name, password, role, email, entOpts) {
      var body = { name: name, password: password, role: role || 'editor' };
      if (typeof email === 'string' && email.trim() !== '') body.email = email.trim();
      if (entOpts && typeof entOpts === 'object') {
        if (entOpts.entityIds) body.entityIds = entOpts.entityIds;
        if (entOpts.canEditSubEntities) body.canEditSubEntities = true;
      }
      return this._fetch('admin-create-user', {
        method: 'POST',
        body: JSON.stringify(body)
      });
    },

    // FIX-SSO-EMAIL-LINK : 4e paramètre email — chaîne vide = suppression côté serveur.
    // name devient optionnel (null/undefined = inchangé) pour les mises à jour email-only.
    // === CSI-ME === 5e paramètre entOpts (facultatif) : { entityIds, canEditSubEntities, detachConfirm }.
    updateUser: function(userId, name, role, email, entOpts) {
      var body = { id: userId };
      if (name !== undefined && name !== null) body.name = name;
      if (role) body.role = role;
      if (typeof email === 'string') body.email = email.trim();
      if (entOpts && typeof entOpts === 'object') {
        if (entOpts.entityIds !== undefined) body.entityIds = entOpts.entityIds;
        if (entOpts.canEditSubEntities !== undefined) body.canEditSubEntities = entOpts.canEditSubEntities;
        if (entOpts.detachConfirm) body.detachConfirm = true;
      }
      return this._fetch('update-user', {
        method: 'POST',
        body: JSON.stringify(body)
      });
    },

    changePassword: function(currentPassword, newPassword) {
      var self = this;
      return this._fetch('change-password', {
        method: 'POST',
        body: JSON.stringify({ currentPassword: currentPassword, newPassword: newPassword })
      }).then(function(r) {
        if (r.ok && r.data && r.data.token) {
          _token = r.data.token;
          sessionStorage.setItem(TOKEN_KEY, r.data.token);
          self._scheduleTokenRefresh();
        }
        return r;
      });
    },

    // ============================================================
    // Security endpoints (admin-only)
    // ============================================================

    /**
     * Récupère le statut de sécurité du serveur (admin uniquement)
     */
    getSecurityStatus: function() {
      return this._fetch('security-status', { method: 'GET' }).then(function(r) {
        return r.ok ? r.data : null;
      });
    },

    /**
     * Récupère le journal d'audit (admin uniquement)
     * @param {number} limit - Nombre d'entrées (défaut: 50)
     * @param {number} offset - Offset pour pagination (défaut: 0)
     * @param {string} eventType - Filtre par type d'événement (optionnel)
     */
    getAuditLog: function(limit, offset, eventType) {
      var params = 'audit-log&limit=' + (limit || 50) + '&offset=' + (offset || 0);
      if (eventType) params += '&eventType=' + encodeURIComponent(eventType);
      return this._fetch(params, { method: 'GET' }).then(function(r) {
        return r.ok ? r.data : { entries: [], total: 0 };
      });
    },

    /**
     * Met à jour la configuration de sécurité (admin uniquement)
     * @param {object} config - { ipFilterMode, ipAllowlist, ipDenylist, passwordMaxAgeDays }
     */
    updateSecurityConfig: function(config) {
      return this._fetch('update-security-config', {
        method: 'POST',
        body: JSON.stringify(config)
      });
    },

    // ============================================================
    // SSO endpoints (admin-only)
    // ============================================================

    getSsoConfig: function() {
      return this._fetch('sso-config', { method: 'GET' }).then(function(r) {
        return r.ok ? r.data : null;
      });
    },

    saveSsoConfig: function(config) {
      return this._fetch('sso-config', {
        method: 'POST',
        body: JSON.stringify(config)
      });
    },

    testSsoConnection: function(issuer) {
      return this._fetch('sso-test', {
        method: 'POST',
        body: JSON.stringify({ issuer: issuer })
      });
    },

    getSsoInitUrl: function() {
      return API_BASE + '?action=sso-init';
    },

    // (audit 2026-07-11, finding 2.4) URL de déconnexion IdP. Le serveur redirige vers
    // l'end_session_endpoint si idpLogout est activé, sinon simplement vers l'application.
    getSsoLogoutUrl: function() {
      return API_BASE + '?action=sso-logout';
    },

    // ============================================================
    // === CSI-MCP === Interface MCP (accès IA lecture seule, admin only)
    // ============================================================
    // ⚠️ mcp.php est un endpoint DISTINCT de users.php : _fetch() n'est pas
    // réutilisable tel quel (il préfixe API_BASE). D'où _mcpFetch, calqué à
    // l'identique (ne rejette jamais ; renvoie {ok, status, data}).

    _mcpFetch: function(action, options) {
      options = options || {};
      var headers = options.headers || {};
      if (!options.method || options.method.toUpperCase() !== 'GET') {
        headers['Content-Type'] = 'application/json';
      }
      if (_token) headers['Authorization'] = 'Bearer ' + _token;
      options.headers = headers;
      return (window.fetchWithTimeout || fetch)(MCP_API_BASE + '?action=' + action, options)
        .then(function(r) {
          return r.json()
            .then(function(d) { return { ok: r.ok, status: r.status, data: d }; })
            .catch(function() {
              // mcp.php absent du serveur (déploiement partiel) → Apache renvoie du HTML.
              // On le distingue explicitement pour que la carte affiche un message utile.
              return { ok: false, status: r.status, notDeployed: (r.status === 404 || r.status === 403),
                       data: { error: _t('users.mcpNotDeployed', 'mcp.php absent du serveur') } };
            });
        });
    },

    // ============================================================
    // === CSI-UPD === Mise à jour serveur par paquet ZIP signé (admin only)
    // ============================================================
    // Même raison que _mcpFetch : update.php est un endpoint DISTINCT. Cloné plutôt
    // que paramétré (choix documenté ci-dessus). Deux différences assumées :
    //   - timeout EXPLICITE : le défaut de fetchWithTimeout est 8 s, très insuffisant
    //     pour téléverser ~3 Mo de base64 puis écrire des dizaines de fichiers ;
    //   - notDeployed : une installation antérieure à cette fonctionnalité n'a pas
    //     update.php (œuf-et-poule assumé) → la carte doit le dire, pas planter.

    _updFetch: function(action, options, timeoutMs) {
      options = options || {};
      var headers = options.headers || {};
      if (options.method && options.method.toUpperCase() !== 'GET') {
        headers['Content-Type'] = 'application/json';
      }
      if (_token) headers['Authorization'] = 'Bearer ' + _token;
      options.headers = headers;
      var url = UPDATE_API_BASE + '?action=' + action;
      var p = (timeoutMs && window.fetchWithTimeout)
        ? window.fetchWithTimeout(url, options, timeoutMs)
        : (window.fetchWithTimeout || fetch)(url, options);
      return p.then(function(r) {
        return r.json()
          .then(function(d) { return { ok: r.ok, status: r.status, data: d }; })
          .catch(function() {
            return { ok: false, status: r.status, notDeployed: (r.status === 404 || r.status === 403),
                     data: { error: _t('serverUpdate.notDeployed', 'update.php absent du serveur') } };
          });
      }).catch(function(err) {
        // Réseau coupé / timeout : ne jamais rejeter (contrat des wrappers).
        return { ok: false, status: 0, data: { error: (err && err.name === 'AbortError')
          ? _t('serverUpdate.timeout', 'Délai dépassé — la mise à jour est peut-être toujours en cours côté serveur.')
          : _t('serverUpdate.unavailable', 'Service de mise à jour indisponible') } };
      });
    },

    /** État de l'updater : version installée, prérequis techniques, sauvegarde dispo. */
    getUpdateStatus: function() {
      return this._updFetch('status', { method: 'GET' });
    },

    /** Téléverse un paquet (base64) et renvoie le PLAN, sans rien appliquer. */
    uploadUpdatePackage: function(base64Zip) {
      return this._updFetch('upload', {
        method: 'POST',
        body: JSON.stringify({ package: base64Zip })
      }, 180000);
    },

    /** Applique le paquet en attente. force=true passe outre les éditeurs actifs. */
    applyUpdate: function(force) {
      return this._updFetch('apply', {
        method: 'POST',
        body: JSON.stringify(force ? { force: true } : {})
      }, 180000);
    },

    /** Restaure la version précédente du CODE (les données ne sont pas touchées). */
    rollbackUpdate: function() {
      return this._updFetch('rollback', { method: 'POST', body: '{}' }, 180000);
    },

    /** URL publique de l'endpoint MCP, à coller dans le client IA. */
    getMcpEndpointUrl: function() {
      try {
        return new URL(MCP_API_BASE, window.location.href).href;
      } catch (e) {
        return MCP_API_BASE;
      }
    },

    getMcpConfig: function() {
      return this._mcpFetch('config', { method: 'GET' });
    },

    saveMcpConfig: function(config) {
      return this._mcpFetch('config', { method: 'POST', body: JSON.stringify(config) });
    },

    listMcpTokens: function() {
      return this._mcpFetch('tokens', { method: 'GET' });
    },

    /** Le jeton en clair n'est renvoyé QU'ICI, une seule fois. Ne jamais le persister. */
    createMcpToken: function(label, userId, expiresInDays) {
      return this._mcpFetch('token-create', {
        method: 'POST',
        body: JSON.stringify({ label: label, userId: userId, expiresInDays: expiresInDays })
      });
    },

    revokeMcpToken: function(id) {
      return this._mcpFetch('token-revoke', { method: 'POST', body: JSON.stringify({ id: id }) });
    },

    testMcpEndpoint: function() {
      return this._mcpFetch('selftest', { method: 'GET' });
    },

    /**
     * Rafraîchit le token JWT avant expiration
     */
    refreshToken: function() {
      var self = this;
      return this._fetch('refresh-token', { method: 'POST' }).then(function(r) {
        if (r.ok && r.data.token) {
          _token = r.data.token;
          sessionStorage.setItem(TOKEN_KEY, _token);
          // Update user data if returned
          if (r.data.user) {
            _currentUser = r.data.user;
            // === CSI-ME === NE PAS ingérer ici : refresh-token ne fait que renouveler
            // le JWT (identité inchangée) et ne porte PAS le bloc multiEntity — un ingest
            // désactiverait le mode et réinitialiserait le périmètre actif en pleine
            // session (revue Lot 1c). L'ingestion vit sur login/setup/register/me/sso.
            sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
          }
          self._scheduleTokenRefresh();
        }
        return r;
      });
    },

    /**
     * Planifie le rafraîchissement automatique du token
     * Appelle refreshToken 15 minutes avant l'expiration
     */
    _refreshTimerId: null,

    _scheduleTokenRefresh: function() {
      var self = this;
      if (this._refreshTimerId) {
        clearTimeout(this._refreshTimerId);
        this._refreshTimerId = null;
      }
      if (!_token) return;

      // Decode JWT payload to read exp claim
      try {
        var parts = _token.split('.');
        if (parts.length !== 3) return;
        var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
        if (!payload.exp) return;

        var now = Math.floor(Date.now() / 1000);
        var timeUntilExpiry = payload.exp - now;

        // BUG C8 fix: if token already expired, clear session immediately
        if (timeUntilExpiry <= 0) {
          void 0;
          _token = null;
          _currentUser = null;
          sessionStorage.removeItem(TOKEN_KEY);
          sessionStorage.removeItem(USER_KEY);
          if (typeof self.showLoginModal === 'function') {
            self.showLoginModal();
          }
          return;
        }

        // Refresh 15 minutes before expiry (or at half-life if TTL < 30 min)
        var refreshIn = Math.max(60, timeUntilExpiry - 900); // at least 60s from now

        this._refreshTimerId = setTimeout(function() {
          self._refreshTimerId = null;
          if (_token) {
            // FIX-AUTH-REFRESH (audit 2026-06-10) : refreshToken() résout {ok:false} sur erreur
            // HTTP sans jamais rejeter → l'échec était ignoré silencieusement (session zombie).
            // Le traitement du résultat est désormais centralisé dans _runScheduledRefresh.
            self._runScheduledRefresh();
          }
        }, refreshIn * 1000);
      } catch(e) {
        // Invalid token format — skip auto-refresh
      }
    },

    /**
     * FIX-AUTH-REFRESH (audit 2026-06-10) : exécute le refresh planifié et gère ses échecs.
     * - 401/403 (jti révoqué, compte suspendu) → nettoyage de session + modale de login.
     * - 5xx ou erreur réseau → nouvelle tentative dans 60 s au lieu d'abandonner.
     * Pas de boucle de modales : la modale n'est affichée qu'une fois (après nettoyage,
     * _token est null et plus aucun timer de refresh n'est réarmé).
     */
    _runScheduledRefresh: function() {
      var self = this;
      var REFRESH_RETRY_DELAY_MS = 60000;
      function clearSessionAndShowLogin() {
        _token = null;
        _currentUser = null;
        sessionStorage.removeItem(TOKEN_KEY);
        sessionStorage.removeItem(USER_KEY);
        if (typeof self.showLoginModal === 'function') {
          self.showLoginModal();
        }
      }
      function retryLater() {
        // Ne pas écraser un timer déjà replanifié (ex. refresh réussi en parallèle)
        if (self._refreshTimerId || !_token) return;
        self._refreshTimerId = setTimeout(function() {
          self._refreshTimerId = null;
          if (_token) self._runScheduledRefresh();
        }, REFRESH_RETRY_DELAY_MS);
      }
      this.refreshToken().then(function(r) {
        if (r && r.ok && r.data && r.data.token) return; // succès — refreshToken a déjà replanifié
        if (r && (r.status === 401 || r.status === 403)) {
          void 0;
          clearSessionAndShowLogin();
          return;
        }
        void 0;
        retryLater();
      }).catch(function(err) {
        void 0;
        retryLater();
      });
    },

    // ============================================================
    // State accessors
    // ============================================================

    isLoggedIn: function() { return !!_token && !!_currentUser; },
    getCurrentUser: function() { return _currentUser; },
    getToken: function() { return _token; },
    getTabId: function() { return TAB_ID; },
    isTabIdStable: function() { return TAB_ID_STABLE; },
    isAdmin: function() { return _currentUser && _currentUser.role === 'admin'; },
    isViewer: function() { return _currentUser && _currentUser.role === 'viewer'; },
    isEditor: function() { return _currentUser && _currentUser.role === 'editor'; },
    mustChangePassword: function() { return _currentUser && !!_currentUser.mustChangePassword; },
    isSsoUser: function() { return _currentUser && !!_currentUser.sso; },

    getAuthHeader: function() {
      if (_token) return 'Bearer ' + _token;
      return '';
    },

    // ============================================================
    // UI: Modale de login
    // ============================================================

    showLoginModal: function(callback) {
      var self = this;
      var t = window.t || function(k, d) { return d || k; };

      // Block HTTP access in server mode (except localhost/dev)
      var isHttps = location.protocol === 'https:';
      var isLocalhost = location.hostname === 'localhost' || location.hostname === '127.0.0.1' || location.hostname === '::1';
      var isFileMode = location.protocol === 'file:';

      if (!isHttps && !isLocalhost && !isFileMode) {
        var httpsUrl = 'https://' + location.host + location.pathname + location.search + location.hash;
        var wall = document.createElement('div');
        wall.style.cssText = 'position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#0f172a;color:#f1f5f9;font-family:system-ui;padding:2rem;text-align:center;gap:1rem;z-index:99999;';

        var wIcon = document.createElement('div');
        wIcon.style.fontSize = '3rem';
        wIcon.textContent = '\u26A0\uFE0F';
        wall.appendChild(wIcon);

        // FIX-I18N-AUTH (audit 2026-06-10, auth-manager.js:656) : \u00e9cran de blocage HTTP traduit
        var wTitle = document.createElement('h1');
        wTitle.style.cssText = 'font-size:1.5rem;color:#f87171;margin:0;';
        wTitle.textContent = t('auth.httpBlockedTitle', 'Connexion non sécurisée');
        wall.appendChild(wTitle);

        var wMsg = document.createElement('p');
        wMsg.style.cssText = 'max-width:480px;color:#94a3b8;line-height:1.6;margin:0;';
        wMsg.textContent = t('auth.httpBlockedMsg', 'Cyber-Assistant transmet des données d\'authentification sensibles. L\'accès en HTTP non chiffré est bloqué en mode serveur. Utilisez HTTPS pour accéder à cette application.');
        wall.appendChild(wMsg);

        var wBtn = document.createElement('a');
        wBtn.href = httpsUrl;
        wBtn.style.cssText = 'margin-top:1rem;padding:0.75rem 2rem;background:#2563eb;color:#fff;border-radius:6px;text-decoration:none;font-weight:600;font-size:1rem;';
        wBtn.textContent = t('auth.httpsAccessBtn', 'Accéder en HTTPS') + ' \u2192';
        wall.appendChild(wBtn);

        var wNote = document.createElement('p');
        wNote.style.cssText = 'font-size:0.75rem;color:#475569;margin:0;';
        wNote.textContent = t('auth.httpBlockedNote', 'Si vous utilisez un certificat auto-signé, acceptez l\'exception de sécurité dans votre navigateur.');
        wall.appendChild(wNote);

        document.body.appendChild(wall);
        return;
      }

      // D'abord vérifier le statut serveur
      // FIX-AUTH-STATUS (audit 2026-06-10) : getStatus() résout l'objet d'erreur {error:...}
      // sur réponse HTTP non-2xx (500/502/503) → `configured` était undefined et la modale
      // de SETUP ADMIN s'affichait à tort sur un serveur déjà configuré (utilisateur piégé).
      // Le setup n'est affiché QUE sur une réponse saine (r.ok) avec configured === false ;
      // sur erreur, formulaire de LOGIN avec bandeau d'indisponibilité + bouton réessayer.
      this._fetch('status', { method: 'GET' }).then(function(r) {
        if (r.ok) _authStatus = r.data;
        if (r.ok && r.data && r.data.configured === false) {
          // Serveur sain et aucun utilisateur → afficher le setup admin
          self._showSetupModal(callback);
          return;
        }
        if (r.ok && r.data && r.data.configured) {
          self._showLoginForm(r.data, callback);
          return;
        }
        // Réponse d'erreur (5xx, proxy, JSON invalide) → login + message d'indisponibilité
        self._showLoginForm({ configured: true, userCount: 0, maxUsers: 5, serverUnavailable: true }, callback);
      }).catch(function() {
        self._showLoginForm({ configured: true, userCount: 0, maxUsers: 5, serverUnavailable: true }, callback);
      });
    },

    _showLoginForm: function(status, callback) {
      var self = this;
      var t = window.t || function(k, d) { return d || k; };
      var LM = window.CSI_LicenseManager;
      var orgRaw = LM && LM.getLicense() ? LM.getLicense().organization : '';
      // BUG-782: Escape org name to prevent XSS via license data
      var orgEl = document.createElement('span');
      orgEl.textContent = orgRaw;
      var org = orgEl.innerHTML; // safe: re-injected after escaping via textContent

      // Anti-clickjacking: refuse to display in iframe
      if (window.self !== window.top) {
        var iframeBlock = document.createElement('div');
        iframeBlock.style.cssText = 'display:flex;align-items:center;justify-content:center;height:100vh;background:#1e293b;color:white;font-family:sans-serif;';
        var iframeMsg = document.createElement('div');
        iframeMsg.style.cssText = 'text-align:center';
        // FIX-I18N-AUTH (audit 2026-06-10, auth-manager.js:656) : message anti-clickjacking traduit
        iframeMsg.textContent = t('auth.iframeBlocked', 'L\'application ne peut pas être affichée dans un iframe pour des raisons de sécurité.');
        iframeBlock.appendChild(iframeMsg);
        document.body.textContent = '';
        document.body.appendChild(iframeBlock);
        return;
      }

      // Build HTTPS indicator via DOM (no innerHTML for security)
      var securityBadgeDiv = document.createElement('div');
      var isSecure = location.protocol === 'https:';
      securityBadgeDiv.style.cssText = 'padding:6px 24px;font-size:0.78rem;display:flex;align-items:center;gap:6px;' +
        (isSecure ? 'background:rgba(34,197,94,0.15);color:#22c55e;' : 'background:rgba(245,158,11,0.15);color:#f59e0b;');
      var badgeIcon = document.createElement('i');
      badgeIcon.className = isSecure ? 'fas fa-lock' : 'fas fa-exclamation-triangle';
      securityBadgeDiv.appendChild(badgeIcon);
      var badgeText = document.createElement('span');
      // FIX-I18N-AUTH (audit 2026-06-10) : indicateur HTTPS traduit
      badgeText.textContent = isSecure
        ? t('auth.connEncrypted', 'Connexion chiffrée') + ' (HTTPS)'
        : t('auth.connUnencrypted', 'Connexion non chiffrée') + ' \u2014 ' + t('auth.useHttpsProd', 'utilisez HTTPS en production');
      securityBadgeDiv.appendChild(badgeText);

      var overlay = document.createElement('div');
      overlay.className = 'license-modal-overlay';
      // Opaque background — hides the dashboard completely for security
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg, #1e293b 0%, #334155 50%, #1e293b 100%);display:flex;align-items:center;justify-content:center;z-index:10000;animation:fadeIn 0.2s ease;';

      // Build modal via DOM — org is XSS-safe (escaped above)
      var modal = document.createElement('div');
      modal.style.cssText = 'width:90%;max-width:420px;background:var(--color-surface,#fff);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.2);overflow:hidden;';

      // Header
      var header = document.createElement('div');
      header.style.cssText = 'padding:20px 24px;background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%);color:white;';
      var h3 = document.createElement('h3');
      h3.style.cssText = 'margin:0;font-size:1.2rem;';
      var shieldIcon = document.createElement('i');
      shieldIcon.className = 'fas fa-shield-alt';
      h3.appendChild(shieldIcon);
      h3.appendChild(document.createTextNode(' Cyber-Assistant'));
      header.appendChild(h3);
      var subtitle = document.createElement('p');
      subtitle.style.cssText = 'margin:4px 0 0;font-size:0.9rem;opacity:0.9;';
      subtitle.textContent = t('auth.loginRequired', 'Accéder au Système de Management de la Sécurité');
      header.appendChild(subtitle);
      if (org) {
        var orgP = document.createElement('p');
        orgP.style.cssText = 'margin:4px 0 0;font-size:0.8rem;opacity:0.7;';
        orgP.textContent = orgRaw; // use raw text, textContent escapes it
        header.appendChild(orgP);
      }
      modal.appendChild(header);
      modal.appendChild(securityBadgeDiv);

      // Form body
      var formBody = document.createElement('div');
      formBody.style.cssText = 'padding:24px;';

      // FIX-AUTH-STATUS (audit 2026-06-10) : bandeau d'indisponibilité serveur (statut en
      // erreur HTTP au boot) + bouton réessayer qui re-tente getStatus via showLoginModal.
      if (status && status.serverUnavailable) {
        var unavailDiv = document.createElement('div');
        unavailDiv.style.cssText = 'padding:8px 12px;background:#fef3c7;color:#92400e;border-radius:6px;margin-bottom:12px;font-size:0.85rem;';
        var unavailMsg = document.createElement('div');
        unavailMsg.textContent = t('auth.serverUnavailable', 'Serveur momentanément indisponible — la connexion peut échouer.');
        unavailDiv.appendChild(unavailMsg);
        var retryBtn = document.createElement('button');
        retryBtn.style.cssText = 'margin-top:6px;padding:4px 12px;border:1px solid #92400e;background:transparent;color:#92400e;border-radius:4px;cursor:pointer;font-size:0.8rem;';
        retryBtn.textContent = t('common.retry', 'Réessayer');
        retryBtn.addEventListener('click', function() {
          overlay.remove();
          self.showLoginModal(callback);
        });
        unavailDiv.appendChild(retryBtn);
        formBody.appendChild(unavailDiv);
      }

      var nameLabel = document.createElement('label');
      nameLabel.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
      nameLabel.textContent = t('auth.username', 'Nom d\'utilisateur');
      formBody.appendChild(nameLabel);

      var nameInput = document.createElement('input');
      nameInput.type = 'text';
      nameInput.id = 'csiLoginName';
      nameInput.autocomplete = 'username';
      nameInput.spellcheck = false;
      nameInput.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;';
      formBody.appendChild(nameInput);

      var pwdLabel = document.createElement('label');
      pwdLabel.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
      pwdLabel.textContent = t('auth.password', 'Mot de passe');
      formBody.appendChild(pwdLabel);

      var pwdInput = document.createElement('input');
      pwdInput.type = 'password';
      pwdInput.id = 'csiLoginPwd';
      pwdInput.autocomplete = 'current-password';
      pwdInput.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:4px;font-size:0.95rem;box-sizing:border-box;';
      formBody.appendChild(pwdInput);

      var capsLockWarn = document.createElement('div');
      capsLockWarn.id = 'csiCapsLockWarn';
      capsLockWarn.style.cssText = 'display:none;padding:4px 0;color:#f59e0b;font-size:0.78rem;margin-bottom:8px;';
      // FIX-I18N-AUTH (audit 2026-06-10) : avertissement verr. maj. traduit
      capsLockWarn.textContent = '\u26a0\ufe0f ' + t('auth.capsLockOn', 'Verrouillage majuscules activé');
      formBody.appendChild(capsLockWarn);

      var attemptWarn = document.createElement('div');
      attemptWarn.id = 'csiAttemptWarn';
      attemptWarn.style.cssText = 'display:none;padding:6px 10px;background:#fef3c7;color:#92400e;border-radius:6px;margin-bottom:8px;font-size:0.8rem;';
      formBody.appendChild(attemptWarn);

      var errorDiv = document.createElement('div');
      errorDiv.id = 'csiLoginError';
      errorDiv.style.cssText = 'display:none;padding:8px 12px;background:#fee2e2;color:#b91c1c;border-radius:6px;margin-bottom:12px;font-size:0.85rem;';
      formBody.appendChild(errorDiv);

      var cooldownDiv = document.createElement('div');
      cooldownDiv.id = 'csiLoginCooldown';
      cooldownDiv.style.cssText = 'display:none;padding:10px 12px;background:#fef3c7;color:#92400e;border-radius:6px;margin-bottom:12px;font-size:0.85rem;text-align:center;';
      var clockIcon = document.createElement('i');
      clockIcon.className = 'fas fa-clock';
      cooldownDiv.appendChild(clockIcon);
      cooldownDiv.appendChild(document.createTextNode(' '));
      var cooldownText = document.createElement('span');
      cooldownText.id = 'csiCooldownText';
      cooldownDiv.appendChild(cooldownText);
      formBody.appendChild(cooldownDiv);

      var loginBtn = document.createElement('button');
      loginBtn.id = 'csiLoginBtn';
      loginBtn.style.cssText = 'width:100%;padding:10px;border:none;background:var(--color-primary,#3b82f6);color:white;border-radius:6px;cursor:pointer;font-size:0.95rem;font-weight:500;';
      loginBtn.textContent = t('auth.login', 'Se connecter');
      formBody.appendChild(loginBtn);

      // --- SSO Button (only if SSO is configured) ---
      var ssoBtn = null; // declared here for scope in login error handler
      if (status && status.ssoEnabled) {
        var ssoMode = status.ssoMode || 'sso_only';

        if (ssoMode === 'sso_only') {
          // Hide the password form by default, show only SSO button
          nameLabel.style.display = 'none';
          nameInput.style.display = 'none';
          pwdLabel.style.display = 'none';
          pwdInput.style.display = 'none';
          capsLockWarn.style.display = 'none';
          loginBtn.style.display = 'none';
        }

        // Separator (only visible when both forms shown)
        if (ssoMode === 'sso_and_password') {
          var separator = document.createElement('div');
          separator.style.cssText = 'display:flex;align-items:center;gap:12px;margin:16px 0;';
          var sepLine1 = document.createElement('div');
          sepLine1.style.cssText = 'flex:1;height:1px;background:var(--color-border,#ddd);';
          var sepText = document.createElement('span');
          sepText.style.cssText = 'font-size:0.8rem;color:var(--color-text-secondary,#999);';
          sepText.textContent = t('auth.or', 'ou'); // FIX-I18N-AUTH
          var sepLine2 = document.createElement('div');
          sepLine2.style.cssText = 'flex:1;height:1px;background:var(--color-border,#ddd);';
          separator.appendChild(sepLine1);
          separator.appendChild(sepText);
          separator.appendChild(sepLine2);
          formBody.appendChild(separator);
        }

        ssoBtn = document.createElement('button');
        ssoBtn.style.cssText = 'width:100%;padding:10px;border:1px solid var(--color-primary,#3b82f6);background:transparent;color:var(--color-primary,#3b82f6);border-radius:6px;cursor:pointer;font-size:0.95rem;font-weight:500;display:flex;align-items:center;justify-content:center;gap:8px;';
        var ssoIcon = document.createElement('i');
        ssoIcon.className = 'fas fa-building';
        ssoBtn.appendChild(ssoIcon);
        ssoBtn.appendChild(document.createTextNode(t('auth.ssoLogin', 'Se connecter via SSO entreprise')));
        ssoBtn.addEventListener('click', function() {
          window.location.href = self.getSsoInitUrl();
        });
        formBody.appendChild(ssoBtn);

        // Admin local access link (sso_only mode only)
        if (ssoMode === 'sso_only') {
          var adminLink = document.createElement('div');
          adminLink.style.cssText = 'margin-top:20px;text-align:center;';
          var adminA = document.createElement('a');
          adminA.href = '#';
          adminA.style.cssText = 'font-size:0.78rem;color:var(--color-text-secondary,#999);text-decoration:none;';
          adminA.textContent = t('auth.adminAccess', 'Accès administrateur');
          adminA.addEventListener('click', function(e) {
            e.preventDefault();
            var isVisible = nameLabel.style.display !== 'none';
            nameLabel.style.display = isVisible ? 'none' : 'block';
            nameInput.style.display = isVisible ? 'none' : 'block';
            pwdLabel.style.display = isVisible ? 'none' : 'block';
            pwdInput.style.display = isVisible ? 'none' : 'block';
            loginBtn.style.display = isVisible ? 'none' : 'block';
            adminA.textContent = isVisible
              ? t('auth.adminAccess', 'Accès administrateur')
              : t('auth.hideAdminAccess', 'Masquer');
            if (!isVisible) nameInput.focus();
          });
          adminLink.appendChild(adminA);
          formBody.appendChild(adminLink);
        }
      }

      // --- Cybersecurity quote ---
      var _quote = CSI_CYBER_QUOTES[Math.floor(Math.random() * CSI_CYBER_QUOTES.length)];
      var quoteBox = document.createElement('div');
      quoteBox.style.cssText = 'margin-top:20px;padding:16px 0 0;border-top:1px solid rgba(59,130,246,0.15);text-align:center;';
      var quoteText = document.createElement('p');
      quoteText.style.cssText = 'font-style:italic;font-size:0.8rem;color:var(--color-text-secondary,#999);margin:0 0 6px 0;line-height:1.5;';
      quoteText.textContent = '\u00ab\u00a0' + _quote.text + '\u00a0\u00bb';
      var quoteAuthor = document.createElement('p');
      quoteAuthor.style.cssText = 'font-size:0.75rem;color:var(--color-text-secondary,#999);margin:0;opacity:0.7;';
      quoteAuthor.textContent = '\u2014 ' + _quote.author;
      quoteBox.appendChild(quoteText);
      quoteBox.appendChild(quoteAuthor);
      formBody.appendChild(quoteBox);

      modal.appendChild(formBody);
      overlay.appendChild(modal);
      document.body.appendChild(overlay);

      // Fix C6: Clean up countdown timer if overlay is removed from DOM
      var _overlayObserver = new MutationObserver(function() {
        if (!document.body.contains(overlay)) {
          if (_loginCountdownTimer) { clearInterval(_loginCountdownTimer); _loginCountdownTimer = null; }
          if (_loginInactivityTimer) { clearTimeout(_loginInactivityTimer); _loginInactivityTimer = null; }
          _overlayObserver.disconnect();
        }
      });
      _overlayObserver.observe(document.body, { childList: true });

      // Caps Lock detection
      pwdInput.addEventListener('keydown', function(e) {
        if (e.getModifierState) {
          capsLockWarn.style.display = e.getModifierState('CapsLock') ? 'block' : 'none';
        }
      });
      pwdInput.addEventListener('keyup', function(e) {
        if (e.getModifierState) {
          capsLockWarn.style.display = e.getModifierState('CapsLock') ? 'block' : 'none';
        }
      });

      // Inactivity timeout — clear fields after 5 minutes
      var _loginInactivityTimer = null;
      var LOGIN_INACTIVITY_TIMEOUT = 5 * 60 * 1000;
      function resetInactivityTimer() {
        if (_loginInactivityTimer) clearTimeout(_loginInactivityTimer);
        _loginInactivityTimer = setTimeout(function() {
          nameInput.value = '';
          pwdInput.value = '';
          hideError();
          attemptWarn.style.display = 'none';
          showError(t('auth.sessionTimeout', 'Formulaire expiré par inactivité. Veuillez ressaisir vos identifiants.'));
        }, LOGIN_INACTIVITY_TIMEOUT);
      }
      nameInput.addEventListener('input', resetInactivityTimer);
      pwdInput.addEventListener('input', resetInactivityTimer);
      nameInput.addEventListener('focus', resetInactivityTimer);
      pwdInput.addEventListener('focus', resetInactivityTimer);
      resetInactivityTimer();

      function showError(msg) {
        errorDiv.textContent = msg;
        errorDiv.style.display = 'block';
      }

      function hideError() {
        errorDiv.style.display = 'none';
      }

      function updateAttemptWarning() {
        if (_loginFailCount > 0 && _loginFailCount < CLIENT_LOCKOUT_THRESHOLD) {
          // FIX-I18N-AUTH (audit 2026-06-10) : compteur de tentatives traduit
          attemptWarn.textContent = t('auth.attemptWarning', 'Tentative {n}/{max} — verrouillage après {max} échecs')
            .replace('{n}', _loginFailCount).replace(/\{max\}/g, CLIENT_LOCKOUT_THRESHOLD);
          attemptWarn.style.display = 'block';
        } else {
          attemptWarn.style.display = 'none';
        }
      }

      function setFormDisabled(disabled) {
        nameInput.disabled = disabled;
        pwdInput.disabled = disabled;
        loginBtn.disabled = disabled;
        loginBtn.style.opacity = disabled ? '0.5' : '1';
        loginBtn.style.cursor = disabled ? 'not-allowed' : 'pointer';
      }

      function startCooldown(durationMs) {
        var newLockUntil = Date.now() + durationMs;
        if (newLockUntil > _loginLockUntil) {
          _loginLockUntil = newLockUntil;
        }
        setFormDisabled(true);
        hideError();
        attemptWarn.style.display = 'none';
        cooldownDiv.style.display = 'block';

        function updateCountdown() {
          var remaining = Math.max(0, Math.ceil((_loginLockUntil - Date.now()) / 1000));
          if (remaining <= 0) {
            clearInterval(_loginCountdownTimer);
            _loginCountdownTimer = null;
            cooldownDiv.style.display = 'none';
            setFormDisabled(false);
            loginBtn.textContent = t('auth.login', 'Se connecter');
            return;
          }
          var min = Math.floor(remaining / 60);
          var sec = remaining % 60;
          var timeStr = min > 0 ? (min + 'min ' + (sec < 10 ? '0' : '') + sec + 's') : (sec + 's');
          cooldownText.textContent = t('auth.cooldown', 'Trop de tentatives. Réessayez dans ') + timeStr;
        }

        if (_loginCountdownTimer) clearInterval(_loginCountdownTimer);
        updateCountdown();
        _loginCountdownTimer = setInterval(updateCountdown, 1000);
      }

      // Check if already in cooldown on form show
      if (_loginLockUntil > Date.now()) {
        startCooldown(_loginLockUntil - Date.now());
      }

      loginBtn.addEventListener('click', function() {
        resetInactivityTimer();
        // Check client-side cooldown
        if (_loginLockUntil > Date.now()) {
          startCooldown(_loginLockUntil - Date.now());
          return;
        }

        var name = nameInput.value.trim();
        var pwd = pwdInput.value;
        if (!name || !pwd) { showError(t('auth.fieldsRequired', 'Tous les champs sont requis')); return; }
        loginBtn.disabled = true;
        loginBtn.textContent = '...';
        self.login(name, pwd).then(function(r) {
          if (r.ok) {
            // Success -> reset lockout state
            _loginFailCount = 0;
            _loginLockUntil = 0;
            if (_loginCountdownTimer) { clearInterval(_loginCountdownTimer); _loginCountdownTimer = null; }
            if (_loginInactivityTimer) { clearTimeout(_loginInactivityTimer); _loginInactivityTimer = null; }
            // M26 (audit 2026-07-10, corrigé post-revue de régression) : un RE-LOGIN en cours
            // de session (refresh rejeté, 3×401) ne doit exposer ni l'état mémoire ni le
            // périmètre de l'utilisateur PRÉCÉDENT à une AUTRE personne sur le même poste. On
            // ne recharge donc QUE si l'identité ré-authentifiée DIFFÈRE de celle de la session
            // en cours. Pour le MÊME utilisateur, on NE recharge PAS : on laisse le flux normal
            // (callback) s'exécuter — notamment le replay FIX-FULLSYNC-STALE de sync-manager
            // (requestServerSync(keys)) qui repousse les éditions non synchronisées. Recharger
            // dans ce cas neutralisait ce replay et perdait les modifications en attente.
            var _reNewUserId = (r.data && r.data.user && r.data.user.id) || null;
            if (self._sessionEstablished && self._sessionUserId && _reNewUserId
                && _reNewUserId !== self._sessionUserId) {
              overlay.remove();
              location.reload();
              return;
            }
            self._sessionEstablished = true;
            self._sessionUserId = _reNewUserId;
            // Show last login info if available
            if (r.data && r.data.user && r.data.user.lastLoginAt) {
              var lastDate = new Date(r.data.user.lastLoginAt);
              var lastStr = lastDate.toLocaleDateString() + ' ' + lastDate.toLocaleTimeString();
              var lastIp = r.data.user.lastLoginIp || '';
              var toastMsg = t('auth.lastLogin', 'Dernière connexion') + ' : ' + lastStr;
              if (lastIp) toastMsg += ' (' + lastIp + ')';
              setTimeout(function() {
                if (window.showToast2) window.showToast2(toastMsg, 'info');
              }, 500);
            }
            overlay.remove();
            // Note: mustChangePassword is handled by the initApp flow (bundle.js)
            // which checks _authMgr.mustChangePassword() after login callback resolves.
            // Do NOT show the force-change modal here — it would create a duplicate
            // modal without the proper callback to resolve the init Promise.
            if (callback) callback(true);
          } else {
            _loginFailCount++;
            updateAttemptWarning();
            // Server returned 429 with lockout info
            if (r.data && r.data.locked && r.data.retryAfter) {
              startCooldown(r.data.retryAfter * 1000);
            } else if (_loginFailCount >= CLIENT_LOCKOUT_THRESHOLD) {
              startCooldown(_getClientLockDuration(_loginFailCount));
            } else {
              showError(r.data.error || t('auth.loginFailed', 'Échec de connexion'));
              // If server says this account requires SSO
              if (r.data && r.data.ssoRequired && ssoBtn) {
                ssoBtn.style.borderColor = '#22c55e';
                ssoBtn.style.color = '#22c55e';
                ssoBtn.style.fontWeight = '700';
                setTimeout(function() {
                  ssoBtn.style.borderColor = '';
                  ssoBtn.style.color = '';
                  ssoBtn.style.fontWeight = '';
                }, 3000);
              }
              loginBtn.disabled = false;
              loginBtn.textContent = t('auth.login', 'Se connecter');
            }
          }
        }).catch(function() {
          _loginFailCount++;
          updateAttemptWarning();
          if (_loginFailCount >= CLIENT_LOCKOUT_THRESHOLD) {
            startCooldown(_getClientLockDuration(_loginFailCount));
          } else {
            showError(t('auth.loginFailed', 'Échec de connexion'));
            loginBtn.disabled = false;
            loginBtn.textContent = t('auth.login', 'Se connecter');
          }
        });
      });

      pwdInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') loginBtn.click(); });

      // Non-dismissible: no backdrop click, no Escape key

      setTimeout(function() { nameInput.focus(); }, 100);
    },

    _showSetupModal: function(callback) {
      var self = this;
      var t = window.t || function(k, d) { return d || k; };

      // Anti-clickjacking: refuse to render inside an iframe
      if (window.self !== window.top) {
        var iframeBlock = document.createElement('div');
        iframeBlock.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:#1e293b;display:flex;align-items:center;justify-content:center;z-index:10000;color:white;font-size:1.2rem;padding:40px;text-align:center;';
        // FIX-I18N-AUTH (audit 2026-06-10) : message anti-clickjacking traduit
        iframeBlock.textContent = t('auth.iframeBlocked', 'L\'application ne peut pas être affichée dans un iframe pour des raisons de sécurité.');
        document.body.appendChild(iframeBlock);
        return;
      }

      var overlay = document.createElement('div');
      overlay.className = 'license-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg, #1e293b 0%, #334155 50%, #1e293b 100%);display:flex;align-items:center;justify-content:center;z-index:10000;';

      var card = document.createElement('div');
      card.style.cssText = 'width:90%;max-width:420px;background:var(--color-surface,#fff);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.2);overflow:hidden;';

      // Header
      var header = document.createElement('div');
      header.style.cssText = 'padding:20px 24px;background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%);color:white;';
      var h3 = document.createElement('h3');
      h3.style.cssText = 'margin:0;font-size:1.2rem;';
      var icon = document.createElement('i');
      icon.className = 'fas fa-shield-alt';
      h3.appendChild(icon);
      h3.appendChild(document.createTextNode(' Cyber-Assistant'));
      header.appendChild(h3);
      var subtitle = document.createElement('p');
      subtitle.style.cssText = 'margin:4px 0 0;font-size:0.9rem;opacity:0.9;';
      subtitle.textContent = t('auth.setupTitle', 'Configuration initiale');
      header.appendChild(subtitle);

      // HTTPS indicator
      var isHttps = location.protocol === 'https:';
      var httpsBadge = document.createElement('div');
      httpsBadge.style.cssText = 'margin-top:8px;padding:4px 10px;border-radius:4px;font-size:0.8rem;display:inline-block;' +
        (isHttps ? 'background:rgba(34,197,94,0.2);color:#bbf7d0;' : 'background:rgba(251,146,60,0.3);color:#fed7aa;');
      // FIX-I18N-AUTH (audit 2026-06-10) : indicateur HTTPS traduit
      httpsBadge.textContent = isHttps
        ? '\uD83D\uDD12 ' + t('auth.connEncrypted', 'Connexion chiffrée')
        : '\u26A0\uFE0F ' + t('auth.connUnencrypted', 'Connexion non chiffrée');
      header.appendChild(httpsBadge);
      if (!isHttps) {
        var httpsWarn = document.createElement('p');
        httpsWarn.style.cssText = 'margin:4px 0 0;font-size:0.75rem;opacity:0.8;';
        httpsWarn.textContent = t('auth.httpsProdWarning', 'Les données transitent en clair. Activez HTTPS en production.'); // FIX-I18N-AUTH
        header.appendChild(httpsWarn);
      }

      card.appendChild(header);

      // Body
      var body = document.createElement('div');
      body.style.cssText = 'padding:24px;';

      // Admin notice
      var adminNotice = document.createElement('div');
      adminNotice.style.cssText = 'margin:0 0 16px;padding:10px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;font-size:0.85rem;color:#1e40af;';
      var adminIcon = document.createElement('i');
      adminIcon.className = 'fas fa-user-shield';
      adminIcon.style.marginRight = '6px';
      adminNotice.appendChild(adminIcon);
      adminNotice.appendChild(document.createTextNode(t('auth.setupAdminNotice', 'Le premier utilisateur créé sera administrateur avec tous les droits de gestion.')));
      body.appendChild(adminNotice);

      // Name label + input
      var nameLabel = document.createElement('label');
      nameLabel.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
      nameLabel.textContent = t('auth.yourName', 'Votre nom');
      body.appendChild(nameLabel);
      var nameInput = document.createElement('input');
      nameInput.type = 'text';
      nameInput.autocomplete = 'username';
      nameInput.spellcheck = false;
      nameInput.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;';
      body.appendChild(nameInput);

      // Password label + input
      var pwdLabel = document.createElement('label');
      pwdLabel.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
      pwdLabel.textContent = t('auth.password', 'Mot de passe');
      body.appendChild(pwdLabel);
      var pwdInput = document.createElement('input');
      pwdInput.type = 'password';
      pwdInput.autocomplete = 'new-password';
      pwdInput.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:4px;font-size:0.95rem;box-sizing:border-box;';
      body.appendChild(pwdInput);

      // Caps Lock warning for password
      var capsWarn = document.createElement('div');
      capsWarn.style.cssText = 'display:none;margin-bottom:8px;font-size:0.8rem;color:#d97706;';
      capsWarn.textContent = '\u26A0\uFE0F ' + t('auth.capsLockOn', 'Verrouillage majuscules activé'); // FIX-I18N-AUTH
      body.appendChild(capsWarn);

      // Caps Lock detection
      function checkCaps(e) {
        if (e.getModifierState) {
          capsWarn.style.display = e.getModifierState('CapsLock') ? 'block' : 'none';
        }
      }
      pwdInput.addEventListener('keydown', checkCaps);
      pwdInput.addEventListener('keyup', checkCaps);

      // Spacer when caps warn hidden
      var pwdSpacer = document.createElement('div');
      pwdSpacer.style.cssText = 'margin-bottom:8px;';
      body.appendChild(pwdSpacer);

      // Confirm label + input
      var confirmLabel = document.createElement('label');
      confirmLabel.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
      confirmLabel.textContent = t('auth.confirmPassword', 'Confirmer le mot de passe');
      body.appendChild(confirmLabel);
      var confirmInput = document.createElement('input');
      confirmInput.type = 'password';
      confirmInput.autocomplete = 'new-password';
      confirmInput.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:4px;font-size:0.95rem;box-sizing:border-box;';
      body.appendChild(confirmInput);

      // Caps Lock warning for confirm
      var capsWarn2 = document.createElement('div');
      capsWarn2.style.cssText = 'display:none;margin-bottom:8px;font-size:0.8rem;color:#d97706;';
      capsWarn2.textContent = '\u26A0\uFE0F ' + t('auth.capsLockOn', 'Verrouillage majuscules activé'); // FIX-I18N-AUTH
      body.appendChild(capsWarn2);
      function checkCaps2(e) {
        if (e.getModifierState) {
          capsWarn2.style.display = e.getModifierState('CapsLock') ? 'block' : 'none';
        }
      }
      confirmInput.addEventListener('keydown', checkCaps2);
      confirmInput.addEventListener('keyup', checkCaps2);

      // Password policy
      var policyText = document.createElement('p');
      policyText.style.cssText = 'margin:0 0 16px;font-size:0.8rem;color:var(--color-text-secondary,#999);';
      policyText.textContent = t('auth.passwordPolicy', '18+ caractères, majuscule, minuscule, caractère spécial');
      body.appendChild(policyText);

      // Real-time password strength indicator
      var strengthList = document.createElement('div');
      strengthList.style.cssText = 'margin:0 0 16px;font-size:0.8rem;';
      var checks = [
        // FIX-I18N-AUTH (audit 2026-06-10) : libell\u00e9s traduits (m\u00eames cl\u00e9s que validatePwd)
        { id: 'len', label: t('auth.pwdMinChars', '18+ caractères'), test: function(p) { return p.length >= 18; } },
        { id: 'upper', label: t('auth.pwdUppercase', 'majuscule'), test: function(p) { return /[A-Z]/.test(p); } },
        { id: 'lower', label: t('auth.pwdLowercase', 'minuscule'), test: function(p) { return /[a-z]/.test(p); } },
        { id: 'special', label: t('auth.pwdSpecial', 'caractère spécial'), test: function(p) { return /[^a-zA-Z0-9]/.test(p); } }
      ];
      var checkEls = [];
      checks.forEach(function(c) {
        var row = document.createElement('div');
        row.style.cssText = 'margin-bottom:2px;color:#999;';
        row.textContent = '\u2716 ' + c.label;
        strengthList.appendChild(row);
        checkEls.push({ el: row, test: c.test });
      });
      body.appendChild(strengthList);

      pwdInput.addEventListener('input', function() {
        var val = pwdInput.value;
        checkEls.forEach(function(c) {
          var ok = c.test(val);
          c.el.style.color = ok ? '#16a34a' : '#999';
          c.el.textContent = (ok ? '\u2714 ' : '\u2716 ') + c.el.textContent.substring(2);
        });
      });

      // Error div
      var errorDiv = document.createElement('div');
      errorDiv.style.cssText = 'display:none;padding:8px 12px;background:#fee2e2;color:#b91c1c;border-radius:6px;margin-bottom:12px;font-size:0.85rem;';
      body.appendChild(errorDiv);

      // Submit button
      var setupBtn = document.createElement('button');
      setupBtn.style.cssText = 'width:100%;padding:10px;border:none;background:var(--color-primary,#3b82f6);color:white;border-radius:6px;cursor:pointer;font-size:0.95rem;font-weight:500;';
      setupBtn.textContent = t('auth.createAdminAccount', 'Créer le compte admin');
      body.appendChild(setupBtn);

      card.appendChild(body);
      overlay.appendChild(card);
      document.body.appendChild(overlay);

      function showError(msg) {
        errorDiv.textContent = msg;
        errorDiv.style.display = 'block';
      }

      function validatePwd(pwd) {
        var errors = [];
        if (!pwd || pwd.length < 18) errors.push(t('auth.pwdMinChars', '18+ caractères'));
        if (!/[A-Z]/.test(pwd)) errors.push(t('auth.pwdUppercase', 'majuscule'));
        if (!/[a-z]/.test(pwd)) errors.push(t('auth.pwdLowercase', 'minuscule'));
        if (!/[^a-zA-Z0-9]/.test(pwd)) errors.push(t('auth.pwdSpecial', 'caractère spécial'));
        return errors;
      }

      setupBtn.addEventListener('click', function() {
        var name = nameInput.value.trim();
        var pwd = pwdInput.value;
        var confirm = confirmInput.value;
        if (!name) { showError(t('auth.nameRequired', 'Nom requis')); return; }
        var errors = validatePwd(pwd);
        if (errors.length) { showError(t('auth.password', 'Mot de passe') + ': ' + errors.join(', ')); return; } // FIX-I18N-AUTH
        if (pwd !== confirm) { showError(t('auth.passwordMismatch', 'Les mots de passe ne correspondent pas')); return; }

        setupBtn.disabled = true;
        setupBtn.textContent = '...';
        self.setup(name, pwd).then(function(r) {
          if (r.ok) {
            overlay.remove();
            if (typeof window.showToast2 === 'function') {
              window.showToast2(t('auth.accountCreated', 'Compte admin créé'), 'success');
            }
            if (callback) callback(true);
          } else {
            showError(r.data.error || t('common.error', 'Erreur'));
            setupBtn.disabled = false;
            setupBtn.textContent = t('auth.createAdminAccount', 'Créer le compte admin');
          }
        }).catch(function() {
          showError(t('auth.setupFailed', 'Erreur lors de la configuration'));
          setupBtn.disabled = false;
          setupBtn.textContent = t('auth.createAdminAccount', 'Créer le compte admin');
        });
      });

      // Setup modal must not be dismissible by backdrop click — user must authenticate
      setTimeout(function() { nameInput.focus(); }, 100);
    },

    _showRegisterModal: function(callback) {
      var self = this;
      var t = window.t || function(k, d) { return d || k; };

      var overlay = document.createElement('div');
      overlay.className = 'license-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:10000;';

      // BUG H21 XSS fix: build modal via DOM API instead of innerHTML with t() values
      var modalBox = document.createElement('div');
      modalBox.style.cssText = 'width:90%;max-width:420px;background:var(--color-surface,#fff);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.2);overflow:hidden;';

      var headerDiv = document.createElement('div');
      headerDiv.style.cssText = 'padding:20px 24px;background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%);color:white;';
      var h3 = document.createElement('h3');
      h3.style.cssText = 'margin:0;font-size:1.2rem;';
      var icon = document.createElement('i');
      icon.className = 'fas fa-user-plus';
      h3.appendChild(icon);
      h3.appendChild(document.createTextNode(' ' + t('auth.createAccount', 'Créer un compte')));
      headerDiv.appendChild(h3);
      modalBox.appendChild(headerDiv);

      var bodyDiv = document.createElement('div');
      bodyDiv.style.cssText = 'padding:24px;';

      function createLabel(text) {
        var lbl = document.createElement('label');
        lbl.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
        lbl.textContent = text;
        return lbl;
      }
      function createInput(type, id, ac) {
        var inp = document.createElement('input');
        inp.type = type;
        inp.id = id;
        inp.autocomplete = ac;
        inp.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;';
        return inp;
      }

      bodyDiv.appendChild(createLabel(t('auth.username', 'Nom d\'utilisateur')));
      bodyDiv.appendChild(createInput('text', 'csiRegName', 'username'));
      bodyDiv.appendChild(createLabel(t('auth.password', 'Mot de passe')));
      bodyDiv.appendChild(createInput('password', 'csiRegPwd', 'new-password'));
      bodyDiv.appendChild(createLabel(t('auth.confirmPassword', 'Confirmer le mot de passe')));
      bodyDiv.appendChild(createInput('password', 'csiRegConfirm', 'new-password'));

      var pPolicy = document.createElement('p');
      pPolicy.style.cssText = 'margin:0 0 16px;font-size:0.8rem;color:var(--color-text-secondary,#999);';
      pPolicy.textContent = t('auth.passwordPolicy', '18+ caractères, majuscule, minuscule, caractère spécial');
      bodyDiv.appendChild(pPolicy);

      var errDiv = document.createElement('div');
      errDiv.id = 'csiRegError';
      errDiv.style.cssText = 'display:none;padding:8px 12px;background:#fee2e2;color:#b91c1c;border-radius:6px;margin-bottom:12px;font-size:0.85rem;';
      bodyDiv.appendChild(errDiv);

      var regButton = document.createElement('button');
      regButton.id = 'csiRegBtn';
      regButton.style.cssText = 'width:100%;padding:10px;border:none;background:var(--color-primary,#3b82f6);color:white;border-radius:6px;cursor:pointer;font-size:0.95rem;font-weight:500;';
      regButton.textContent = t('auth.createAccount', 'Créer un compte');
      bodyDiv.appendChild(regButton);

      var backDiv = document.createElement('div');
      backDiv.style.cssText = 'margin-top:12px;text-align:center;font-size:0.85rem;';
      var backA = document.createElement('a');
      backA.href = '#';
      backA.id = 'csiBackToLogin';
      backA.style.cssText = 'color:var(--color-primary,#3b82f6);';
      backA.textContent = t('auth.backToLogin', 'Retour à la connexion');
      backDiv.appendChild(backA);
      bodyDiv.appendChild(backDiv);

      modalBox.appendChild(bodyDiv);
      overlay.appendChild(modalBox);

      document.body.appendChild(overlay);

      var nameInput = overlay.querySelector('#csiRegName');
      var pwdInput = overlay.querySelector('#csiRegPwd');
      var confirmInput = overlay.querySelector('#csiRegConfirm');
      var regBtn = overlay.querySelector('#csiRegBtn');
      var errorDiv = overlay.querySelector('#csiRegError');
      var backLink = overlay.querySelector('#csiBackToLogin');

      function showError(msg) { errorDiv.textContent = msg; errorDiv.style.display = 'block'; }

      function validatePwd(pwd) {
        var errors = [];
        if (!pwd || pwd.length < 18) errors.push(t('auth.pwdMinChars', '18+ caractères'));
        if (!/[A-Z]/.test(pwd)) errors.push(t('auth.pwdUppercase', 'majuscule'));
        if (!/[a-z]/.test(pwd)) errors.push(t('auth.pwdLowercase', 'minuscule'));
        if (!/[^a-zA-Z0-9]/.test(pwd)) errors.push(t('auth.pwdSpecial', 'caractère spécial'));
        return errors;
      }

      regBtn.addEventListener('click', function() {
        var name = nameInput.value.trim();
        var pwd = pwdInput.value;
        var confirm = confirmInput.value;
        if (!name) { showError(t('auth.nameRequired', 'Nom requis')); return; }
        var errors = validatePwd(pwd);
        if (errors.length) { showError(t('auth.password', 'Mot de passe') + ': ' + errors.join(', ')); return; } // FIX-I18N-AUTH
        if (pwd !== confirm) { showError(t('auth.passwordMismatch', 'Les mots de passe ne correspondent pas')); return; }

        regBtn.disabled = true;
        regBtn.textContent = '...';
        self.register(name, pwd).then(function(r) {
          if (r.ok) {
            overlay.remove();
            if (typeof window.showToast2 === 'function') {
              window.showToast2(t('auth.userCreated', 'Compte créé'), 'success');
            }
            if (callback) callback(true);
          } else {
            showError(r.data.error || t('common.error', 'Erreur'));
            regBtn.disabled = false;
            regBtn.textContent = t('auth.createAccount', 'Créer un compte');
          }
        }).catch(function() {
          showError(t('auth.registrationFailed', 'Erreur lors de l\'inscription'));
          regBtn.disabled = false;
          regBtn.textContent = t('auth.createAccount', 'Créer un compte');
        });
      });

      backLink.addEventListener('click', function(e) {
        e.preventDefault();
        overlay.remove();
        self.showLoginModal(callback);
      });

      // Register modal must not be dismissible by backdrop click — user must authenticate
      setTimeout(function() { nameInput.focus(); }, 100);
    },

    // ============================================================
    // UI: Changement de mot de passe
    // ============================================================

    /**
     * Modal forcée (non-dismissible) — after first login with temp password
     */
    showForcePasswordChangeModal: function(callback) {
      this._showPasswordChangeUI(false, callback);
    },

    /**
     * Modal volontaire (dismissible) — user wants to change password
     */
    showChangePasswordModal: function(callback) {
      this._showPasswordChangeUI(true, callback);
    },

    _showPasswordChangeUI: function(dismissible, callback) {
      var self = this;
      var t = window.t || function(k, d) { return d || k; };

      var overlay = document.createElement('div');
      overlay.className = 'license-modal-overlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:' + (dismissible ? 'rgba(0,0,0,0.5)' : 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #1e293b 100%)') + ';display:flex;align-items:center;justify-content:center;z-index:10001;';

      // BUG H21 XSS fix: build modal via DOM API instead of innerHTML with t() values
      var titleText = dismissible ? t('auth.changePassword', 'Changer le mot de passe') : t('auth.mustChangePassword', 'Changement de mot de passe obligatoire');

      var chgModalBox = document.createElement('div');
      chgModalBox.style.cssText = 'width:90%;max-width:420px;background:var(--color-surface,#fff);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.2);overflow:hidden;';

      var chgHeader = document.createElement('div');
      chgHeader.style.cssText = 'padding:20px 24px;background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%);color:white;';
      var chgH3 = document.createElement('h3');
      chgH3.style.cssText = 'margin:0;font-size:1.2rem;';
      var chgIcon = document.createElement('i');
      chgIcon.className = 'fas fa-key';
      chgH3.appendChild(chgIcon);
      chgH3.appendChild(document.createTextNode(' ' + titleText));
      chgHeader.appendChild(chgH3);
      if (!dismissible) {
        var subtitleP = document.createElement('p');
        subtitleP.style.cssText = 'margin:4px 0 0;font-size:0.85rem;opacity:0.9;';
        subtitleP.textContent = t('auth.mustChangePasswordHint', 'Votre mot de passe temporaire doit être changé.');
        chgHeader.appendChild(subtitleP);
      }
      chgModalBox.appendChild(chgHeader);

      var chgBody = document.createElement('div');
      chgBody.style.cssText = 'padding:24px;';

      function createChgLabel(text) {
        var lbl = document.createElement('label');
        lbl.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;font-size:0.9rem;color:var(--color-text,#333);';
        lbl.textContent = text;
        return lbl;
      }
      function createChgInput(id, ac) {
        var inp = document.createElement('input');
        inp.type = 'password';
        inp.id = id;
        inp.autocomplete = ac;
        inp.style.cssText = 'width:100%;padding:10px 12px;border:1px solid var(--color-border,#ddd);border-radius:6px;margin-bottom:12px;font-size:0.95rem;box-sizing:border-box;';
        return inp;
      }

      chgBody.appendChild(createChgLabel(t('auth.currentPassword', 'Mot de passe actuel')));
      chgBody.appendChild(createChgInput('csiChgCurrent', 'current-password'));
      chgBody.appendChild(createChgLabel(t('auth.newPassword', 'Nouveau mot de passe')));
      chgBody.appendChild(createChgInput('csiChgNew', 'new-password'));
      chgBody.appendChild(createChgLabel(t('auth.confirmPassword', 'Confirmer le mot de passe')));
      chgBody.appendChild(createChgInput('csiChgConfirm', 'new-password'));

      var chgPolicy = document.createElement('p');
      chgPolicy.style.cssText = 'margin:0 0 16px;font-size:0.8rem;color:var(--color-text-secondary,#999);';
      chgPolicy.textContent = t('auth.passwordPolicy', '18+ caractères, majuscule, minuscule, caractère spécial');
      chgBody.appendChild(chgPolicy);

      var chgErrDiv = document.createElement('div');
      chgErrDiv.id = 'csiChgError';
      chgErrDiv.style.cssText = 'display:none;padding:8px 12px;background:#fee2e2;color:#b91c1c;border-radius:6px;margin-bottom:12px;font-size:0.85rem;';
      chgBody.appendChild(chgErrDiv);

      var btnRow = document.createElement('div');
      btnRow.style.cssText = 'display:flex;gap:12px;';
      if (dismissible) {
        var cancelButton = document.createElement('button');
        cancelButton.id = 'csiChgCancel';
        cancelButton.style.cssText = 'flex:1;padding:10px;border:1px solid var(--color-border,#ddd);background:var(--color-surface,#fff);border-radius:6px;cursor:pointer;font-size:0.95rem;';
        cancelButton.textContent = t('common.cancel', 'Annuler');
        btnRow.appendChild(cancelButton);
      }
      var chgButton = document.createElement('button');
      chgButton.id = 'csiChgBtn';
      chgButton.style.cssText = 'flex:1;padding:10px;border:none;background:var(--color-primary,#3b82f6);color:white;border-radius:6px;cursor:pointer;font-size:0.95rem;font-weight:500;';
      chgButton.textContent = t('auth.changePasswordBtn', 'Changer');
      btnRow.appendChild(chgButton);
      chgBody.appendChild(btnRow);

      chgModalBox.appendChild(chgBody);
      overlay.appendChild(chgModalBox);
      document.body.appendChild(overlay);

      var currentInput = overlay.querySelector('#csiChgCurrent');
      var newInput = overlay.querySelector('#csiChgNew');
      var confirmInput = overlay.querySelector('#csiChgConfirm');
      var chgBtn = overlay.querySelector('#csiChgBtn');
      var cancelBtn = overlay.querySelector('#csiChgCancel');
      var errorDiv = overlay.querySelector('#csiChgError');

      function showError(msg) { errorDiv.textContent = msg; errorDiv.style.display = 'block'; }

      function validatePwd(pwd) {
        var errors = [];
        if (!pwd || pwd.length < 18) errors.push(t('auth.pwdMinChars', '18+ caractères'));
        if (!/[A-Z]/.test(pwd)) errors.push(t('auth.pwdUppercase', 'majuscule'));
        if (!/[a-z]/.test(pwd)) errors.push(t('auth.pwdLowercase', 'minuscule'));
        if (!/[^a-zA-Z0-9]/.test(pwd)) errors.push(t('auth.pwdSpecial', 'caractère spécial'));
        return errors;
      }

      chgBtn.addEventListener('click', function() {
        var current = currentInput.value;
        var newPwd = newInput.value;
        var confirm = confirmInput.value;
        if (!current || !newPwd) { showError(t('auth.fieldsRequired', 'Tous les champs sont requis')); return; }
        var errors = validatePwd(newPwd);
        if (errors.length) { showError(t('auth.password', 'Mot de passe') + ': ' + errors.join(', ')); return; } // FIX-I18N-AUTH
        if (newPwd !== confirm) { showError(t('auth.passwordMismatch', 'Les mots de passe ne correspondent pas')); return; }

        chgBtn.disabled = true;
        chgBtn.textContent = '...';
        self.changePassword(current, newPwd).then(function(r) {
          if (r.ok) {
            if (_currentUser) _currentUser.mustChangePassword = false;
            overlay.remove();
            if (typeof window.showToast2 === 'function') {
              window.showToast2(t('auth.passwordChanged', 'Mot de passe changé'), 'success');
            }
            if (callback) callback(true);
          } else {
            showError(r.data.error || t('common.error', 'Erreur'));
            chgBtn.disabled = false;
            chgBtn.textContent = t('auth.changePasswordBtn', 'Changer');
          }
        }).catch(function() {
          showError(t('auth.changePasswordFailed', 'Erreur lors du changement de mot de passe'));
          chgBtn.disabled = false;
          chgBtn.textContent = t('auth.changePasswordBtn', 'Changer');
        });
      });

      if (cancelBtn) {
        cancelBtn.addEventListener('click', function() {
          overlay.remove();
          if (callback) callback(false);
        });
      }

      // Block Escape key for forced modal
      if (!dismissible) {
        overlay.addEventListener('keydown', function(e) {
          if (e.key === 'Escape') e.preventDefault();
        });
      }

      setTimeout(function() { currentInput.focus(); }, 100);
      confirmInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') chgBtn.click(); });
    },

    // ============================================================
    // Initialisation
    // ============================================================

    init: function() {
      var self = this;

      // SSO callback: detect one-time code in URL fragment (#sso-code=XXX)
      // The server redirects here after successful OIDC flow.
      // We exchange the opaque code for JWT+user via POST sso-redeem.
      if (window.location.hash && window.location.hash.indexOf('sso-code=') !== -1) {
        var hashParams = new URLSearchParams(window.location.hash.substring(1));
        var ssoCode = hashParams.get('sso-code');
        // Validate code format: 32 hex chars (matches server-side validation)
        if (ssoCode && /^[0-9a-f]{32}$/.test(ssoCode)) {
          // Clean URL fragment immediately (code is one-time, no retry possible)
          if (window.history && window.history.replaceState) {
            window.history.replaceState(null, '', window.location.pathname + window.location.search);
          }
          // Exchange code for JWT + user
          (window.fetchWithTimeout || fetch)(API_BASE + '?action=sso-redeem', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ code: ssoCode })
          }).then(function(r) {
            return r.json().then(function(d) { return { ok: r.ok, data: d }; })
              .catch(function() { return { ok: false, data: { error: 'Reponse SSO invalide' } }; });
          }).then(function(r) {
            if (r.ok && r.data.token && r.data.user) {
              _token = r.data.token;
              _currentUser = r.data.user;
          _ingestEntityContext(r.data); /* === CSI-ME === */
              sessionStorage.setItem(TOKEN_KEY, _token);
              sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
              self._scheduleTokenRefresh();
              try { localStorage.setItem('smsi_welcome_seen', '1'); } catch(e) {}
              // Validate with /me for server-authoritative user data
              self._fetch('me', { method: 'GET' }).then(function(meR) {
                if (meR.ok && meR.data && meR.data.user) {
                  _currentUser = meR.data.user;
                  _ingestEntityContext(meR.data); /* === CSI-ME === (audit SSO) le /me porte le bloc multiEntity — la réponse sso-redeem ne contient que {token,user} : sans cette ingestion, toute la session SSO tournait feature éteinte (pas de périmètre, pushes 409) */
                  _applyUserLanguage(_currentUser); // FEAT-USER-LANGUAGE
                  sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
                }
                if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
              }).catch(function() {
                // /me failed but we have the token — proceed anyway
                if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
              });
            } else {
              void 0;
              // Show error to user — they need to know SSO login failed
              if (window.showToast2) {
                window.showToast2('Echec de l\'authentification SSO : ' + (r.data.error || 'erreur inconnue'), 'error');
              }
              if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
            }
          }).catch(function() {
            void 0;
            if (window.showToast2) {
              window.showToast2('Echec de l\'authentification SSO. Verifiez votre connexion et reessayez.', 'error');
            }
            if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
          });
          return; // Don't fall through to session restore while redeem is in-flight
        } else if (ssoCode) {
          // Invalid format — clean fragment and ignore
          void 0;
          if (window.history && window.history.replaceState) {
            window.history.replaceState(null, '', window.location.pathname + window.location.search);
          }
        }
      }

      // Restaurer le token depuis sessionStorage
      var savedToken = sessionStorage.getItem(TOKEN_KEY);
      var savedUser = sessionStorage.getItem(USER_KEY);
      if (savedToken && savedUser) {
        _token = savedToken;
        try { _currentUser = JSON.parse(savedUser); } catch(e) { _currentUser = null; }
        // PASS2-003: Validate restored JWT by calling a lightweight endpoint
        if (_token && _currentUser) {
          this._fetch('me', { method: 'GET' }).then(function(r) {
            if (!r.ok) {
              void 0;
              _token = null;
              _currentUser = null;
              sessionStorage.removeItem(TOKEN_KEY);
              sessionStorage.removeItem(USER_KEY);
            } else {
              // Token valide — replace _currentUser with server-authoritative version
              if (r.data && r.data.user) {
                _currentUser = r.data.user;
                _ingestEntityContext(r.data); /* === CSI-ME === */
                _applyUserLanguage(_currentUser); // FEAT-USER-LANGUAGE
                sessionStorage.setItem(USER_KEY, JSON.stringify(_currentUser));
              }
              self._scheduleTokenRefresh();
            }
            if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
          }).catch(function() {
            /* network error — keep token, let sync handle it */
            if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
          });
        } else {
          // Token present but user data corrupted — clear everything
          void 0;
          _token = null;
          sessionStorage.removeItem(TOKEN_KEY);
          sessionStorage.removeItem(USER_KEY);
          if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
        }
      } else {
        // No saved token — auth is immediately ready
        if (_authReadyResolve) { _authReadyResolve(); _authReadyResolve = null; }
      }
    },

    /**
     * BUG C9 fix: wait for auth initialization to complete before using isLoggedIn()
     */
    onAuthReady: function(cb) { _authReadyPromise.then(cb); }
  };

  // Inerte en mode local
  if (window.CSI_LicenseManager && !window.CSI_LicenseManager.isServerMode() && (window.CSI_LicenseManager.isDetected() || window.location.protocol === 'file:')) {
    // Mode local confirmé → exposer un stub inerte
    window.CSI_AuthManager = {
      isLoggedIn: function() { return false; },
      getCurrentUser: function() { return null; },
      getToken: function() { return null; },
      getTabId: function() { return TAB_ID; },
    isTabIdStable: function() { return TAB_ID_STABLE; },
      isAdmin: function() { return false; },
      isViewer: function() { return false; },
      isEditor: function() { return false; },
      getAuthHeader: function() { return ''; },
      mustChangePassword: function() { return false; },
      changePassword: function() { return Promise.resolve({ ok: false }); },
      showLoginModal: function(cb) { if (cb) cb(false); },
      showForcePasswordChangeModal: function(cb) { if (cb) cb(false); },
      showChangePasswordModal: function(cb) { if (cb) cb(false); },
      init: function() {},
      getStatus: function() { return Promise.resolve(null); },
      login: function() { return Promise.resolve({ ok: false }); },
      logout: function() { return Promise.resolve(); },
      validateToken: function() { return Promise.resolve(null); },
      listUsers: function() { return Promise.resolve([]); },
      deleteUser: function() { return Promise.resolve({ ok: false }); },
      resetPassword: function() { return Promise.resolve({ ok: false }); },
      suspendUser: function() { return Promise.resolve({ ok: false }); },
      createUser: function() { return Promise.resolve({ ok: false }); },
      updateUser: function() { return Promise.resolve({ ok: false }); },
      register: function() { return Promise.resolve({ ok: false }); },
      setup: function() { return Promise.resolve({ ok: false }); },
      wasSetup: function() { return false; },
      getSecurityStatus: function() { return Promise.resolve(null); },
      getAuditLog: function() { return Promise.resolve({ entries: [], total: 0 }); },
      updateSecurityConfig: function() { return Promise.resolve({ ok: false }); },
      refreshToken: function() { return Promise.resolve(null); },
      onAuthReady: function(cb) { if (cb) cb(); },
      getSsoConfig: function() { return Promise.resolve(null); },
      saveSsoConfig: function() { return Promise.resolve({ ok: false }); },
      testSsoConnection: function() { return Promise.resolve(null); },
      getSsoInitUrl: function() { return ''; },
      isSsoUser: function() { return false; },
      // === CSI-MCP === stubs inertes obligatoires : la carte MCP n'existe pas en
      // mode local, mais toute methode appelee au boot doit etre un no-op, pas un throw.
      getMcpEndpointUrl: function() { return ''; },
      getMcpConfig: function() { return Promise.resolve({ ok: false, data: null }); },
      saveMcpConfig: function() { return Promise.resolve({ ok: false }); },
      listMcpTokens: function() { return Promise.resolve({ ok: false, data: { tokens: [] } }); },
      createMcpToken: function() { return Promise.resolve({ ok: false }); },
      revokeMcpToken: function() { return Promise.resolve({ ok: false }); },
      testMcpEndpoint: function() { return Promise.resolve({ ok: false, data: null }); }
    };
    void 0;
    return;
  }

  // Mode serveur ou détection pas encore faite → exposer le vrai manager
  AuthManager.init();

  if (window.CSI && window.CSI.register) {
    window.CSI.register('auth-manager', {
      exports: { CSI_AuthManager: AuthManager }
    });
  }

  window.CSI_AuthManager = AuthManager;
  void 0;
})();
