export class OWAFusion {
  static fuseTriplesWithMeta(values, possibilities, reliabilities, metas, weights, mode = 'max', normalize = true, traceOptions = null) {
    if (!values.length) return { value: 0, possibility: 0, reliability: 0, meta: null };
    if (values.length === 1) {
      return {
        value: values[0],
        possibility: possibilities[0],
        reliability: reliabilities[0],
        meta: metas[0]
      };
    }

    const modeLower = typeof mode === 'string' ? mode.toLowerCase() : mode;
    const canFastPath = !weights && !(traceOptions && traceOptions.includeTrace) &&
      (modeLower === 'max' || modeLower === 'min' || modeLower === 'top2' || modeLower === 'top3');
    if (canFastPath) {
      const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
      const priorities = new Array(values.length);
      const ruleTypes = new Array(values.length);
      for (let i = 0; i < metas.length; i++) {
        const meta = metas[i];
        priorities[i] = meta?.rule?.priority ?? 0;
        ruleTypes[i] = typeOrder[meta?.ruleType] ?? 0;
      }

      const compareDesc = (a, b) => {
        const valDiff = values[b] - values[a];
        if (valDiff !== 0) return valDiff;
        const prioDiff = priorities[b] - priorities[a];
        if (prioDiff !== 0) return prioDiff;
        return ruleTypes[b] - ruleTypes[a];
      };
      const compareAsc = (a, b) => {
        const valDiff = values[a] - values[b];
        if (valDiff !== 0) return valDiff;
        const prioDiff = priorities[a] - priorities[b];
        if (prioDiff !== 0) return prioDiff;
        return ruleTypes[a] - ruleTypes[b];
      };

      if (modeLower === 'max') {
        let bestIdx = 0;
        for (let i = 1; i < values.length; i++) {
          if (compareDesc(bestIdx, i) > 0) {
            bestIdx = i;
          }
        }
        return {
          value: values[bestIdx],
          possibility: possibilities[bestIdx],
          reliability: reliabilities[bestIdx],
          meta: metas[bestIdx]
        };
      }

      if (modeLower === 'min') {
        let bestIdx = 0;
        for (let i = 1; i < values.length; i++) {
          if (compareAsc(bestIdx, i) > 0) {
            bestIdx = i;
          }
        }
        return {
          value: values[bestIdx],
          possibility: possibilities[bestIdx],
          reliability: reliabilities[bestIdx],
          meta: metas[bestIdx]
        };
      }

      const topCount = modeLower === 'top2' ? 2 : 3;
      const count = Math.min(topCount, values.length);
      const topIndices = [];
      for (let i = 0; i < values.length; i++) {
        if (topIndices.length === 0) {
          topIndices.push(i);
          continue;
        }
        let insertAt = topIndices.length;
        for (let j = 0; j < topIndices.length; j++) {
          if (compareDesc(i, topIndices[j]) > 0) continue;
          insertAt = j;
          break;
        }
        topIndices.splice(insertAt, 0, i);
        if (topIndices.length > count) {
          topIndices.pop();
        }
      }

      const weight = 1 / count;
      let valueSum = 0;
      let possibilitySum = 0;
      let reliabilitySum = 0;
      let selectedIdx = topIndices[0];
      let maxContribution = weight * values[selectedIdx];

      for (let i = 0; i < topIndices.length; i++) {
        const idx = topIndices[i];
        const contribution = weight * values[idx];
        valueSum += contribution;
        possibilitySum += weight * possibilities[idx];
        reliabilitySum += weight * reliabilities[idx];

        const currentPriority = priorities[idx];
        const selectedPriority = priorities[selectedIdx];
        const currentRuleType = ruleTypes[idx];
        const selectedRuleType = ruleTypes[selectedIdx];
        const contributionDiff = Math.abs(contribution - maxContribution);
        const avgContribution = (contribution + maxContribution) / 2;
        const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;

        if (contribution > maxContribution ||
            (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
            (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
            (contribution === maxContribution && values[idx] > values[selectedIdx])) {
          maxContribution = contribution;
          selectedIdx = idx;
        }
      }

      return {
        value: valueSum,
        possibility: possibilitySum,
        reliability: reliabilitySum,
        meta: metas[selectedIdx]
      };
    }

    const sparseCount = weights && weights._owaSparseCount;
    if (sparseCount && sparseCount <= 3 && weights.length === values.length && !(traceOptions && traceOptions.includeTrace)) {
        const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
        const priorities = new Array(values.length);
        const ruleTypes = new Array(values.length);
        for (let i = 0; i < metas.length; i++) {
          const meta = metas[i];
          priorities[i] = meta?.rule?.priority ?? 0;
          ruleTypes[i] = typeOrder[meta?.ruleType] ?? 0;
        }
        const compareDesc = (a, b) => {
          const valDiff = values[b] - values[a];
          if (valDiff !== 0) return valDiff;
          const prioDiff = priorities[b] - priorities[a];
          if (prioDiff !== 0) return prioDiff;
          return ruleTypes[b] - ruleTypes[a];
        };

        const topIndices = [];
        for (let i = 0; i < values.length; i++) {
          if (topIndices.length === 0) {
            topIndices.push(i);
            continue;
          }
        let insertAt = topIndices.length;
        for (let j = 0; j < topIndices.length; j++) {
          if (compareDesc(i, topIndices[j]) > 0) continue;
          insertAt = j;
          break;
        }
          topIndices.splice(insertAt, 0, i);
          if (topIndices.length > sparseCount) {
            topIndices.pop();
          }
        }

        if (OWAFusion.sortCounter) {
          const counter = OWAFusion.sortCounter;
          counter.total = (counter.total || 0) + 1;
          counter.byMode = counter.byMode || {};
          counter.byMethod = counter.byMethod || {};
          counter.byMode[mode] = (counter.byMode[mode] || 0) + 1;
          counter.byMethod.fuseTriplesWithMetaSparse = (counter.byMethod.fuseTriplesWithMetaSparse || 0) + 1;
        }

        let valueSum = 0;
        let possibilitySum = 0;
        let reliabilitySum = 0;
        let selectedIdx = topIndices[0];
        let maxContribution = weights[0] * values[selectedIdx];

        for (let i = 0; i < topIndices.length; i++) {
          const idx = topIndices[i];
          const weight = weights[i] || 0;
          const contribution = weight * values[idx];
          valueSum += contribution;
          possibilitySum += weight * possibilities[idx];
          reliabilitySum += weight * reliabilities[idx];

          const currentPriority = priorities[idx];
          const selectedPriority = priorities[selectedIdx];
          const currentRuleType = ruleTypes[idx];
          const selectedRuleType = ruleTypes[selectedIdx];
          const contributionDiff = Math.abs(contribution - maxContribution);
          const avgContribution = (contribution + maxContribution) / 2;
          const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;

          if (contribution > maxContribution ||
              (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
              (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
              (contribution === maxContribution && values[idx] > values[selectedIdx])) {
            maxContribution = contribution;
            selectedIdx = idx;
          }
        }

        return {
          value: valueSum,
          possibility: possibilitySum,
          reliability: reliabilitySum,
          meta: metas[selectedIdx]
        };
    }

    if (OWAFusion.sortCounter) {
      const counter = OWAFusion.sortCounter;
      counter.total = (counter.total || 0) + 1;
      counter.byMode = counter.byMode || {};
      counter.byMethod = counter.byMethod || {};
      counter.byMode[mode] = (counter.byMode[mode] || 0) + 1;
      counter.byMethod.fuseTriplesWithMeta = (counter.byMethod.fuseTriplesWithMeta || 0) + 1;
    }

    const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };

    const sortedValues = new Array(values.length);
    const sortedPossibilities = new Array(values.length);
    const sortedReliabilities = new Array(values.length);
    const sortedMetas = new Array(values.length);
    for (let i = 0; i < values.length; i++) {
      sortedValues[i] = values[i];
      sortedPossibilities[i] = possibilities[i];
      sortedReliabilities[i] = reliabilities[i];
      sortedMetas[i] = metas[i];
    }

    const typeOrderLookup = typeOrder;
    const priorities = new Array(values.length);
    const ruleTypes = new Array(values.length);
    for (let i = 0; i < sortedMetas.length; i++) {
      const meta = sortedMetas[i];
      priorities[i] = meta?.rule?.priority ?? 0;
      ruleTypes[i] = typeOrderLookup[meta?.ruleType] ?? 0;
    }

    const indices = sortedValues.map((_, i) => i);
    indices.sort((a, b) => {
      const valDiff = sortedValues[b] - sortedValues[a];
      if (valDiff !== 0) return valDiff;
      const prioDiff = priorities[b] - priorities[a];
      if (prioDiff !== 0) return prioDiff;
      return ruleTypes[b] - ruleTypes[a];
    });

    const originalValues = sortedValues.slice();
    const originalPossibilities = sortedPossibilities.slice();
    const originalReliabilities = sortedReliabilities.slice();
    const originalMetas = sortedMetas.slice();
    for (let i = 0; i < indices.length; i++) {
      const idx = indices[i];
      sortedValues[i] = originalValues[idx];
      sortedPossibilities[i] = originalPossibilities[idx];
      sortedReliabilities[i] = originalReliabilities[idx];
      sortedMetas[i] = originalMetas[idx];
      priorities[i] = priorities[idx];
      ruleTypes[i] = ruleTypes[idx];
    }

    let owaWeights;
    if (weights && weights.length === values.length) {
      owaWeights = weights;
    } else {
      owaWeights = this.generateOWAWeights(values.length, mode, null, normalize);
    }

    let valueSum = 0;
    let possibilitySum = 0;
    let reliabilitySum = 0;
    let selectedIdx = 0;
    let maxContribution = 0;
    const contributions = traceOptions?.includeTrace ? new Array(sortedValues.length) : null;

    for (let i = 0; i < sortedValues.length; i++) {
      const weight = owaWeights[i];
      const contribution = weight * sortedValues[i];
      valueSum += contribution;
      possibilitySum += weight * sortedPossibilities[i];
      reliabilitySum += weight * sortedReliabilities[i];
      if (contributions) {
        contributions[i] = contribution;
      }

      const currentPriority = priorities[i];
      const selectedPriority = priorities[selectedIdx];
      const currentRuleType = ruleTypes[i];
      const selectedRuleType = ruleTypes[selectedIdx];
      const contributionDiff = Math.abs(contribution - maxContribution);
      const avgContribution = (contribution + maxContribution) / 2;
      const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;

      if (contribution > maxContribution ||
          (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
          (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
          (contribution === maxContribution && sortedValues[i] > sortedValues[selectedIdx])) {
        maxContribution = contribution;
        selectedIdx = i;
      }
    }

    const response = {
      value: valueSum,
      possibility: possibilitySum,
      reliability: reliabilitySum,
      meta: sortedMetas[selectedIdx]
    };
    if (traceOptions?.includeTrace) {
      response.trace = {
        weights: owaWeights,
        sortedValues,
        sortedPossibilities,
        sortedReliabilities,
        contributions,
        selectedIndex: selectedIdx
      };
    }
    return response;
  }

  static fuseWithMeta(values, metas, weights, mode = 'max', normalize = true, traceOptions = null) {
    if (!values.length) return { value: 0, meta: null };
    if (values.length === 1) {
      return { value: values[0], meta: metas[0] };
    }

    const modeLower = typeof mode === 'string' ? mode.toLowerCase() : mode;
    const canFastPath = !weights && !(traceOptions && traceOptions.includeTrace) &&
      (modeLower === 'max' || modeLower === 'min' || modeLower === 'top2' || modeLower === 'top3');
    if (canFastPath) {
      const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
      const priorities = new Array(values.length);
      const ruleTypes = new Array(values.length);
      for (let i = 0; i < metas.length; i++) {
        const meta = metas[i];
        priorities[i] = meta?.rule?.priority ?? 0;
        ruleTypes[i] = typeOrder[meta?.ruleType] ?? 0;
      }

      const compareDesc = (a, b) => {
        const valDiff = values[b] - values[a];
        if (valDiff !== 0) return valDiff;
        const prioDiff = priorities[b] - priorities[a];
        if (prioDiff !== 0) return prioDiff;
        return ruleTypes[b] - ruleTypes[a];
      };
      const compareAsc = (a, b) => {
        const valDiff = values[a] - values[b];
        if (valDiff !== 0) return valDiff;
        const prioDiff = priorities[a] - priorities[b];
        if (prioDiff !== 0) return prioDiff;
        return ruleTypes[a] - ruleTypes[b];
      };

      if (modeLower === 'max') {
        let bestIdx = 0;
        for (let i = 1; i < values.length; i++) {
          if (compareDesc(bestIdx, i) > 0) {
            bestIdx = i;
          }
        }
        return { value: values[bestIdx], meta: metas[bestIdx] };
      }

      if (modeLower === 'min') {
        let bestIdx = 0;
        for (let i = 1; i < values.length; i++) {
          if (compareAsc(bestIdx, i) > 0) {
            bestIdx = i;
          }
        }
        return { value: values[bestIdx], meta: metas[bestIdx] };
      }

      const topCount = modeLower === 'top2' ? 2 : 3;
      const count = Math.min(topCount, values.length);
      const topIndices = [];
      for (let i = 0; i < values.length; i++) {
        if (topIndices.length === 0) {
          topIndices.push(i);
          continue;
        }
        let insertAt = topIndices.length;
        for (let j = 0; j < topIndices.length; j++) {
          if (compareDesc(i, topIndices[j]) > 0) continue;
          insertAt = j;
          break;
        }
        topIndices.splice(insertAt, 0, i);
        if (topIndices.length > count) {
          topIndices.pop();
        }
      }

      const weight = 1 / count;
      let valueSum = 0;
      let selectedIdx = topIndices[0];
      let maxContribution = weight * values[selectedIdx];

      for (let i = 0; i < topIndices.length; i++) {
        const idx = topIndices[i];
        const contribution = weight * values[idx];
        valueSum += contribution;

        const currentPriority = priorities[idx];
        const selectedPriority = priorities[selectedIdx];
        const currentRuleType = ruleTypes[idx];
        const selectedRuleType = ruleTypes[selectedIdx];
        const contributionDiff = Math.abs(contribution - maxContribution);
        const avgContribution = (contribution + maxContribution) / 2;
        const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;

        if (contribution > maxContribution ||
            (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
            (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
            (contribution === maxContribution && values[idx] > values[selectedIdx])) {
          maxContribution = contribution;
          selectedIdx = idx;
        }
      }

      return { value: valueSum, meta: metas[selectedIdx] };
    }

    const sparseCount = weights && weights._owaSparseCount;
    if (sparseCount && sparseCount <= 3 && weights.length === values.length && !(traceOptions && traceOptions.includeTrace)) {
        const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
        const priorities = new Array(values.length);
        const ruleTypes = new Array(values.length);
        for (let i = 0; i < metas.length; i++) {
          const meta = metas[i];
          priorities[i] = meta?.rule?.priority ?? 0;
          ruleTypes[i] = typeOrder[meta?.ruleType] ?? 0;
        }

        const compareDesc = (a, b) => {
          const valDiff = values[b] - values[a];
          if (valDiff !== 0) return valDiff;
          const prioDiff = priorities[b] - priorities[a];
          if (prioDiff !== 0) return prioDiff;
          return ruleTypes[b] - ruleTypes[a];
        };

        const topIndices = [];
        for (let i = 0; i < values.length; i++) {
          if (topIndices.length === 0) {
            topIndices.push(i);
            continue;
          }
        let insertAt = topIndices.length;
        for (let j = 0; j < topIndices.length; j++) {
          if (compareDesc(i, topIndices[j]) > 0) continue;
          insertAt = j;
          break;
        }
          topIndices.splice(insertAt, 0, i);
          if (topIndices.length > sparseCount) {
            topIndices.pop();
          }
        }

        if (OWAFusion.sortCounter) {
          const counter = OWAFusion.sortCounter;
          counter.total = (counter.total || 0) + 1;
          counter.byMode = counter.byMode || {};
          counter.byMethod = counter.byMethod || {};
          counter.byMode[mode] = (counter.byMode[mode] || 0) + 1;
          counter.byMethod.fuseWithMetaSparse = (counter.byMethod.fuseWithMetaSparse || 0) + 1;
        }

        let valueSum = 0;
        let selectedIdx = topIndices[0];
        let maxContribution = weights[0] * values[selectedIdx];

        for (let i = 0; i < topIndices.length; i++) {
          const idx = topIndices[i];
          const weight = weights[i] || 0;
          const contribution = weight * values[idx];
          valueSum += contribution;

          const currentPriority = priorities[idx];
          const selectedPriority = priorities[selectedIdx];
          const currentRuleType = ruleTypes[idx];
          const selectedRuleType = ruleTypes[selectedIdx];
          const contributionDiff = Math.abs(contribution - maxContribution);
          const avgContribution = (contribution + maxContribution) / 2;
          const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;

          if (contribution > maxContribution ||
              (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
              (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
              (contribution === maxContribution && values[idx] > values[selectedIdx])) {
            maxContribution = contribution;
            selectedIdx = idx;
          }
        }

        return { value: valueSum, meta: metas[selectedIdx] };
    }

    if (OWAFusion.sortCounter) {
      const counter = OWAFusion.sortCounter;
      counter.total = (counter.total || 0) + 1;
      counter.byMode = counter.byMode || {};
      counter.byMethod = counter.byMethod || {};
      counter.byMode[mode] = (counter.byMode[mode] || 0) + 1;
      counter.byMethod.fuseWithMeta = (counter.byMethod.fuseWithMeta || 0) + 1;
    }
    
    const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
    
    const sortedValues = new Array(values.length);
    const sortedMetas = new Array(values.length);
    for (let i = 0; i < values.length; i++) {
      sortedValues[i] = values[i];
      sortedMetas[i] = metas[i];
    }
    
    // Sort by value (descending), then by priority, then by rule type
    // This is the core of OWA - we always sort first
    const typeOrderLookup = typeOrder;
    const priorities = new Array(values.length);
    const ruleTypes = new Array(values.length);
    for (let i = 0; i < sortedMetas.length; i++) {
      const meta = sortedMetas[i];
      priorities[i] = meta?.rule?.priority ?? 0;
      ruleTypes[i] = typeOrderLookup[meta?.ruleType] ?? 0;
    }
    const indices = sortedValues.map((_, i) => i);
    indices.sort((a, b) => {
      const valDiff = sortedValues[b] - sortedValues[a];
      if (valDiff !== 0) return valDiff;
      const prioDiff = priorities[b] - priorities[a];
      if (prioDiff !== 0) return prioDiff;
      return ruleTypes[b] - ruleTypes[a];
    });
    const originalValues = sortedValues.slice();
    const originalMetas = sortedMetas.slice();
    for (let i = 0; i < indices.length; i++) {
      const idx = indices[i];
      sortedValues[i] = originalValues[idx];
      sortedMetas[i] = originalMetas[idx];
      priorities[i] = priorities[idx];
      ruleTypes[i] = ruleTypes[idx];
    }
    
    // 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, normalize);
    }
    
    // Apply OWA: sum of (weight_i * sorted_value_i)
    let sum = 0;
    let selectedIdx = 0;
    let maxContribution = 0;
    const contributions = traceOptions?.includeTrace ? new Array(sortedValues.length) : null;
    
    for (let i = 0; i < sortedValues.length; i++) {
      const contribution = owaWeights[i] * sortedValues[i];
      sum += contribution;
      if (contributions) {
        contributions[i] = contribution;
      }
      
      // Select the index with the highest weighted contribution for metadata
      // If contributions are close (within 10%), prefer higher priority
      const currentMeta = sortedMetas[i];
      const selectedMeta = sortedMetas[selectedIdx];
      const currentPriority = priorities[i];
      const selectedPriority = priorities[selectedIdx];
      const currentRuleType = ruleTypes[i];
      const selectedRuleType = ruleTypes[selectedIdx];
      
      // Calculate relative difference in contributions
      const contributionDiff = Math.abs(contribution - maxContribution);
      const avgContribution = (contribution + maxContribution) / 2;
      const relativeContributionDiff = avgContribution > 0 ? contributionDiff / avgContribution : 0;
      
      if (contribution > maxContribution || 
          (relativeContributionDiff < 0.15 && currentPriority > selectedPriority) ||
          (relativeContributionDiff < 0.15 && currentPriority === selectedPriority && currentRuleType > selectedRuleType) ||
          (contribution === maxContribution && sortedValues[i] > sortedValues[selectedIdx])) {
        maxContribution = contribution;
        selectedIdx = i;
      }
    }
    
    const response = { value: sum, meta: sortedMetas[selectedIdx] };
    if (traceOptions?.includeTrace) {
      response.trace = {
        weights: owaWeights,
        sortedValues,
        contributions,
        selectedIndex: selectedIdx
      };
    }
    return response;
  }
  
  /**
   * Generate OWA weights for different aggregation strategies
   * Unified method supporting all aggregator types including Sum operator
   * @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 to sum to 1.0 (default) or preserve for Sum operator
   */
  static generateOWAWeights(length, mode, customWeights = null, normalize = true) {
    if (!mode || length <= 0) {
      return [1, ...Array(Math.max(0, length - 1)).fill(0)]; // Default: max
    }
    
    const weights = new Array(length).fill(0);
    
    switch (mode.toLowerCase()) {
      case 'max':
        // MAX: All weight on the highest value
        weights[0] = 1;
        break;
        
      case 'min':
        // MIN: All weight on the lowest value (last in sorted order)
        weights[length - 1] = 1;
        break;
        
      case 'sum':
      case 'sum_unbounded':
        // SUM: Equal weights, but NOT normalized to sum to 1.0
        // This creates scaling effect where result = k * average
        weights.fill(1); // Each element contributes its full value
        normalize = false; // Override normalization for Sum operator
        break;
        
      case 'avg':
      case 'average':
      case 'mean':
        // AVERAGE: Equal weights
        weights.fill(1 / length);
        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)] = 1;
          } else {
            // Even length: equal weight on two middle elements
            const mid1 = length / 2 - 1;
            const mid2 = length / 2;
            weights[mid1] = 0.5;
            weights[mid2] = 0.5;
          }
        } else {
          // For larger sets, weight toward the majority (top 60% of values get equal weight)
          const majorityCount = Math.max(1, Math.ceil(length * 0.6));
          const majorityWeight = 1 / majorityCount;
          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] = 1;
        } else if (length === 2) {
          weights[0] = 0.5;
          weights[1] = 0.5;
        } else if (length % 2 === 1) {
          // Odd length: single median
          const medianIndex = Math.floor(length / 2);
          weights[medianIndex] = 1;
        } else {
          // Even length: average of two middle values
          const mid1 = Math.floor(length / 2) - 1;
          const mid2 = Math.floor(length / 2);
          weights[mid1] = 0.5;
          weights[mid2] = 0.5;
        }
        break;
        
      case 'optimistic':
        // OPTIMISTIC: More weight on higher values (exponential decay)
        for (let i = 0; i < length; i++) {
          weights[i] = Math.pow(0.5, i);
        }
        // Normalize
        const optimisticSum = weights.reduce((a, b) => a + b, 0);
        for (let i = 0; i < length; i++) {
          weights[i] /= optimisticSum;
        }
        break;
        
      case 'pessimistic':
        // PESSIMISTIC: More weight on lower values
        for (let i = 0; i < length; i++) {
          weights[length - 1 - i] = Math.pow(0.5, i);
        }
        // Normalize
        const pessimisticSum = weights.reduce((a, b) => a + b, 0);
        for (let i = 0; i < length; i++) {
          weights[i] /= pessimisticSum;
        }
        break;
        
      case 'top2':
        // TOP2: Equal weight on top 2 values
        if (length >= 2) {
          weights[0] = 0.5;
          weights[1] = 0.5;
        } else if (length === 1) {
          weights[0] = 1;
        }
        break;
        
      case 'top3':
        // TOP3: Equal weight on top 3 values
        const top3Count = Math.min(3, length);
        for (let i = 0; i < top3Count; i++) {
          weights[i] = 1 / top3Count;
        }
        break;
        
      case 'priority':
        // PRIORITY: Use normalized rule priorities as weights
        // This requires the customWeights parameter to contain priority values
        if (customWeights && Array.isArray(customWeights) && customWeights.length === length) {
          // Normalize priorities to create OWA weights
          const maxPriority = Math.max(...customWeights, 1); // Avoid division by zero
          const normalizedPriorities = customWeights.map(p => (p || 0) / maxPriority);
          
          // Convert to weights (higher priority = higher weight)
          const sum = normalizedPriorities.reduce((a, b) => a + b, 0);
          if (sum > 0) {
            return normalizedPriorities.map(p => p / sum);
          } else {
            // Fallback to equal weights if all priorities are 0
            weights.fill(1 / length);
          }
        } else {
          // Fallback to max if no priorities provided
          weights[0] = 1;
        }
        break;
        
      case 'owa':
      case 'custom':
        // CUSTOM: Custom OWA weights provided by user
        if (customWeights && Array.isArray(customWeights)) {
          if (customWeights.length === length) {
            // Use weights as provided, respecting normalize parameter
            if (normalize) {
              const sum = customWeights.reduce((a, b) => a + b, 0);
              return sum > 0 ? customWeights.map(w => w / sum) : customWeights;
            } else {
              return [...customWeights]; // Return as-is for Sum operations
            }
          } 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);
            
            // Apply normalization if requested
            if (normalize) {
              const sum = normalized.reduce((a, b) => a + b, 0);
              return sum > 0 ? normalized.map(w => w / sum) : normalized;
            } else {
              return normalized;
            }
          }
        }
        // Fallback to max if no valid custom weights
        weights[0] = 1;
        break;
        
      default:
        console.warn(`Unknown aggregator '${mode}', defaulting to 'max'`);
        weights[0] = 1;
        break;
    }
    
    return weights;
  }
  
  /**
   * Convenience methods for common OWA operations
   */
  static max(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'max');
  }
  
  static min(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'min');
  }
  
  static average(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'average');
  }
  
  static majority(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'majority');
  }
  
  static median(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'median');
  }
  
  static optimistic(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'optimistic');
  }
  
  static pessimistic(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'pessimistic');
  }
  
  static top2(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'top2');
  }
  
  static top3(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'top3');
  }
  
  static priority(values, metas, priorities) {
    return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'priority', priorities), 'priority');
  }
  
  static custom(values, metas, customWeights) {
    return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'custom', customWeights), 'custom');
  }
  
  /**
   * Sum aggregation - each value contributes its full amount (no normalization)
   */
  static sum(values, metas) {
    return this.fuseWithMeta(values, metas, null, 'sum', false);
  }

  /**
   * Fuse intervals using OWA with metadata selection
   * @param {Array<{min: number, max: number}>} intervals - Array of intervals to fuse
   * @param {Array} metas - Metadata for each interval
   * @param {Array<number>} weights - OWA weights (optional)
   * @param {string} mode - Aggregation mode
   * @param {boolean} normalize - Whether to normalize weights
   * @returns {{interval: {min: number, max: number}, meta: any}}
   */
  static fuseIntervalsWithMeta(intervals, metas, weights, mode = 'max', normalize = true) {
    if (!intervals.length) return { interval: { min: 0, max: 0 }, meta: null };
    
    // Filter out null/invalid intervals
    const validIndices = [];
    const validIntervals = [];
    const validMetas = [];
    
    for (let i = 0; i < intervals.length; i++) {
      if (intervals[i] && intervals[i].min !== undefined && intervals[i].max !== undefined) {
        validIndices.push(i);
        validIntervals.push(intervals[i]);
        validMetas.push(metas[i]);
      }
    }
    
    if (!validIntervals.length) return { interval: { min: 0, max: 0 }, meta: null };
    
    // For single interval, return it directly
    if (validIntervals.length === 1) {
      return { interval: validIntervals[0], meta: validMetas[0] };
    }
    
    // Generate OWA weights
    const owaWeights = weights && weights.length === validIntervals.length ? 
      weights : this.generateOWAWeights(validIntervals.length, mode, null, normalize);
    
    // Apply interval-specific aggregation based on mode
    let fusedInterval;
    let selectedMeta;
    
    switch (mode.toLowerCase()) {
      case 'max':
        // For max: select interval with highest max value
        const maxIdx = validIntervals.reduce((maxI, interval, i) => 
          interval.max > validIntervals[maxI].max ? i : maxI, 0);
        fusedInterval = validIntervals[maxIdx];
        selectedMeta = validMetas[maxIdx];
        break;
        
      case 'min':
        // For min: select interval with lowest min value
        const minIdx = validIntervals.reduce((minI, interval, i) => 
          interval.min < validIntervals[minI].min ? i : minI, 0);
        fusedInterval = validIntervals[minIdx];
        selectedMeta = validMetas[minIdx];
        break;
        
      case 'sum':
        // Sum intervals: [sum of mins, sum of maxs]
        fusedInterval = {
          min: validIntervals.reduce((sum, interval) => sum + interval.min, 0),
          max: validIntervals.reduce((sum, interval) => sum + interval.max, 0)
        };
        // For sum, use first meta or create composite
        selectedMeta = validMetas[0];
        break;
        
      case 'average':
      case 'mean':
        // Average intervals: [avg of mins, avg of maxs]
        fusedInterval = {
          min: validIntervals.reduce((sum, interval) => sum + interval.min, 0) / validIntervals.length,
          max: validIntervals.reduce((sum, interval) => sum + interval.max, 0) / validIntervals.length
        };
        selectedMeta = validMetas[0];
        break;
        
      case 'union':
        // Union of intervals: [min of mins, max of maxs]
        fusedInterval = {
          min: Math.min(...validIntervals.map(i => i.min)),
          max: Math.max(...validIntervals.map(i => i.max))
        };
        // Select meta from interval contributing to the bounds
        const unionMinContributor = validIntervals.findIndex(i => i.min === fusedInterval.min);
        const unionMaxContributor = validIntervals.findIndex(i => i.max === fusedInterval.max);
        selectedMeta = validMetas[unionMaxContributor] || validMetas[unionMinContributor];
        break;
        
      case 'intersection':
        // Intersection of intervals: [max of mins, min of maxs]
        const intersectionMin = Math.max(...validIntervals.map(i => i.min));
        const intersectionMax = Math.min(...validIntervals.map(i => i.max));
        if (intersectionMin <= intersectionMax) {
          fusedInterval = { min: intersectionMin, max: intersectionMax };
        } else {
          // Empty intersection
          fusedInterval = { min: 0, max: 0 };
        }
        selectedMeta = validMetas[0];
        break;
        
      case 'majority':
      case 'median':
        // For median/majority: use interval arithmetic on sorted midpoints
        const midpoints = validIntervals.map(i => (i.min + i.max) / 2);
        const sortedIndices = midpoints
          .map((v, i) => ({ v, i }))
          .sort((a, b) => a.v - b.v)
          .map(item => item.i);
        
        const medianIdx = Math.floor(validIntervals.length / 2);
        const medianSelectedIdx = sortedIndices[medianIdx];
        fusedInterval = validIntervals[medianSelectedIdx];
        selectedMeta = validMetas[medianSelectedIdx];
        break;
        
      default:
        // Default: weighted interval fusion using OWA on mins and maxs separately
        const sortedByMidpoint = validIntervals
          .map((interval, i) => ({
            interval,
            meta: validMetas[i],
            midpoint: (interval.min + interval.max) / 2,
            idx: i
          }))
          .sort((a, b) => b.midpoint - a.midpoint);
        
        // Apply OWA weights to sorted intervals
        let fusedMin = 0;
        let fusedMax = 0;
        let maxWeight = 0;
        let owaSelectedIdx = 0;
        
        for (let i = 0; i < sortedByMidpoint.length; i++) {
          const weight = owaWeights[i];
          fusedMin += weight * sortedByMidpoint[i].interval.min;
          fusedMax += weight * sortedByMidpoint[i].interval.max;
          
          if (weight > maxWeight) {
            maxWeight = weight;
            owaSelectedIdx = i;
          }
        }
        
        fusedInterval = { min: fusedMin, max: fusedMax };
        selectedMeta = sortedByMidpoint[owaSelectedIdx].meta;
        break;
    }
    
    return { interval: fusedInterval, meta: selectedMeta };
  }
  
  /**
   * Helper method to check if a value is within TTL
   * @param {number} timestamp - The timestamp to check
   * @param {number} ttlMs - TTL in milliseconds (default 24 hours)
   * @returns {boolean} Whether the value is within TTL
   */
  static isWithinTTL(timestamp, ttlMs = 24 * 60 * 60 * 1000, now = null) {
    if (!timestamp) return false;
    const age = (now !== null && now !== undefined ? now : Date.now()) - timestamp;
    return age <= ttlMs;
  }
  
  /**
   * Convenience interval fusion methods
   */
  static maxInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'max');
  }
  
  static minInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'min');
  }
  
  static sumInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'sum');
  }
  
  static averageInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'average');
  }
  
  static unionInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'union');
  }
  
  static intersectionInterval(intervals, metas) {
    return this.fuseIntervalsWithMeta(intervals, metas, null, 'intersection');
  }
}

/**
 * Convert aggregator name to OWA weights
 * @param {string} aggregator - Aggregator type (max, min, majority, median, optimistic, pessimistic, sum, etc.)
 * @param {number} length - Number of values to aggregate
 * @param {Array<number>} customWeights - Custom weights for 'owa'/'priority' aggregators
 * @param {boolean} normalize - Whether to normalize weights to sum to 1.0 (default) or preserve for Sum operator
 * @returns {Array<number>} OWA weights
 */
export function getOWAWeights(aggregator, length, customWeights = null, normalize = true) {
  return OWAFusion.generateOWAWeights(length, aggregator, customWeights, normalize);
}

/**
 * Convenience function to get OWA weights from rule configuration
 * @param {Object} rule - Rule configuration
 * @param {number} length - Number of values to aggregate
 * @param {Array<Object>} metas - Optional metadata array containing rule priorities
 * @param {boolean} normalize - Whether to normalize weights (default true)
 * @returns {Array<number>} OWA weights
 */
export function getOWAWeightsFromRule(rule, length, metas = null, normalize = true) {
  const hasPriorityMeta = rule.aggregator === 'priority' && metas && Array.isArray(metas);
  if (!hasPriorityMeta) {
    if (!rule._owaWeightsCache) {
      rule._owaWeightsCache = new Map();
    }
    const cacheKey = `${length}|${normalize ? 1 : 0}|${rule.aggregator || 'max'}|${Array.isArray(rule.owaWeights) ? 'custom' : 'default'}`;
    const cached = rule._owaWeightsCache.get(cacheKey);
    if (cached) {
      return cached;
    }
  }

  // If explicit owaWeights provided, use them
  if (rule.owaWeights && Array.isArray(rule.owaWeights)) {
    const weights = OWAFusion.generateOWAWeights(length, 'custom', rule.owaWeights, normalize);
    if (rule._owaSparseCount && rule._owaSparseCount <= weights.length) {
      weights._owaSparseCount = rule._owaSparseCount;
    }
    if (!hasPriorityMeta) {
      rule._owaWeightsCache.set(`${length}|${normalize ? 1 : 0}|${rule.aggregator || 'max'}|custom`, weights);
    }
    return weights;
  }
  
  // If aggregator specified, convert to OWA weights
  if (rule.aggregator) {
    // For sum aggregator, don't normalize by default
    const shouldNormalize = rule.aggregator === 'sum' ? false : normalize;
    
    // 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 OWAFusion.generateOWAWeights(length, 'priority', priorities, shouldNormalize);
    }

    const weights = OWAFusion.generateOWAWeights(length, rule.aggregator, rule.owaWeights, shouldNormalize);
    if (rule._owaSparseCount && rule._owaSparseCount <= weights.length) {
      weights._owaSparseCount = rule._owaSparseCount;
    }
    if (!hasPriorityMeta) {
      rule._owaWeightsCache.set(`${length}|${shouldNormalize ? 1 : 0}|${rule.aggregator || 'max'}|default`, weights);
    }
    return weights;
  }

  const weights = OWAFusion.generateOWAWeights(length, 'max', null, normalize);
  if (!hasPriorityMeta) {
    rule._owaWeightsCache.set(`${length}|${normalize ? 1 : 0}|max|default`, weights);
  }
  return weights;
}
