/**
 * Cyber-Assistant - Local Trial Manager
 * Gere le trial 14 jours et la licence en mode file:// pour l'edition Corporate Local.
 * Tout est client-side — pas de serveur implique.
 * Anti-tampering via fingerprint HMAC et ancre buildDate (edition.js).
 */
(function() {
  'use strict';

  /* ------------------------------------------------------------------ */
  /*  Constantes                                                         */
  /* ------------------------------------------------------------------ */

  var TRIAL_DAYS = 14;
  // FIX-TRIAL-ABERRANT (revue audit 2026-06-11) : au-delà de ce seuil, un écoulement
  // « constaté » n'est plus crédible comme expiration d'un essai de 14 j — c'est presque
  // sûrement une horloge système faussée (année mal réglée, pile CMOS, VM désynchronisée).
  // On retourne alors « expiré » pour la session courante MAIS sans graver l'irréversibilité
  // (flag + ancre monotone), afin qu'une correction d'horloge restaure l'essai. Le recul
  // frauduleux après une vraie expiration (≤ ce seuil) reste bloqué.
  var TRIAL_ABERRANT_DAYS = 180;

  var KEYS = {
    TRIAL_START:  'smsi_corporate_trial_start',
    TRIAL_FP:     'smsi_corporate_trial_fp',
    LICENSE_KEY:  'smsi_corporate_license_key',
    // FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : ancre de progression monotone
    // (date max deja observee) + flag d'expiration persistant.
    TRIAL_LAST_SEEN: 'smsi_corporate_trial_seen',
    TRIAL_EXPIRED:   'smsi_corporate_trial_expired'
  };

  /* ------------------------------------------------------------------ */
  /*  Etat interne                                                       */
  /* ------------------------------------------------------------------ */

  var _state = null;
  var _detected = false;

  /* ------------------------------------------------------------------ */
  /*  Utilitaires                                                        */
  /* ------------------------------------------------------------------ */

  /**
   * Retourne la date de build comme borne basse (ne jamais demarrer avant).
   * Lit depuis _csiEdition.buildDate (edition.js).
   * Format attendu: YYYY-MM-DD
   */
  function getBuildDate() {
    var ed = window._csiEdition;
    var raw = ed && ed.buildDate ? ed.buildDate : null;
    if (!raw) return null;
    var d = new Date(raw + 'T00:00:00');
    return isNaN(d.getTime()) ? null : d;
  }

  /**
   * Date du jour tronquee a minuit (UTC-safe via ISO split).
   */
  function today() {
    return new Date(new Date().toISOString().split('T')[0] + 'T00:00:00');
  }

  /**
   * Nombre de jours ecoules entre deux dates (tronquees).
   */
  function daysBetween(a, b) {
    var ms = b.getTime() - a.getTime();
    return Math.floor(ms / 86400000);
  }

  /**
   * FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : serialise une date en
   * YYYY-MM-DD LOCAL (pas toISOString, qui convertit en UTC et fait perdre un
   * jour au round-trip sur les fuseaux en avance sur UTC — l'ancre lastSeen
   * doit etre stable store/load pour rester monotone).
   */
  function toLocalIsoDate(d) {
    var m = ('0' + (d.getMonth() + 1)).slice(-2);
    var day = ('0' + d.getDate()).slice(-2);
    return d.getFullYear() + '-' + m + '-' + day;
  }

  /**
   * Hash SHA-256 via crypto.subtle (async).
   * Fallback : hash maison simple (djb2-like) si crypto.subtle indisponible.
   */
  function sha256(str) {
    if (window.crypto && window.crypto.subtle && window.crypto.subtle.digest) {
      var buf = new TextEncoder().encode(str);
      return window.crypto.subtle.digest('SHA-256', buf).then(function(hash) {
        var bytes = new Uint8Array(hash);
        var hex = '';
        for (var i = 0; i < bytes.length; i++) {
          hex += ('0' + bytes[i].toString(16)).slice(-2);
        }
        return hex;
      });
    }
    // Fallback : hash djb2 etendu
    var h = 5381;
    for (var i = 0; i < str.length; i++) {
      h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
    }
    return Promise.resolve('djb2_' + h.toString(16));
  }

  /**
   * Calcule le fingerprint du trial pour anti-tampering.
   */
  function computeFingerprint(startedAt) {
    var raw = startedAt + '|' + window.navigator.userAgent.length + '|' + screen.width;
    return sha256(raw);
  }

  /**
   * Decode une chaine base64url en string UTF-8.
   */
  function base64urlDecode(str) {
    // Remplacer les caracteres base64url par base64 standard
    var b64 = str.replace(/-/g, '+').replace(/_/g, '/');
    // Ajouter le padding si necessaire
    while (b64.length % 4 !== 0) {
      b64 += '=';
    }
    return atob(b64);
  }

  /* ------------------------------------------------------------------ */
  /*  Logique trial                                                      */
  /* ------------------------------------------------------------------ */

  /**
   * Initialise ou lit l'etat du trial depuis localStorage.
   * Retourne une Promise<Object> avec les infos trial.
   */
  function detectTrial() {
    var startRaw = null;
    try {
      startRaw = localStorage.getItem(KEYS.TRIAL_START);
    } catch (e) {
      void 0;
    }

    var buildDate = getBuildDate();
    var now = today();

    // FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : lire l'ancre monotone et le
    // flag d'expiration persistant avant tout calcul.
    var lastSeenRaw = null;
    var expiredFlag = null;
    try {
      lastSeenRaw = localStorage.getItem(KEYS.TRIAL_LAST_SEEN);
      expiredFlag = localStorage.getItem(KEYS.TRIAL_EXPIRED);
    } catch (e) { /* ignore */ }

    // FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : une expiration deja constatee
    // est definitive — reculer l'horloge systeme ne reactive plus le trial.
    // L'activation d'une licence legitime n'est pas affectee : detect() verifie la
    // licence AVANT le trial, et activate() ne passe pas par detectTrial().
    if (expiredFlag === '1') {
      return Promise.resolve({
        licensed: false,
        trial: true,
        trialDaysLeft: 0,
        trialExpired: true
      });
    }

    // --- Pas encore de trial → initialiser ---
    if (!startRaw) {
      // La date de debut ne peut pas etre avant la date de build
      var startDate = now;
      if (buildDate && buildDate.getTime() > now.getTime()) {
        startDate = buildDate;
      }
      return initTrial(startDate);
    }

    // --- Trial existant → verifier le fingerprint ---
    var storedFp = null;
    try {
      storedFp = localStorage.getItem(KEYS.TRIAL_FP);
    } catch (e) { /* ignore */ }

    return computeFingerprint(startRaw).then(function(expectedFp) {
      if (storedFp !== expectedFp) {
        // Fingerprint invalide → trial tampered, on reset
        void 0;
        var resetDate = now;
        if (buildDate && buildDate.getTime() > now.getTime()) {
          resetDate = buildDate;
        }
        // Ne pas remonter avant la date de build
        if (buildDate && resetDate.getTime() < buildDate.getTime()) {
          resetDate = buildDate;
        }
        return initTrial(resetDate);
      }

      // Fingerprint OK → calculer les jours restants
      var startDate = new Date(startRaw + 'T00:00:00');
      if (isNaN(startDate.getTime())) {
        void 0;
        return initTrial(buildDate || now);
      }

      // Anti-reset : si la date stockee est avant la date de build → utiliser build date
      if (buildDate && startDate.getTime() < buildDate.getTime()) {
        void 0;
        return initTrial(buildDate);
      }

      // FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : ancre de progression monotone.
      // On retient la date la plus avancee deja observee (lastSeen) ; si l'horloge
      // systeme a ete reculee, le calcul repart de cette ancre — les jours restants
      // ne peuvent donc jamais remonter en reculant l'horloge.
      var effectiveNow = now;
      if (lastSeenRaw) {
        var lastSeen = new Date(lastSeenRaw + 'T00:00:00');
        if (!isNaN(lastSeen.getTime()) && lastSeen.getTime() > effectiveNow.getTime()) {
          effectiveNow = lastSeen;
        }
      }

      var elapsed = daysBetween(startDate, effectiveNow);

      // FIX-TRIAL-ABERRANT (revue audit 2026-06-11) : horloge manifestement faussée
      // (écoulement > seuil aberrant pour un essai de 14 j) → expiré pour la session,
      // mais on NE grave NI l'ancre monotone NI le flag définitif : une correction
      // d'horloge restaure l'essai. Cas accidentel courant : année mal réglée (~+1 an).
      if (elapsed > TRIAL_ABERRANT_DAYS) {
        return {
          licensed: false,
          trial: true,
          trialDaysLeft: 0,
          trialExpired: true
        };
      }

      // Écoulement crédible : avancer l'ancre monotone (anti-recul) et, à l'expiration,
      // mémoriser le flag persistant (FIX-TRIAL-CLOCK-ROLLBACK, audit 2026-06-10).
      try {
        localStorage.setItem(KEYS.TRIAL_LAST_SEEN, toLocalIsoDate(effectiveNow));
      } catch (e) { /* ignore */ }

      var daysLeft = Math.max(0, TRIAL_DAYS - elapsed);

      if (daysLeft <= 0) {
        try { localStorage.setItem(KEYS.TRIAL_EXPIRED, '1'); } catch (e) { /* ignore */ }
      }

      return {
        licensed: false,
        trial: true,
        trialDaysLeft: daysLeft,
        trialExpired: daysLeft <= 0
      };
    });
  }

  /**
   * Initialise le trial a la date donnee et stocke le fingerprint.
   */
  function initTrial(startDate) {
    var isoDate = startDate.toISOString().split('T')[0];
    var now = today();
    var elapsed = daysBetween(startDate, now);
    var daysLeft = Math.max(0, TRIAL_DAYS - elapsed);

    return computeFingerprint(isoDate).then(function(fp) {
      try {
        localStorage.setItem(KEYS.TRIAL_START, isoDate);
        localStorage.setItem(KEYS.TRIAL_FP, fp);
      } catch (e) {
        void 0;
      }

      // FIX-TRIAL-CLOCK-ROLLBACK (audit 2026-06-10) : tenir l'ancre monotone a jour
      // des l'initialisation (sans jamais la faire reculer) et persister
      // l'expiration si elle est constatee des le depart.
      try {
        var prevSeenRaw = localStorage.getItem(KEYS.TRIAL_LAST_SEEN);
        var prevSeen = prevSeenRaw ? new Date(prevSeenRaw + 'T00:00:00') : null;
        if (!prevSeen || isNaN(prevSeen.getTime()) || prevSeen.getTime() < now.getTime()) {
          localStorage.setItem(KEYS.TRIAL_LAST_SEEN, toLocalIsoDate(now));
        }
        if (daysLeft <= 0) {
          localStorage.setItem(KEYS.TRIAL_EXPIRED, '1');
        }
      } catch (e) { /* ignore */ }

      void 0;

      return {
        licensed: false,
        trial: true,
        trialDaysLeft: daysLeft,
        trialExpired: daysLeft <= 0
      };
    });
  }

  /* ------------------------------------------------------------------ */
  /*  Logique licence                                                    */
  /* ------------------------------------------------------------------ */

  /**
   * Verifie une licence stockee en localStorage.
   * Retourne une Promise<Object|null> — null si la licence est invalide/absente.
   */
  function checkStoredLicense() {
    var keyRaw = null;
    try {
      keyRaw = localStorage.getItem(KEYS.LICENSE_KEY);
    } catch (e) { /* ignore */ }

    if (!keyRaw) return Promise.resolve(null);

    return validateLicenseKey(keyRaw).then(function(result) {
      if (result && result.ok) {
        return {
          licensed: true,
          trial: false,
          organization: result.organization || null,
          expiresAt: result.expiresAt || null,
          licenseExpired: false
        };
      }
      // Licence invalide → la supprimer
      void 0;
      try {
        localStorage.removeItem(KEYS.LICENSE_KEY);
      } catch (e) { /* ignore */ }
      return null;
    }).catch(function(err) {
      void 0;
      return null;
    });
  }

  /**
   * Valide une cle de licence CSI2-...
   * Retourne Promise<Object> avec { ok, organization, expiresAt } ou throw.
   */
  function validateLicenseKey(licenseKey) {
    // 1. Verifier le prefixe
    if (!licenseKey || licenseKey.indexOf('CSI2-') !== 0) {
      return Promise.reject(new Error('Format de clé invalide (préfixe CSI2- attendu)'));
    }

    // 2. Decoder le base64url apres le prefixe
    var encoded = licenseKey.substring(5);
    var decoded;
    try {
      decoded = base64urlDecode(encoded);
    } catch (e) {
      return Promise.reject(new Error('Clé de licence illisible (base64 invalide)'));
    }

    // 3. Parser le JSON
    var payload;
    try {
      payload = JSON.parse(decoded);
    } catch (e) {
      return Promise.reject(new Error('Clé de licence corrompue (JSON invalide)'));
    }

    // 4. Verifier la structure
    if (!payload.license || !payload.signature) {
      return Promise.reject(new Error('Structure de licence invalide'));
    }

    var license = payload.license;

    // 5. Verifier que les features contiennent 'local-iso-access' et PAS 'server-sync'
    var features = license.features || [];
    if (features.indexOf('local-iso-access') === -1) {
      return Promise.reject(new Error('Cette licence n\'est pas compatible avec l\'édition Corporate Local'));
    }
    if (features.indexOf('server-sync') !== -1) {
      return Promise.reject(new Error('Cette licence est destinée à l\'édition serveur, pas Corporate Local'));
    }

    // 6. Valider la signature ECDSA via l'API exposee par bundle.js
    var validateFn = window.validateLicenseSignature;
    if (typeof validateFn !== 'function') {
      return Promise.reject(new Error('Fonction de validation de signature non disponible'));
    }

    return Promise.resolve().then(function() {
      // validateLicenseSignature attend 3 arguments (licenseData, signature, meta),
      // pas le fichier de licence complet. Passer payload tel quel mettait
      // licenseData = payload (type/organization undefined), signature = undefined
      // et meta = undefined → la garde meta rejetait TOUTE activation.
      return validateFn(payload.license, payload.signature, payload.meta);
    }).then(function(valid) {
      if (!valid) {
        throw new Error('Signature de licence invalide');
      }

      // 7. Verifier l'expiration
      var isExpiredFn = window.isLicenseExpired;
      if (typeof isExpiredFn === 'function' && isExpiredFn(license)) {
        throw new Error('La licence a expiré');
      }

      return {
        ok: true,
        organization: license.organization || license.org || null,
        expiresAt: license.expiresAt || null
      };
    });
  }

  /* ------------------------------------------------------------------ */
  /*  Module public                                                      */
  /* ------------------------------------------------------------------ */

  var LocalTrialManager = {

    /**
     * @returns {boolean} true si l'edition est corporate-local
     */
    isCorporateLocal: function() {
      var ed = window._csiEdition;
      return ed && ed.id === 'corporate-local';
    },

    /**
     * Detecte l'etat trial/licence pour l'edition Corporate Local.
     * @param {boolean} [forceRefresh] - forcer la re-detection
     * @returns {Promise<Object>} etat trial/licence
     */
    detect: function(forceRefresh) {
      // Deja detecte → retourner le cache
      if (_detected && !forceRefresh) return Promise.resolve(_state);

      // Pas corporate-local → mode libre
      if (!LocalTrialManager.isCorporateLocal()) {
        _state = { licensed: true, trial: false };
        _detected = true;
        void 0;
        return Promise.resolve(_state);
      }

      // 1. Verifier la licence stockee
      return checkStoredLicense().then(function(licenseState) {
        if (licenseState) {
          _state = licenseState;
          _detected = true;
          void 0;
          return _state;
        }

        // 2. Pas de licence valide → verifier le trial
        return detectTrial().then(function(trialState) {
          _state = trialState;
          _detected = true;
          if (_state.trialExpired) {
            void 0;
          } else {
            void 0;
          }
          return _state;
        });
      });
    },

    /**
     * Active une licence via cle CSI2-...
     * @param {string} licenseKey - cle de licence a activer
     * @returns {Promise<Object>} resultat de l'activation
     */
    activate: function(licenseKey) {
      return validateLicenseKey(licenseKey).then(function(result) {
        // Stocker la cle dans localStorage
        try {
          localStorage.setItem(KEYS.LICENSE_KEY, licenseKey);
        } catch (e) {
          void 0;
        }

        // Mettre a jour l'etat interne
        _state = {
          licensed: true,
          trial: false,
          organization: result.organization || null,
          expiresAt: result.expiresAt || null,
          licenseExpired: false
        };
        _detected = true;

        void 0;

        return { ok: true, organization: result.organization || null };
      });
    },

    /**
     * @returns {Object|null} etat trial/licence complet
     */
    getState: function() {
      return _state;
    },

    /**
     * @returns {boolean} true si la detection a deja ete effectuee
     */
    isDetected: function() {
      return _detected;
    },

    /**
     * @returns {boolean} true si le trial est actif et qu'il reste des jours
     */
    isTrialActive: function() {
      return _state !== null && _state.trial === true && _state.trialDaysLeft > 0;
    },

    /**
     * @returns {boolean} true si le trial est expire
     */
    isTrialExpired: function() {
      return _state !== null && _state.trial === true &&
        (_state.trialExpired === true || _state.trialDaysLeft <= 0);
    },

    /**
     * @returns {boolean} true si une licence valide est active
     */
    isLicensed: function() {
      return _state !== null && _state.licensed === true;
    },

    /**
     * @returns {boolean} true si la licence est expiree
     */
    isLicenseExpired: function() {
      return _state !== null && _state.licenseExpired === true;
    },

    /**
     * @returns {number} jours restants du trial (0 si aucun)
     */
    getDaysLeft: function() {
      return (_state && _state.trialDaysLeft) || 0;
    },

    /**
     * @returns {string|null} nom de l'organisation ou null
     */
    getOrganization: function() {
      return (_state && _state.organization) || null;
    },

    /**
     * @returns {boolean} true si l'utilisateur doit entrer une cle de licence
     */
    needsLicense: function() {
      return LocalTrialManager.isTrialExpired() || LocalTrialManager.isLicenseExpired();
    }
  };

  /* ------------------------------------------------------------------ */
  /*  Enregistrement                                                     */
  /* ------------------------------------------------------------------ */

  if (window.CSI && window.CSI.register) {
    window.CSI.register('local-trial-manager', {
      exports: { CSI_LocalTrialManager: LocalTrialManager }
    });
  }

  window.CSI_LocalTrialManager = LocalTrialManager;

  void 0;
})();
