/**
 * roleStabilityEngine.js
 * Measures how stable a player's role is over time and applies
 * recency decay for high-variance players.
 */

/**
 * Calculate a stability score for a player's role profile.
 *
 * Rules:
 * - role_variance_score below 0.2: zero decay regardless of age (locked-in role)
 * - role_variance_score above 0.5: apply recency decay (recent games weighted more)
 * - Between 0.2 and 0.5: partial decay scaling linearly
 *
 * @param {Object} roleProfile — current role distribution
 * @param {number} roleVarianceScore — output of calculateRoleVariance (0-1)
 * @param {Array<Object>} historicalActivations — array of { date, roleProfile } objects,
 *        ordered oldest to newest
 * @returns {{ stability_score: number, role_change_events: number, decay_weights_by_period: Array<number> }}
 */
function calculateStability(roleProfile, roleVarianceScore, historicalActivations) {
  if (!historicalActivations || historicalActivations.length === 0) {
    return {
      stability_score: roleVarianceScore <= 0.2 ? 1.0 : 0.5,
      role_change_events: 0,
      decay_weights_by_period: [],
    };
  }

  const decayWeights = applyDecayWeights(historicalActivations, roleVarianceScore);

  // Count role change events: when the dominant role shifts between consecutive games
  let roleChangeEvents = 0;
  for (let i = 1; i < historicalActivations.length; i++) {
    const prevDominant = _getDominantFromActivation(historicalActivations[i - 1]);
    const currDominant = _getDominantFromActivation(historicalActivations[i]);
    if (prevDominant && currDominant && prevDominant !== currDominant) {
      roleChangeEvents++;
    }
  }

  // Stability is inverse of normalized change rate, adjusted by decay context
  const maxPossibleChanges = Math.max(1, historicalActivations.length - 1);
  const changeRate = roleChangeEvents / maxPossibleChanges;

  // Weighted consistency: how much the weighted recent profiles agree with current
  let weightedConsistency = 0;
  let totalWeight = 0;

  for (let i = 0; i < historicalActivations.length; i++) {
    const activation = historicalActivations[i];
    const weight = decayWeights[i] || 1;
    const similarity = _profileSimilarity(roleProfile, activation.roleProfile || {});
    weightedConsistency += similarity * weight;
    totalWeight += weight;
  }

  const avgConsistency = totalWeight > 0 ? weightedConsistency / totalWeight : 0.5;

  // Final stability: blend of low change rate and high consistency
  const stabilityScore = Math.min(1, Math.max(0,
    avgConsistency * 0.6 + (1 - changeRate) * 0.4
  ));

  return {
    stability_score: Math.round(stabilityScore * 1000) / 1000,
    role_change_events: roleChangeEvents,
    decay_weights_by_period: decayWeights.map((w) => Math.round(w * 1000) / 1000),
  };
}

/**
 * Apply recency decay weights to historical instances.
 *
 * - roleVarianceScore <= 0.2: all weights = 1.0 (no decay)
 * - roleVarianceScore >= 0.5: full exponential decay (lambda = 0.1)
 * - Between: linear interpolation of decay strength
 *
 * Newest instance (last in array) gets weight 1.0.
 * Older instances decay from there.
 *
 * @param {Array<Object>} instances — historical activations, oldest first
 * @param {number} roleVarianceScore — 0-1
 * @returns {Array<number>} array of weights, same length as instances
 */
function applyDecayWeights(instances, roleVarianceScore) {
  if (!instances || instances.length === 0) return [];

  const n = instances.length;

  // No decay for locked-in roles
  if (roleVarianceScore <= 0.2) {
    return new Array(n).fill(1.0);
  }

  // Decay strength: 0 at variance=0.2, 1.0 at variance>=0.5
  const decayStrength = Math.min(1.0, Math.max(0, (roleVarianceScore - 0.2) / 0.3));

  const lambda = 0.1 * decayStrength;
  const weights = [];

  for (let i = 0; i < n; i++) {
    // Distance from newest (last element)
    const age = n - 1 - i;
    const decayedWeight = Math.exp(-lambda * age);
    // Blend between no-decay (1.0) and full decay based on strength
    const weight = 1.0 * (1 - decayStrength) + decayedWeight * decayStrength;
    weights.push(weight);
  }

  return weights;
}

/**
 * Get dominant role from an activation record.
 * @private
 */
function _getDominantFromActivation(activation) {
  if (!activation || !activation.roleProfile) return null;
  const entries = Object.entries(activation.roleProfile);
  if (entries.length === 0) return null;
  return entries.reduce((best, curr) => (curr[1] > best[1] ? curr : best))[0];
}

/**
 * Cosine-ish similarity between two role profiles.
 * Measures overlap of distributions.
 * @private
 */
function _profileSimilarity(profileA, profileB) {
  const allKeys = new Set([...Object.keys(profileA), ...Object.keys(profileB)]);
  if (allKeys.size === 0) return 1;

  let dotProduct = 0;
  let magA = 0;
  let magB = 0;

  for (const key of allKeys) {
    const a = profileA[key] || 0;
    const b = profileB[key] || 0;
    dotProduct += a * b;
    magA += a * a;
    magB += b * b;
  }

  const magnitude = Math.sqrt(magA) * Math.sqrt(magB);
  if (magnitude === 0) return 0;

  return dotProduct / magnitude;
}

module.exports = {
  calculateStability,
  applyDecayWeights,
};
