/**
 * Cyber-Assistant - Module Framework Mapper
 * Systeme de mapping transversal entre referentiels de securite.
 * Permet les correspondances bidirectionnelles entre ISO 27002, NIS2, DORA, PCI DSS, etc.
 *
 * Depends on: registry.js, constants.js
 * Data sources: data/frameworks/*.js, data/mappings/*.js
 */
(function() {
  'use strict';

  // ============================================================
  // Private state
  // ============================================================
  var _frameworks = {};     // { id: frameworkDefinition }
  var _mappings = {};       // { 'source:target': mappingData }
  var _indexes = {};        // { 'source:target': { forward: {}, reverse: {} } }
  var _appDataBindings = {};   // { id: { appDataKey, refField, statusField, maturityFields, aliasOf } }
  var _aliases = {};        // { aliasId: targetId } - tracks which IDs are aliases
  var _initialized = false;

  // ============================================================
  // Internal helpers
  // ============================================================

  /**
   * Build bidirectional indexes for a mapping pair
   */
  function _buildIndex(sourceId, targetId) {
    var key = sourceId + ':' + targetId;
    var data = _mappings[key];
    if (!data || !data.mappings) return;

    var forward = {};
    var reverse = {};

    data.mappings.forEach(function(m) {
      // Forward index: source -> [targets]
      if (!forward[m.sourceRef]) forward[m.sourceRef] = [];
      forward[m.sourceRef].push({
        targetRef: m.targetRef,
        relationship: m.relationship,
        coverage: m.coverage,
        notes: m.notes || '',
        isDefault: true
      });

      // Reverse index: target -> [sources]
      if (!reverse[m.targetRef]) reverse[m.targetRef] = [];
      reverse[m.targetRef].push({
        sourceRef: m.sourceRef,
        relationship: m.relationship,
        coverage: m.coverage,
        notes: m.notes || '',
        isDefault: true
      });
    });

    _indexes[key] = { forward: forward, reverse: reverse };
  }

  /**
   * Merge custom mappings from appData into indexes
   */
  function _mergeCustomMappings() {
    if (typeof appData === 'undefined' || !appData.frameworkMapper) return;

    var customs = appData.frameworkMapper.customMappings || [];
    customs.forEach(function(cm) {
      var key = cm.sourceFramework + ':' + cm.targetFramework;
      if (!_indexes[key]) {
        _indexes[key] = { forward: {}, reverse: {} };
      }

      var entry = {
        targetRef: cm.targetRef,
        relationship: cm.relationship,
        coverage: cm.coverage,
        notes: cm.notes || '',
        isDefault: false,
        customId: cm.id
      };

      // Check if this overrides an existing default mapping
      var fwd = _indexes[key].forward[cm.sourceRef];
      if (fwd) {
        var existingIdx = -1;
        for (var i = 0; i < fwd.length; i++) {
          if (fwd[i].targetRef === cm.targetRef && fwd[i].isDefault) {
            existingIdx = i;
            break;
          }
        }
        if (cm.isOverride && existingIdx >= 0) {
          var existing = fwd[existingIdx];
          fwd[existingIdx] = {
            targetRef: cm.targetRef,
            relationship: cm.relationship || existing.relationship,
            coverage: cm.coverage !== undefined ? cm.coverage : existing.coverage,
            notes: cm.notes || existing.notes,
            isDefault: false,
            customId: cm.id
          };
        } else if (existingIdx < 0) {
          fwd.push(entry);
        }
      } else {
        _indexes[key].forward[cm.sourceRef] = [entry];
      }

      // Reverse
      var revEntry = {
        sourceRef: cm.sourceRef,
        relationship: cm.relationship,
        coverage: cm.coverage,
        notes: cm.notes || '',
        isDefault: false,
        customId: cm.id
      };

      var rev = _indexes[key].reverse[cm.targetRef];
      if (rev) {
        var existingRevIdx = -1;
        for (var j = 0; j < rev.length; j++) {
          if (rev[j].sourceRef === cm.sourceRef && rev[j].isDefault) {
            existingRevIdx = j;
            break;
          }
        }
        if (cm.isOverride && existingRevIdx >= 0) {
          var existingRev = rev[existingRevIdx];
          rev[existingRevIdx] = {
            sourceRef: cm.sourceRef,
            relationship: cm.relationship || existingRev.relationship,
            coverage: cm.coverage !== undefined ? cm.coverage : existingRev.coverage,
            notes: cm.notes || existingRev.notes,
            isDefault: false,
            customId: cm.id
          };
        } else if (existingRevIdx < 0) {
          rev.push(revEntry);
        }
      } else {
        _indexes[key].reverse[cm.targetRef] = [revEntry];
      }
    });
  }

  /**
   * Get the appData ref field for a framework's controls
   */
  function _getAppDataControls(frameworkId) {
    if (typeof appData === 'undefined') return [];
    if (frameworkId === 'iso27002') return appData.controls || [];
    if (frameworkId === 'nis2' || frameworkId === 'cyfun2025') return appData.nis2Controls || [];
    // Future frameworks: appData[frameworkId + 'Controls']
    var key = frameworkId + 'Controls';
    return appData[key] || [];
  }

  /**
   * Get the ref field name for matching appData controls to framework refs
   */
  function _getRefField(frameworkId) {
    if (frameworkId === 'iso27002') return 'numero';
    if (frameworkId === 'nis2' || frameworkId === 'cyfun2025') return '_nis2Ref'; // special handling
    return 'ref';
  }

  /**
   * Extract the NIS2/CyFun category code from a control.
   * New CyFun format: uses categoryId field (e.g., "PR.AA")
   * Legacy format: extracts from categorie field (e.g., "Gestion d'actifs (ID.AM)" -> "ID.AM")
   */
  function _extractNis2Ref(control) {
    if (!control) return '';
    // New CyFun format: use categoryId directly
    if (control.categoryId) return control.categoryId;
    // Legacy format: extract from parentheses in categorie field
    if (control.categorie) {
      var match = control.categorie.match(/\(([^)]+)\)/);
      return match ? match[1] : '';
    }
    return '';
  }

  // ============================================================
  // Public API
  // ============================================================
  var FrameworkMapper = {

    // ----------------------------------------------------------
    // Framework Registration & Query
    // ----------------------------------------------------------

    /**
     * Register a framework definition
     */
    registerFramework: function(id, definition) {
      if (!id || !definition || !definition.meta) {
        void 0;
        return false;
      }
      _frameworks[id] = definition;
      return true;
    },

    /**
     * Get a registered framework by ID
     */
    getFramework: function(id) {
      return _frameworks[id] || null;
    },

    /**
     * List all registered frameworks with their enabled status
     */
    listFrameworks: function() {
      var result = [];
      var enabledList = this._getEnabledList();

      Object.keys(_frameworks).forEach(function(id) {
        // Skip alias entries to avoid duplicates (e.g. nis2 -> cyfun2025)
        if (_aliases[id]) return;

        var fw = _frameworks[id];
        result.push({
          id: id,
          name: fw.meta.name,
          shortName: fw.meta.shortName,
          version: fw.meta.version,
          controlCount: fw.meta.controlCount,
          icon: fw.meta.icon,
          color: fw.meta.color,
          enabled: enabledList.indexOf(id) >= 0
        });
      });

      return result;
    },

    /**
     * List only enabled frameworks
     */
    listEnabledFrameworks: function() {
      return this.listFrameworks().filter(function(f) { return f.enabled; });
    },

    /**
     * Check if a framework is enabled
     */
    isEnabled: function(id) {
      return this._getEnabledList().indexOf(id) >= 0;
    },

    /**
     * Enable or disable a framework
     */
    setEnabled: function(id, enabled) {
      if (typeof appData === 'undefined') return;
      if (!appData.frameworkMapper) this._initAppData();

      var list = appData.frameworkMapper.enabledFrameworks;
      var idx = list.indexOf(id);

      if (enabled && idx < 0) {
        list.push(id);
      } else if (!enabled && idx >= 0) {
        list.splice(idx, 1);
      }

      if (typeof saveData === 'function') saveData();
    },

    // ----------------------------------------------------------
    // Mapping Registration
    // ----------------------------------------------------------

    /**
     * Register a mapping table between two frameworks
     */
    registerMapping: function(sourceId, targetId, mappingData) {
      var key = sourceId + ':' + targetId;
      _mappings[key] = mappingData;
      _buildIndex(sourceId, targetId);
      return true;
    },

    // ----------------------------------------------------------
    // Query API - Core
    // ----------------------------------------------------------

    /**
     * Get all mappings FROM a specific control TO another framework
     */
    getMappingsFrom: function(sourceFramework, sourceRef, targetFramework) {
      var key = sourceFramework + ':' + targetFramework;
      var idx = _indexes[key];
      if (!idx) return [];
      return (idx.forward[sourceRef] || []).slice();
    },

    /**
     * Get all mappings TO a specific control FROM another framework
     */
    getMappingsTo: function(targetFramework, targetRef, sourceFramework) {
      var key = sourceFramework + ':' + targetFramework;
      var idx = _indexes[key];
      if (!idx) return [];
      return (idx.reverse[targetRef] || []).slice();
    },

    /**
     * Get all mappings for a control across ALL enabled frameworks
     * Returns { frameworkId: [mappingEntries] }
     */
    getAllMappingsForControl: function(framework, ref) {
      var result = {};
      var self = this;
      var enabledList = this._getEnabledList();

      enabledList.forEach(function(otherId) {
        if (otherId === framework) return;

        // Try forward (this framework -> other)
        var fwdKey = framework + ':' + otherId;
        if (_indexes[fwdKey] && _indexes[fwdKey].forward[ref]) {
          result[otherId] = _indexes[fwdKey].forward[ref].slice();
          return;
        }

        // Try reverse (other -> this framework)
        var revKey = otherId + ':' + framework;
        if (_indexes[revKey] && _indexes[revKey].reverse[ref]) {
          // Convert reverse entries to forward-like format
          result[otherId] = _indexes[revKey].reverse[ref].map(function(entry) {
            return {
              targetRef: entry.sourceRef,
              relationship: self._invertRelationship(entry.relationship),
              coverage: entry.coverage,
              notes: entry.notes,
              isDefault: entry.isDefault
            };
          });
        }
      });

      return result;
    },

    /**
     * Get the full mapping matrix between two frameworks
     */
    getMatrix: function(sourceId, targetId) {
      var key = sourceId + ':' + targetId;
      var idx = _indexes[key];
      if (!idx) {
        // Try reverse
        var revKey = targetId + ':' + sourceId;
        idx = _indexes[revKey];
        if (!idx) return [];

        // Convert reverse to forward
        var self = this;
        var result = [];
        Object.keys(idx.forward).forEach(function(ref) {
          idx.forward[ref].forEach(function(entry) {
            result.push({
              sourceRef: entry.targetRef,
              targetRef: ref,
              relationship: self._invertRelationship(entry.relationship),
              coverage: entry.coverage,
              notes: entry.notes,
              isDefault: entry.isDefault
            });
          });
        });
        return result;
      }

      var result = [];
      Object.keys(idx.forward).forEach(function(ref) {
        idx.forward[ref].forEach(function(entry) {
          result.push({
            sourceRef: ref,
            targetRef: entry.targetRef,
            relationship: entry.relationship,
            coverage: entry.coverage,
            notes: entry.notes,
            isDefault: entry.isDefault
          });
        });
      });
      return result;
    },

    // ----------------------------------------------------------
    // Statistics
    // ----------------------------------------------------------

    /**
     * Get coverage statistics between two frameworks
     */
    getCoverageStats: function(sourceId, targetId) {
      var sourceFw = _frameworks[sourceId];
      var targetFw = _frameworks[targetId];
      if (!sourceFw || !targetFw) return null;

      var matrix = this.getMatrix(sourceId, targetId);
      var mappedSources = {};
      var mappedTargets = {};
      var totalCoverage = 0;

      matrix.forEach(function(m) {
        mappedSources[m.sourceRef] = true;
        mappedTargets[m.targetRef] = true;
        totalCoverage += (m.coverage || 0);
      });

      var mappedSourceCount = Object.keys(mappedSources).length;
      var mappedTargetCount = Object.keys(mappedTargets).length;

      // FIX-MAPPER-STATS-UNDEFINED (audit 2026-06-10) : repli sur 0 pour les référentiels
      // sans controls/requirements ni meta.controlCount (ex. iso27001 : 'clauses' +
      // 'clauseCount'), sinon la barre de stats affiche littéralement « 0/undefined ».
      var actualSourceCount = (sourceFw.controls || sourceFw.requirements || []).length || sourceFw.meta.controlCount || 0;
      // Detect mapping granularity: if mappings target categories (not individual requirements), use category count
      var targetRefs = Object.keys(mappedTargets);
      var targetCategories = targetFw.meta && targetFw.meta.categories ? targetFw.meta.categories : null;
      var targetIsCategories = targetCategories && targetRefs.length > 0 && targetRefs.every(function(ref) {
        return targetCategories.some(function(cat) { return cat.id === ref; });
      });
      var actualTargetCount = targetIsCategories
        ? targetCategories.length
        : (targetFw.controls || targetFw.requirements || []).length || targetFw.meta.controlCount || 0; // FIX-MAPPER-STATS-UNDEFINED (audit 2026-06-10)

      return {
        totalSource: actualSourceCount,
        totalTarget: actualTargetCount,
        totalMappings: matrix.length,
        mappedSources: mappedSourceCount,
        unmappedSources: actualSourceCount - mappedSourceCount,
        mappedTargets: mappedTargetCount,
        unmappedTargets: actualTargetCount - mappedTargetCount,
        avgCoverage: matrix.length > 0 ? Math.round(totalCoverage / matrix.length) : 0
      };
    },

    // ----------------------------------------------------------
    // Status Integration
    // ----------------------------------------------------------

    /**
     * Get the implementation status of a control from appData
     */
    getControlStatus: function(frameworkId, ref) {
      // FIX-MAPPER-STATUS-BINDING (audit 2026-06-10) : utiliser la résolution de binding
      // appData (aliasOf) via getAppDataControls/findControl au lieu du legacy
      // _getAppDataControls codé en dur, sinon le statut reste « - » pour
      // open-security-controls (évaluations dans appData.controls via l'alias iso27002)
      // et recyf2025 (appData.recyfControls, pas appData['recyf2025Controls']).
      var controls = this.getAppDataControls(frameworkId);
      var control = this.findControl(frameworkId, ref);
      var isNis2 = frameworkId === 'nis2' || frameworkId === 'cyfun2025';

      if (!control) return { status: 'unknown', cmm: 'N/A', docMaturity: 0, implMaturity: 0 };

      // CyFun dual scoring: compute average from all controls matching this category
      if (isNis2 && control.categoryId) {
        var catControls = controls.filter(function(cc) { return cc.categoryId === ref; });
        if (catControls.length > 0) {
          var sumDoc = 0, sumImpl = 0, count = 0;
          catControls.forEach(function(cc) {
            if (cc.docMaturity > 0 || cc.implMaturity > 0) {
              sumDoc += cc.docMaturity || 0;
              sumImpl += cc.implMaturity || 0;
              count++;
            }
          });
          var avgDoc = count > 0 ? sumDoc / count : 0;
          var avgImpl = count > 0 ? sumImpl / count : 0;
          var avg = (avgDoc + avgImpl) / 2;
          var inferredStatus = avg >= 3.5 ? 'verifie' : avg >= 2.5 ? 'implemente' : avg >= 1.5 ? 'en_cours' : 'non_implemente';
          return {
            status: inferredStatus,
            cmm: avg > 0 ? avg.toFixed(1) : 'N/A',
            docMaturity: +avgDoc.toFixed(1),
            implMaturity: +avgImpl.toFixed(1)
          };
        }
      }

      var fw = _frameworks[frameworkId];
      return {
        status: control[fw ? fw.meta.statusField : 'status'] || 'non_implemente',
        cmm: control[fw ? (fw.meta.maturityField || (fw.meta.maturityFields && fw.meta.maturityFields[0]) || 'cmm') : 'cmm'] || 'Initial',
        docMaturity: control.docMaturity || 0,
        implMaturity: control.implMaturity || 0
      };
    },

    /**
     * Get inferred status from mapped controls in another framework
     */
    getInferredStatus: function(frameworkId, ref, otherFrameworkId) {
      var mappings = this.getMappingsFrom(frameworkId, ref, otherFrameworkId);

      // Also check reverse
      if (mappings.length === 0) {
        var revMappings = this.getMappingsTo(frameworkId, ref, otherFrameworkId);
        if (revMappings.length > 0) {
          var self = this;
          mappings = revMappings.map(function(m) {
            return { targetRef: m.sourceRef, relationship: self._invertRelationship(m.relationship), coverage: m.coverage };
          });
        }
      }

      if (mappings.length === 0) return { inferredStatus: 'unknown', basedOn: [] };

      var basedOn = [];
      var statusScores = { 'verifie': 4, 'implemente': 3, 'en_cours': 2, 'non_implemente': 1, 'unknown': 0 };
      var totalScore = 0;
      var totalWeight = 0;

      var self = this;
      mappings.forEach(function(m) {
        var targetStatus = self.getControlStatus(otherFrameworkId, m.targetRef);
        var score = statusScores[targetStatus.status] || 0;
        var weight = (m.coverage || 50) / 100;
        totalScore += score * weight;
        totalWeight += weight;

        basedOn.push({
          ref: m.targetRef,
          status: targetStatus.status,
          coverage: m.coverage,
          relationship: m.relationship
        });
      });

      var avgScore = totalWeight > 0 ? totalScore / totalWeight : 0;
      var inferredStatus = 'non_implemente';
      if (avgScore >= 3.5) inferredStatus = 'verifie';
      else if (avgScore >= 2.5) inferredStatus = 'implemente';
      else if (avgScore >= 1.5) inferredStatus = 'en_cours';

      return { inferredStatus: inferredStatus, basedOn: basedOn };
    },

    // ----------------------------------------------------------
    // Custom Mappings
    // ----------------------------------------------------------

    /**
     * Add or update a custom mapping
     */
    setCustomMapping: function(mapping) {
      if (typeof appData === 'undefined') return;
      if (!appData.frameworkMapper) this._initAppData();

      var customs = appData.frameworkMapper.customMappings;
      if (mapping.id) {
        // Update existing
        for (var i = 0; i < customs.length; i++) {
          if (customs[i].id === mapping.id) {
            customs[i] = mapping;
            this._rebuildIndexes();
            if (typeof saveData === 'function') saveData();
            return mapping.id;
          }
        }
      }

      // New custom mapping
      mapping.id = 'CMAP-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5);
      mapping.createdAt = new Date().toISOString();
      customs.push(mapping);

      this._rebuildIndexes();
      if (typeof saveData === 'function') saveData();
      return mapping.id;
    },

    /**
     * Remove a custom mapping
     */
    removeCustomMapping: function(mappingId) {
      if (typeof appData === 'undefined' || !appData.frameworkMapper) return false;

      var customs = appData.frameworkMapper.customMappings;
      for (var i = 0; i < customs.length; i++) {
        if (customs[i].id === mappingId) {
          customs.splice(i, 1);
          this._rebuildIndexes();
          if (typeof saveData === 'function') saveData();
          return true;
        }
      }
      return false;
    },

    // ----------------------------------------------------------
    // Control Info Lookup (from framework data)
    // ----------------------------------------------------------

    /**
     * Get control info from the framework definition (not appData)
     */
    getControlInfo: function(frameworkId, ref) {
      var fw = _frameworks[frameworkId];
      if (!fw) return null;

      var controls = fw.controls || fw.requirements || [];
      for (var i = 0; i < controls.length; i++) {
        if (controls[i].ref === ref) return controls[i];
      }
      return null;
    },

    /**
     * Get all controls for a framework
     */
    getControls: function(frameworkId) {
      var fw = _frameworks[frameworkId];
      return fw ? (fw.controls || fw.requirements || []).slice() : [];
    },

    // ----------------------------------------------------------
    // AppData Bindings
    // ----------------------------------------------------------

    /**
     * Register an appData binding for a framework
     * @param {string} frameworkId - Framework identifier
     * @param {Object} binding - { appDataKey, refField, statusField, maturityFields, aliasOf }
     */
    registerAppDataBinding: function(frameworkId, binding) {
      _appDataBindings[frameworkId] = binding;
    },

    /**
     * Get appData controls for a framework, using registered bindings
     * @param {string} frameworkId - Framework identifier
     * @returns {Array} Controls array from appData
     */
    getAppDataControls: function(frameworkId) {
      if (typeof appData === 'undefined') return [];
      var binding = _appDataBindings[frameworkId];
      if (binding) {
        if (binding.aliasOf && _appDataBindings[binding.aliasOf]) {
          binding = _appDataBindings[binding.aliasOf];
        }
        return appData[binding.appDataKey] || [];
      }
      // Fallback to legacy logic
      return _getAppDataControls(frameworkId);
    },

    /**
     * Set appData controls for a framework, using registered bindings
     * @param {string} frameworkId - Framework identifier
     * @param {Array} controls - Controls array to store
     */
    setAppDataControls: function(frameworkId, controls) {
      if (typeof appData === 'undefined') return;
      var binding = _appDataBindings[frameworkId];
      if (binding) {
        if (binding.aliasOf && _appDataBindings[binding.aliasOf]) {
          binding = _appDataBindings[binding.aliasOf];
        }
        appData[binding.appDataKey] = controls;
        return;
      }
      // Fallback
      if (frameworkId === 'iso27002') { appData.controls = controls; return; }
      if (frameworkId === 'nis2' || frameworkId === 'cyfun2025') { appData.nis2Controls = controls; return; }
      appData[frameworkId + 'Controls'] = controls;
    },

    /**
     * Find a single control in appData by reference
     * Unifies the 3 different matching strategies used across the codebase
     * @param {string} frameworkId - Framework identifier
     * @param {string} ref - Control reference to find
     * @returns {Object|null} The matching control or null
     */
    findControl: function(frameworkId, ref) {
      var controls = this.getAppDataControls(frameworkId);
      var binding = _appDataBindings[frameworkId];
      var refField = binding ? binding.refField : _getRefField(frameworkId);

      for (var i = 0; i < controls.length; i++) {
        var c = controls[i];
        if (c[refField] === ref) return c;
        if (c.ref === ref) return c;
        if ((frameworkId === 'nis2' || frameworkId === 'cyfun2025') && _extractNis2Ref(c) === ref) return c;
      }
      return null;
    },

    /**
     * Get the actual control count from framework data (not just meta.controlCount)
     * @param {string} frameworkId - Framework identifier
     * @returns {number} Actual number of controls
     */
    getActualControlCount: function(frameworkId) {
      var fw = _frameworks[frameworkId];
      if (!fw) return 0;
      return (fw.controls || fw.requirements || []).length || fw.meta.controlCount || 0;
    },

    /**
     * Get maturity averages grouped by category
     * Centralizes the pattern duplicated in updateNis2RadarChart, renderNis2ProgramRadar, recalcNis2Maturity
     * @param {string} frameworkId - Framework identifier
     * @returns {Object} { categoryId: { docAvg, implAvg, combinedAvg, count, total } }
     */
    getCategoryMaturity: function(frameworkId) {
      var controls = this.getAppDataControls(frameworkId);
      var fw = _frameworks[frameworkId];
      if (!fw || !fw.meta || !fw.meta.categories) return {};

      var result = {};
      fw.meta.categories.forEach(function(cat) {
        var catControls = controls.filter(function(c) {
          return (c.categoryId || c.category) === cat.id;
        });
        var docSum = 0, implSum = 0, count = 0;
        catControls.forEach(function(c) {
          var d = c.docMaturity || 0;
          var impl = c.implMaturity || 0;
          if (d > 0 || impl > 0) {
            docSum += d;
            implSum += impl;
            count++;
          }
        });
        result[cat.id] = {
          docAvg: count > 0 ? +(docSum / count).toFixed(2) : 0,
          implAvg: count > 0 ? +(implSum / count).toFixed(2) : 0,
          combinedAvg: count > 0 ? +((docSum + implSum) / (count * 2)).toFixed(2) : 0,
          count: count,
          total: catControls.length
        };
      });
      return result;
    },

    /**
     * Get conformity/implementation statistics for a framework
     * @param {string} frameworkId - Framework identifier
     * @param {Object} [options] - { soaFilter: boolean }
     * @returns {Object} { total, applicable, implemented, rate }
     */
    getConformityStats: function(frameworkId, options) {
      options = options || {};
      var controls = this.getAppDataControls(frameworkId);
      var binding = _appDataBindings[frameworkId];
      var statusField = (binding && binding.statusField) ? binding.statusField : 'status';

      // FIX-MAPPER-SOA-ALIAS (audit 2026-06-10) : résoudre l'alias (open-security-controls
      // → iso27002) avant le test SoA, pour que le filtre de la Déclaration d'applicabilité
      // s'applique aussi en édition Community (mêmes appData.controls via le binding aliasOf).
      var soaFrameworkId = (binding && binding.aliasOf) ? binding.aliasOf : (_aliases[frameworkId] || frameworkId);
      var applicable = controls;
      if (options.soaFilter && soaFrameworkId === 'iso27002' && typeof appData !== 'undefined') {
        var soaMap = {};
        (appData.soa || []).forEach(function(s) { soaMap[s.controlId] = s; });
        applicable = controls.filter(function(c) {
          var entry = soaMap[c.id];
          return !entry || entry.applicable !== false;
        });
      }

      var implemented = applicable.filter(function(c) {
        var s = c[statusField];
        return s === 'implemente' || s === 'verifie';
      }).length;

      return {
        total: controls.length,
        applicable: applicable.length,
        implemented: implemented,
        rate: applicable.length > 0 ? Math.round(implemented / applicable.length * 100) : 0
      };
    },

    /**
     * Get inferred status with conflict detection
     * @param {string} frameworkId - Framework identifier
     * @param {string} ref - Control reference
     * @param {string} otherFrameworkId - Other framework to infer from
     * @returns {Object} { inferredStatus, basedOn, conflicts }
     */
    getInferredStatusWithConflicts: function(frameworkId, ref, otherFrameworkId) {
      var result = this.getInferredStatus(frameworkId, ref, otherFrameworkId);
      if (!result.basedOn || result.basedOn.length < 2) {
        result.conflicts = [];
        return result;
      }

      var statuses = {};
      result.basedOn.forEach(function(b) {
        statuses[b.status] = (statuses[b.status] || 0) + 1;
      });

      var uniqueStatuses = Object.keys(statuses);
      result.conflicts = [];
      if (uniqueStatuses.length > 1) {
        result.conflicts.push({
          type: 'status_mismatch',
          message: 'Mapped controls have different statuses: ' + uniqueStatuses.join(', '),
          details: statuses
        });
      }
      return result;
    },

    // ----------------------------------------------------------
    // Internal helpers
    // ----------------------------------------------------------

    _getEnabledList: function() {
      if (typeof appData !== 'undefined' && appData.frameworkMapper) {
        return appData.frameworkMapper.enabledFrameworks || [];
      }
      if (typeof window !== 'undefined' && window.FRAMEWORK_MAPPER_DEFAULTS) {
        return window.FRAMEWORK_MAPPER_DEFAULTS.enabledFrameworks;
      }
      // Dynamic fallback: use active controls framework ID
      var fws = window._csiFrameworks || {};
      var ctrlId = fws['iso27002'] ? 'iso27002' : (fws['open-security-controls'] ? 'open-security-controls' : 'iso27002');
      return [ctrlId, 'nis2'];
    },

    _invertRelationship: function(rel) {
      if (rel === 'covers') return 'covered_by';
      if (rel === 'covered_by') return 'covers';
      return rel; // equivalent and related are symmetric
    },

    _initAppData: function() {
      if (typeof appData === 'undefined') return;
      var fws = window._csiFrameworks || {};
      var ctrlId = fws['iso27002'] ? 'iso27002' : (fws['open-security-controls'] ? 'open-security-controls' : 'iso27002');
      var defaults = window.FRAMEWORK_MAPPER_DEFAULTS || {
        enabledFrameworks: [ctrlId, 'nis2'],
        customMappings: [],
        userNotes: {}
      };
      appData.frameworkMapper = JSON.parse(JSON.stringify(defaults));
    },

    _rebuildIndexes: function() {
      // Rebuild all indexes from scratch
      Object.keys(_mappings).forEach(function(key) {
        var parts = key.split(':');
        _buildIndex(parts[0], parts[1]);
      });
      _mergeCustomMappings();
    },

    // ----------------------------------------------------------
    // Initialization
    // ----------------------------------------------------------

    init: function() {
      if (_initialized) return;

      var self = this;

      // 1. Load frameworks from global registry
      if (window._csiFrameworks) {
        Object.keys(window._csiFrameworks).forEach(function(id) {
          self.registerFramework(id, window._csiFrameworks[id]);
        });
      }

      // Ensure NIS2 alias is also registered
      if (window._csiFrameworks && window._csiFrameworks['nis2'] && !_frameworks['nis2']) {
        self.registerFramework('nis2', window._csiFrameworks['nis2']);
      }

      // Process deferred framework aliases
      if (window._csiFrameworkAliases) {
        Object.keys(window._csiFrameworkAliases).forEach(function(aliasId) {
          var targetId = window._csiFrameworkAliases[aliasId];
          if (_frameworks[targetId] && !_frameworks[aliasId]) {
            _frameworks[aliasId] = _frameworks[targetId];
          }
          // Track alias relationship so listFrameworks() can skip duplicates
          _aliases[aliasId] = targetId;
        });
      }

      // Also handle meta.aliasOf: if a framework declares aliasOf and the target is loaded,
      // mark it as an alias so listFrameworks() skips the duplicate (e.g. open-security-controls -> iso27002)
      Object.keys(_frameworks).forEach(function(id) {
        var meta = _frameworks[id].meta;
        if (meta && meta.aliasOf && _frameworks[meta.aliasOf] && !_aliases[id]) {
          _aliases[id] = meta.aliasOf;
        }
      });

      // Consume deferred appData bindings
      if (window._csiAppDataBindings) {
        Object.keys(window._csiAppDataBindings).forEach(function(id) {
          self.registerAppDataBinding(id, window._csiAppDataBindings[id]);
        });
      }

      // 2. Load mappings from global registry
      if (window._csiMappings) {
        Object.keys(window._csiMappings).forEach(function(key) {
          var data = window._csiMappings[key];
          if (data.meta && data.meta.source && data.meta.target) {
            self.registerMapping(data.meta.source, data.meta.target, data);
          }
        });
      }

      // 2b. Re-register mappings under alias equivalents
      // e.g. if 'open-security-controls' has aliasOf:'iso27002', register iso27002:cyfun2025 also as open-security-controls:cyfun2025
      var mappingKeys = Object.keys(_mappings);
      Object.keys(_frameworks).forEach(function(fwId) {
        var meta = _frameworks[fwId].meta;
        if (!meta || !meta.aliasOf) return;
        var canonical = meta.aliasOf;
        mappingKeys.forEach(function(mk) {
          var parts = mk.split(':');
          if (parts[0] === canonical && !_mappings[fwId + ':' + parts[1]]) {
            self.registerMapping(fwId, parts[1], _mappings[mk]);
          }
          if (parts[1] === canonical && !_mappings[parts[0] + ':' + fwId]) {
            self.registerMapping(parts[0], fwId, _mappings[mk]);
          }
        });
      });

      // 3. Merge custom mappings after appData is available
      // (will be called again when appData loads in bundle.js)

      _initialized = true;

      var fwCount = Object.keys(_frameworks).length;
      var mapCount = Object.keys(_mappings).length;
      void 0;
    },

    /**
     * Called after appData is loaded to merge custom mappings.
     * === CSI-ME === (audit cohérence 2026-07-03, MAJEUR) : reconstruire les index de ZÉRO
     * (_rebuildIndexes = index par défaut + fusion des customs du appData COURANT) au lieu
     * d'une fusion additive — appData.frameworkMapper est une donnée PAR PARTITION : après
     * une bascule de périmètre, les mappings personnalisés de la partition précédente
     * restaient visibles (fuite inter-partitions) et se dupliquaient à chaque retour.
     */
    onAppDataLoaded: function() {
      if (typeof appData !== 'undefined' && !appData.frameworkMapper) {
        this._initAppData();
      }
      this._rebuildIndexes();
    },

    /**
     * Apply visibility: hide/show nav elements, dashboard widgets
     * and tabs based on enabled frameworks.
     * Called after wizard close, initApp, and settings toggle.
     */
    applyFrameworkVisibility: function() {
      // Réconcilier le référentiel NIS2 actif (modèle mutuellement exclusif : un seul
      // référentiel NIS2 — Belgique/CyFun OU France/ReCyF — actif à la fois) AVANT de calculer
      // la visibilité, pour que la liste activée reflète le choix de l'utilisateur.
      if (typeof window.reconcileNis2Frameworks === 'function') window.reconcileNis2Frameworks();

      var enabledList = this._getEnabledList();

      // 1) Visibilité des catégories et onglets : logique OR. Un élément de navigation est
      //    visible dès qu'AU MOINS UN framework activé (ou obligatoire) le revendique.
      //    Corrige le bug « dernier qui écrit gagne » : CyFun, l'alias NIS2 et ReCyF partagent
      //    la catégorie 'nis2' ; un référentiel désactivé ne doit jamais masquer la catégorie
      //    d'un référentiel activé (sinon plus aucun onglet NIS2 ne s'affiche).
      var catVisible = {};
      var tabVisible = {};
      Object.keys(_frameworks).forEach(function(id) {
        var fw = _frameworks[id];
        var meta = fw.meta;
        if (!meta) return;
        var on = meta.mandatory || enabledList.indexOf(id) >= 0;
        var nav = meta.nav || {};
        (nav.categories || []).forEach(function(catId) {
          if (catVisible[catId] === undefined) catVisible[catId] = false;
          if (on) catVisible[catId] = true;
        });
        (nav.tabs || []).forEach(function(tabId) {
          if (tabVisible[tabId] === undefined) tabVisible[tabId] = false;
          if (on) tabVisible[tabId] = true;
        });
      });
      Object.keys(catVisible).forEach(function(catId) {
        var vis = catVisible[catId];
        var catBtn = document.querySelector('.nav-ribbon__cat[data-category="' + catId + '"]');
        var panel = document.querySelector('.nav-ribbon__panel[data-panel="' + catId + '"]');
        if (catBtn) catBtn.style.display = vis ? '' : 'none';
        if (panel) panel.style.display = vis ? '' : 'none';
      });
      Object.keys(tabVisible).forEach(function(tabId) {
        var tabBtn = document.querySelector('.nav__item[data-tab="' + tabId + '"]');
        if (tabBtn) tabBtn.style.display = tabVisible[tabId] ? '' : 'none';
      });

      // 2) Widgets de dashboard tagués data-framework : visibilité par framework (inchangé).
      Object.keys(_frameworks).forEach(function(id) {
        var fw = _frameworks[id];
        var meta = fw.meta;
        if (!meta || meta.mandatory) return;
        var isEnabled = enabledList.indexOf(id) >= 0;
        document.querySelectorAll('[data-framework="' + id + '"]').forEach(function(el) {
          el.style.display = isEnabled ? '' : 'none';
        });
      });

      // framework-mapping tab: visible only if 2+ frameworks active
      var mappingTab = document.querySelector('.nav__item[data-tab="framework-mapping"]');
      if (mappingTab) {
        mappingTab.style.display = enabledList.length >= 2 ? '' : 'none';
      }

      // NIS2 référentiel actif (Belgique/CyFun vs France/ReCyF) : affine la visibilité
      // des onglets NIS2 après l'application de la visibilité par framework.
      if (typeof window.applyNis2Referential === 'function') window.applyNis2Referential(false);

      // If active tab is now hidden, go back to dashboard
      var activeTab = document.querySelector('.nav__item--active');
      if (activeTab && activeTab.style.display === 'none') {
        var dashBtn = document.querySelector('.nav__item[data-tab="dashboard"]');
        if (dashBtn) dashBtn.click();
      }

      // If active category is now hidden, go back to gouvernance
      var activeCat = document.querySelector('.nav-ribbon__cat--selected');
      if (activeCat && activeCat.style.display === 'none') {
        var gouvCat = document.querySelector('.nav-ribbon__cat[data-category="gouvernance"]');
        if (gouvCat) gouvCat.click();
      }
    },

    /**
     * Check if initialized
     */
    isInitialized: function() {
      return _initialized;
    }
  };

  // ============================================================
  // CSI Registration
  // ============================================================
  if (window.CSI && window.CSI.register) {
    window.CSI.register('framework-mapper', {
      exports: { CSI_FrameworkMapper: FrameworkMapper },
      init: function() { FrameworkMapper.init(); }
    });
  }

  window.CSI_FrameworkMapper = FrameworkMapper;

  void 0;
})();
