/**
 * OWA Qualitative Fusion - Bag Algebras for Qualitative Scales
 * 
 * Implements Ordered Weighted Averaging (OWA) operations for qualitative scales,
 * similar to the existing OWAFusion but adapted for qualitative capacities and scales.
 * 
 * This provides sophisticated aggregation methods for qualitative values that go beyond
 * simple max/min operations, enabling nuanced evidential reasoning in qualitative settings.
 */

import { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from './QualitativeScale.js';
import { QualitativeCapacity } from './QualitativeCapacity.js';

export class OWAQualitativeFusion {
  
  /**
   * Fuse qualitative values with metadata using Qualitative OWA
   * 
   * This implements a novel qualitative weighted maximum operator where weights act as
   * "gates" that must pass a threshold to allow their corresponding values to be considered.
   * This is distinct from the standard Sugeno integral but provides a practical way to
   * introduce weight influence in purely ordinal contexts.
   * 
   * @param {Array<number>} values - Array of qualitative values from the scale
   * @param {Array} metas - Metadata for each value
   * @param {Array<number>} weights - OWA weights (optional)
   * @param {string} mode - Aggregation mode
   * @param {QualitativeScale} scale - The qualitative scale to use
   * @param {number} activationThreshold - Threshold for weight activation (default 0.5)
   * @returns {{value: number, meta: any}} Fused result with metadata
   */
  static fuseWithMeta(values, metas, weights, mode = 'max', scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    if (!values.length) return { value: scale.bottom, meta: null };
    
    // Validate all values are in the scale
    for (const value of values) {
      if (!scale.contains(value)) {
        throw new Error(`Value ${value} not found in scale ${scale.name}`);
      }
    }
    
    const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
    
    // Create pairs of values and metadata for sorting
    const zipped = values.map((v, i) => ({
      v,
      meta: metas[i],
      idx: i,
      priority: metas[i]?.rule?.priority ?? 0,
      ruleType: typeOrder[metas[i]?.ruleType] ?? 0
    }));
    
    // Sort by value (descending), then by priority, then by rule type
    // This is the core of OWA - we always sort first
    zipped.sort((a, b) => {
      const valueComparison = scale.compare(b.v, a.v);
      if (valueComparison !== 0) return valueComparison;
      if (b.priority !== a.priority) return b.priority - a.priority;
      return b.ruleType - a.ruleType;
    });
    
    const sortedValues = zipped.map(z => z.v);
    const sortedMetas = zipped.map(z => z.meta);
    
    // Determine OWA weights based on mode or explicit weights
    let owaWeights;
    
    if (weights && weights.length === values.length) {
      // Use explicit weights as provided
      owaWeights = weights;
    } else {
      // Generate weights based on mode using unified method
      owaWeights = this.generateOWAWeights(values.length, mode, null, true, scale);
    }
    
    // Apply Qualitative OWA: weighted maximum with configurable activation threshold
    // This is a novel operator where weights act as "gates" that must pass a threshold
    // to allow their corresponding values to be considered in the final max operation
    let resultValue = scale.bottom;
    let selectedIdx = 0;
    let maxWeightedValue = scale.bottom;
    
    // Special handling for certain modes
    if (mode.toLowerCase() === 'sum') {
      // For sum, we take the maximum value (all evidence contributes to the strongest)
      resultValue = sortedValues[0]; // Already sorted in descending order
      selectedIdx = 0;
      maxWeightedValue = resultValue;
    } else if (mode.toLowerCase() === 'avg' || mode.toLowerCase() === 'average' || mode.toLowerCase() === 'mean') {
      // For average, we take the median value (middle of the sorted values)
      const medianIndex = Math.floor(sortedValues.length / 2);
      resultValue = sortedValues[medianIndex];
      selectedIdx = medianIndex;
      maxWeightedValue = resultValue;
    } else {
      // Standard OWA logic for other modes
      for (let i = 0; i < sortedValues.length; i++) {
        // Convert qualitative weight to numeric for threshold comparison
        const numericWeight = this._qualitativeWeightToNumeric(owaWeights[i], scale);
        
        // If weight passes activation threshold, use the value; otherwise use bottom
        const effectiveValue = numericWeight > activationThreshold ? sortedValues[i] : scale.bottom;
        
        if (scale.compare(effectiveValue, resultValue) > 0) {
          resultValue = effectiveValue;
          selectedIdx = i;
          maxWeightedValue = effectiveValue;
        }
      }
    }
    
    return { value: resultValue, meta: sortedMetas[selectedIdx] };
  }
  
  /**
   * Generate OWA weights for different aggregation strategies
   * Adapted for qualitative scales where arithmetic operations are limited
   * 
   * @param {number} length - Number of values to aggregate
   * @param {string} mode - Aggregation mode
   * @param {Array<number>} customWeights - Custom weights for specific modes
   * @param {boolean} normalize - Whether to normalize weights
   * @param {QualitativeScale} scale - The qualitative scale
   * @returns {Array<number>} OWA weights
   */
  static generateOWAWeights(length, mode, customWeights = null, normalize = true, scale = DEFAULT_QUALITATIVE_SCALE) {
    if (!mode || length <= 0) {
      return [scale.top, ...Array(Math.max(0, length - 1)).fill(scale.bottom)]; // Default: max
    }
    
    const weights = new Array(length).fill(scale.bottom);
    
    switch (mode.toLowerCase()) {
      case 'max':
        // MAX: All weight on the highest value
        weights[0] = scale.top;
        break;
        
      case 'min':
        // MIN: All weight on the lowest value (last in sorted order)
        weights[length - 1] = scale.top;
        break;
        
      case 'sum':
        // SUM: In qualitative bag algebra, sum means "all values contribute fully"
        // We use a special approach where all weights are set to top, but the fusion
        // logic will handle this differently to ensure all values contribute
        weights.fill(scale.top); // All elements get full weight
        break;
        
      case 'avg':
      case 'average':
      case 'mean':
        // AVERAGE: Equal weights
        const avgWeight = this._getMiddleValue(scale);
        weights.fill(avgWeight);
        break;
        
      case 'majority':
        // MAJORITY: Weight on the median position(s) or top 60% for larger sets
        if (length <= 3) {
          // For small sets, use median logic
          if (length % 2 === 1) {
            // Odd length: weight on middle element
            weights[Math.floor(length / 2)] = scale.top;
          } else {
            // Even length: equal weight on two middle elements
            const mid1 = length / 2 - 1;
            const mid2 = length / 2;
            weights[mid1] = this._getMiddleValue(scale);
            weights[mid2] = this._getMiddleValue(scale);
          }
        } else {
          // For larger sets, weight toward the majority (top 60% of values)
          const majorityCount = Math.max(1, Math.ceil(length * 0.6));
          const majorityWeight = this._getMiddleValue(scale);
          for (let i = 0; i < majorityCount; i++) {
            weights[i] = majorityWeight;
          }
        }
        break;
        
      case 'median':
        // MEDIAN: Focus on the middle value(s)
        if (length === 1) {
          weights[0] = scale.top;
        } else if (length === 2) {
          weights[0] = this._getMiddleValue(scale);
          weights[1] = this._getMiddleValue(scale);
        } else if (length % 2 === 1) {
          // Odd length: single median
          const medianIndex = Math.floor(length / 2);
          weights[medianIndex] = scale.top;
        } else {
          // Even length: equal weight on two middle values
          const mid1 = Math.floor(length / 2) - 1;
          const mid2 = Math.floor(length / 2);
          weights[mid1] = this._getMiddleValue(scale);
          weights[mid2] = this._getMiddleValue(scale);
        }
        break;
        
      case 'optimistic':
        // OPTIMISTIC: More weight on higher values (qualitative decay)
        for (let i = 0; i < length; i++) {
          weights[i] = this._getOptimisticWeight(i, scale);
        }
        break;
        
      case 'pessimistic':
        // PESSIMISTIC: More weight on lower values
        for (let i = 0; i < length; i++) {
          weights[length - 1 - i] = this._getOptimisticWeight(i, scale);
        }
        break;
        
      case 'top2':
        // TOP2: Equal weight on top 2 values
        if (length >= 2) {
          weights[0] = this._getMiddleValue(scale);
          weights[1] = this._getMiddleValue(scale);
        } else if (length === 1) {
          weights[0] = scale.top;
        }
        break;
        
      case 'top3':
        // TOP3: Equal weight on top 3 values
        const top3Count = Math.min(3, length);
        const top3Weight = this._getMiddleValue(scale);
        for (let i = 0; i < top3Count; i++) {
          weights[i] = top3Weight;
        }
        break;
        
      case 'priority':
        // PRIORITY: Use normalized rule priorities as weights
        if (customWeights && Array.isArray(customWeights) && customWeights.length === length) {
          const maxPriority = Math.max(...customWeights, 1);
          for (let i = 0; i < length; i++) {
            const normalizedPriority = (customWeights[i] || 0) / maxPriority;
            weights[i] = this._priorityToQualitativeWeight(normalizedPriority, scale);
          }
        } else {
          // Fallback to max if no priorities provided
          weights[0] = scale.top;
        }
        break;
        
      case 'owa':
      case 'custom':
        // CUSTOM: Custom OWA weights provided by user
        if (customWeights && Array.isArray(customWeights)) {
          if (customWeights.length === length) {
            return customWeights.map(w => this._numericToQualitativeWeight(w, scale));
          } else if (customWeights.length > 0) {
            // Extend or truncate to match length
            const normalized = [...customWeights];
            while (normalized.length < length) normalized.push(0);
            if (normalized.length > length) normalized.splice(length);
            
            return normalized.map(w => this._numericToQualitativeWeight(w, scale));
          }
        }
        // Fallback to max if no valid custom weights
        weights[0] = scale.top;
        break;
        
      default:
        console.warn(`Unknown aggregator '${mode}', defaulting to 'max'`);
        weights[0] = scale.top;
        break;
    }
    
    return weights;
  }
  
  /**
   * Get a middle value from the scale (for equal weighting)
   */
  static _getMiddleValue(scale) {
    const middleIndex = Math.floor(scale.size / 2);
    return scale.at(middleIndex);
  }
  
  /**
   * Get optimistic weight based on position (higher positions get higher weights)
   */
  static _getOptimisticWeight(position, scale) {
    // Map position to scale values with exponential-like decay
    const totalPositions = scale.size;
    const positionRatio = position / Math.max(1, totalPositions - 1);
    
    // Map to scale index with bias toward higher values
    const scaleIndex = Math.floor(positionRatio * (scale.size - 1));
    return scale.at(scaleIndex);
  }
  
  /**
   * Convert numeric priority to qualitative weight
   */
  static _priorityToQualitativeWeight(priority, scale) {
    // Map priority (0-1) to scale values
    const scaleIndex = Math.floor(priority * (scale.size - 1));
    return scale.at(scaleIndex);
  }
  
  /**
   * Convert numeric weight to qualitative weight
   */
  static _numericToQualitativeWeight(weight, scale) {
    // Clamp weight to [0, 1] and map to scale
    const clampedWeight = Math.max(0, Math.min(1, weight));
    const scaleIndex = Math.floor(clampedWeight * (scale.size - 1));
    return scale.at(scaleIndex);
  }
  
  /**
   * Convert qualitative weight to numeric value for threshold comparison
   * Maps qualitative scale values to [0, 1] range
   */
  static _qualitativeWeightToNumeric(qualitativeWeight, scale) {
    const index = scale.indexOf(qualitativeWeight);
    if (index === -1) return 0;
    return index / (scale.size - 1);
  }
  
  /**
   * Convenience methods for common OWA operations
   */
  static max(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'max', scale, activationThreshold);
  }
  
  static min(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'min', scale, activationThreshold);
  }
  
  static majority(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'majority', scale, activationThreshold);
  }
  
  static median(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'median', scale, activationThreshold);
  }
  
  static optimistic(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'optimistic', scale, activationThreshold);
  }
  
  static pessimistic(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'pessimistic', scale, activationThreshold);
  }
  
  static top2(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'top2', scale, activationThreshold);
  }
  
  static top3(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, null, 'top3', scale, activationThreshold);
  }
  
  static priority(values, metas, priorities, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'priority', priorities, true, scale), 'priority', scale, activationThreshold);
  }
  
  static custom(values, metas, customWeights, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
    return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'custom', customWeights, true, scale), 'custom', scale, activationThreshold);
  }
  
  /**
   * Pointwise OWA Fusion of Qualitative Capacities
   * 
   * ⚠️  THEORETICAL WARNING: This method performs pointwise OWA fusion on capacity values,
   * which does NOT guarantee that the result is a valid qualitative capacity. The resulting
   * set-function may violate the fundamental monotonicity property: A⊆B ⟹ γ(A)≤γ(B).
   * 
   * This happens because OWA operators are not guaranteed to preserve monotonicity when
   * applied pointwise. The "winning" value for subset A might come from a different capacity
   * than the "winning" value for superset B, breaking the monotonicity constraint.
   * 
   * For theoretically sound capacity fusion, use QualitativeFusion.normalizedConjunctive()
   * or QualitativeFusion.disjunctive() which work on QMTs directly.
   * 
   * This method is provided for experimental purposes and other applications where
   * monotonicity is not required.
   * 
   * @param {Array<QualitativeCapacity>} capacities - Array of capacities
   * @param {string} mode - Aggregation mode
   * @param {Array<number>} weights - Optional custom weights
   * @returns {QualitativeCapacity} Pointwise fused capacity (may not be monotonic)
   */
  static pointwiseOWAFusion(capacities, mode = 'max', weights = null) {
    if (!capacities.length) {
      throw new Error('At least one capacity is required');
    }
    
    const scale = capacities[0].scale;
    const stateSpace = capacities[0].stateSpace;
    
    // Validate all capacities use the same scale and state space
    for (const capacity of capacities) {
      if (!capacity.scale.equals(scale)) {
        throw new Error('All capacities must use the same qualitative scale');
      }
      if (capacity.stateSpace.length !== stateSpace.length) {
        throw new Error('All capacities must use the same state space');
      }
    }
    
    // Generate all subsets
    const allSubsets = this._generateAllSubsets(stateSpace);
    const resultQMT = new Map();
    
    for (const subset of allSubsets) {
      // Get capacity values for this subset from all capacities
      const capacityValues = capacities.map(cap => cap.getCapacity(subset));
      const metas = capacities.map((cap, i) => ({ capacityIndex: i, source: 'capacity' }));
      
      // Fuse using OWA
      const fusedResult = this.fuseWithMeta(capacityValues, metas, weights, mode, scale);
      
      if (fusedResult.value !== scale.bottom) {
        resultQMT.set(subset, fusedResult.value);
      }
    }
    
    return new QualitativeCapacity(stateSpace, scale, resultQMT);
  }
  
  /**
   * Sugeno Integral - Theoretically Sound Qualitative Aggregation
   * 
   * The Sugeno integral is the qualitative counterpart to the Choquet integral and provides
   * a theoretically sound way to aggregate qualitative values with respect to a capacity.
   * 
   * S_γ(f) = max_{i=1}^n min(f_{(i)}, γ(A_{(i)}))
   * 
   * where f_{(i)} are the sorted values in descending order and A_{(i)} = {w_{(1)}, ..., w_{(i)}}
   * 
   * @param {QualitativeCapacity} capacity - The capacity γ
   * @param {Map|Object} decisionFunction - Function f: W → L mapping states to scale values
   * @returns {number} Sugeno integral value
   */
  static sugenoIntegral(capacity, decisionFunction) {
    const scale = capacity.scale;
    const stateSpace = capacity.stateSpace;
    
    // Convert decision function to Map if needed
    const f = decisionFunction instanceof Map ? decisionFunction : new Map(Object.entries(decisionFunction));
    
    // Create pairs of (state, value) and sort by value in descending order
    const stateValuePairs = stateSpace
      .map(state => ({
        state,
        value: f.get(state) || scale.bottom
      }))
      .sort((a, b) => scale.compare(b.value, a.value)); // Descending order
    
    let result = scale.bottom;
    
    // Compute Sugeno integral: max_{i=1}^n min(f_{(i)}, γ(A_{(i)}))
    for (let i = 0; i < stateValuePairs.length; i++) {
      // A_{(i)} = {w_{(1)}, ..., w_{(i)}} (first i states in sorted order)
      const Ai = new Set(stateValuePairs.slice(0, i + 1).map(pair => pair.state));
      
      // f_{(i)} is the value of the i-th state in sorted order
      const fi = stateValuePairs[i].value;
      
      // γ(A_{(i)}) is the capacity value of the subset
      const gammaAi = capacity.getCapacity(Ai);
      
      // min(f_{(i)}, γ(A_{(i)}))
      const minValue = scale.min(fi, gammaAi);
      
      // max over all i
      result = scale.max(result, minValue);
    }
    
    return result;
  }
  
  /**
   * Generate all possible subsets of a state space
   */
  static _generateAllSubsets(stateSpace) {
    const subsets = [];
    const n = stateSpace.length;
    
    // Generate all 2^n subsets
    for (let i = 0; i < (1 << n); i++) {
      const subset = new Set();
      for (let j = 0; j < n; j++) {
        if (i & (1 << j)) {
          subset.add(stateSpace[j]);
        }
      }
      subsets.push(subset);
    }
    
    return subsets;
  }
}

/**
 * Convert aggregator name to OWA weights for qualitative scales
 */
export function getOWAQualitativeWeights(aggregator, length, customWeights = null, scale = DEFAULT_QUALITATIVE_SCALE) {
  return OWAQualitativeFusion.generateOWAWeights(length, aggregator, customWeights, true, scale);
}

/**
 * Convenience function to get OWA weights from rule configuration for qualitative scales
 */
export function getOWAQualitativeWeightsFromRule(rule, length, metas = null, scale = DEFAULT_QUALITATIVE_SCALE) {
  // If explicit owaWeights provided, use them
  if (rule.owaWeights && Array.isArray(rule.owaWeights)) {
    return OWAQualitativeFusion.generateOWAWeights(length, 'custom', rule.owaWeights, true, scale);
  }
  
  // If aggregator specified, convert to OWA weights
  if (rule.aggregator) {
    // For priority aggregator, extract priorities from metadata
    if (rule.aggregator === 'priority' && metas && Array.isArray(metas)) {
      const priorities = metas.map(meta => meta?.rule?.priority || meta?.priority || 0);
      return OWAQualitativeFusion.generateOWAWeights(length, 'priority', priorities, true, scale);
    }
    
    return OWAQualitativeFusion.generateOWAWeights(length, rule.aggregator, rule.owaWeights, true, scale);
  }
  
  // Default fallback
  return OWAQualitativeFusion.generateOWAWeights(length, 'max', null, true, scale);
}
