/**
 * Probability Engine
 *
 * Converts betting odds to probabilities and transforms them into Elo ratings
 * for Monte Carlo simulation of playoff brackets.
 *
 * Key concepts:
 * 1. Futures odds (e.g., +550 to win championship) are "compressed" probabilities
 * 2. We "decompress" them using power transformation to get single-game strength
 * 3. Map decompressed strength to Elo scale
 * 4. Use Elo to calculate head-to-head win probabilities
 */

/**
 * Odds format types supported by the system
 */
export type OddsFormat = 'american' | 'decimal';

/**
 * Calibration parameters for Elo conversion
 * These are sport-specific and empirically tuned
 */
export interface EloCalibrationParams {
  /** Power transformation exponent (typically 0.25-0.5) */
  exponent: number;
  /** Minimum Elo rating in the system */
  eloMin: number;
  /** Maximum Elo rating in the system */
  eloMax: number;
}

/**
 * Default calibration parameters (starting point before calibration)
 */
export const DEFAULT_CALIBRATION: EloCalibrationParams = {
  exponent: 0.33, // Cube root
  eloMin: 1250,
  eloMax: 1750,
};

/**
 * Convert American odds to implied probability
 *
 * American odds work as follows:
 * - Positive (+500): Underdog. Probability = 100 / (odds + 100)
 * - Negative (-200): Favorite. Probability = |odds| / (|odds| + 100)
 *
 * @param odds American odds (e.g., +500, -200)
 * @returns Implied probability as decimal (0-1)
 *
 * @example
 * convertAmericanOddsToProbability(500)   // 0.1667 (16.67%)
 * convertAmericanOddsToProbability(-200)  // 0.6667 (66.67%)
 */
export function convertAmericanOddsToProbability(odds: number): number {
  if (odds === 0) {
    throw new Error('American odds cannot be zero');
  }

  if (odds > 0) {
    // Underdog: probability = 100 / (odds + 100)
    return 100 / (odds + 100);
  } else {
    // Favorite: probability = |odds| / (|odds| + 100)
    return Math.abs(odds) / (Math.abs(odds) + 100);
  }
}

/**
 * Convert decimal odds to implied probability
 *
 * Decimal odds (European format) are simpler:
 * Probability = 1 / decimal_odds
 *
 * @param odds Decimal odds (e.g., 6.0, 1.5)
 * @returns Implied probability as decimal (0-1)
 *
 * @example
 * convertDecimalOddsToProbability(6.0)  // 0.1667 (16.67%)
 * convertDecimalOddsToProbability(1.5)  // 0.6667 (66.67%)
 */
export function convertDecimalOddsToProbability(odds: number): number {
  if (odds <= 1) {
    throw new Error('Decimal odds must be greater than 1');
  }

  return 1 / odds;
}

/**
 * Normalize probabilities to sum to exactly 100%
 *
 * Bookmaker odds include "vig" (vigorish/juice), so raw probabilities
 * typically sum to >100%. This function removes the vig proportionally.
 *
 * @param probabilities Array of probabilities (as decimals 0-1)
 * @returns Normalized probabilities that sum to 1.0
 *
 * @example
 * normalizeProbabilities([0.55, 0.50])  // [0.5238, 0.4762] (was 105%, now 100%)
 */
export function normalizeProbabilities(probabilities: number[]): number[] {
  const sum = probabilities.reduce((acc, p) => acc + p, 0);

  if (sum === 0) {
    throw new Error('Cannot normalize probabilities that sum to zero');
  }

  return probabilities.map(p => p / sum);
}

/**
 * Decompress championship probability to single-game strength
 *
 * Championship futures are "compressed" because they represent winning
 * multiple games. We apply a power transformation to decompress them
 * back to relative single-game strength.
 *
 * The exponent is empirically calibrated to match actual game betting lines.
 *
 * @param championshipProb Championship win probability (0-1)
 * @param exponent Power transformation exponent (typically 0.25-0.5)
 * @returns Decompressed strength value
 *
 * @example
 * decompressProbability(0.154, 0.33)  // 2.49 (Colorado at 15.4%)
 * decompressProbability(0.0066, 0.33) // 0.87 (NY Islanders at 0.66%)
 */
export function decompressProbability(
  championshipProb: number,
  exponent: number = DEFAULT_CALIBRATION.exponent
): number {
  if (championshipProb < 0 || championshipProb > 1) {
    throw new Error('Championship probability must be between 0 and 1');
  }

  if (exponent <= 0) {
    throw new Error('Exponent must be positive');
  }

  // Convert to percentage (0-100) for more intuitive scaling
  const percentage = championshipProb * 100;

  // Apply power transformation
  return Math.pow(percentage, exponent);
}

/**
 * Map decompressed strength value to Elo rating scale
 *
 * Takes the decompressed strength values and normalizes them to
 * the Elo scale (typically 1250-1750 for NHL/NFL).
 *
 * @param strength Decompressed strength value
 * @param minStrength Minimum strength across all teams
 * @param maxStrength Maximum strength across all teams
 * @param params Calibration parameters (Elo min/max)
 * @returns Elo rating
 *
 * @example
 * mapToElo(2.49, 0.5, 3.0, {eloMin: 1250, eloMax: 1750})  // 1648
 */
export function mapToElo(
  strength: number,
  minStrength: number,
  maxStrength: number,
  params: Pick<EloCalibrationParams, 'eloMin' | 'eloMax'> = DEFAULT_CALIBRATION
): number {
  if (maxStrength <= minStrength) {
    throw new Error('Max strength must be greater than min strength');
  }

  if (strength < minStrength || strength > maxStrength) {
    // Allow slight rounding errors
    if (Math.abs(strength - minStrength) < 0.001) {
      strength = minStrength;
    } else if (Math.abs(strength - maxStrength) < 0.001) {
      strength = maxStrength;
    } else {
      throw new Error('Strength must be between min and max strength');
    }
  }

  // Normalize to 0-1 range
  const normalized = (strength - minStrength) / (maxStrength - minStrength);

  // Map to Elo scale
  return params.eloMin + (normalized * (params.eloMax - params.eloMin));
}

/**
 * Calculate win probability from Elo ratings
 *
 * Uses standard Elo formula with 400-point scaling factor.
 * A 400-point difference means the stronger team has ~90.9% win probability.
 *
 * @param eloA Elo rating of team A
 * @param eloB Elo rating of team B
 * @returns Probability that team A wins (0-1)
 *
 * @example
 * eloWinProbability(1648, 1324)  // 0.828 (82.8%)
 * eloWinProbability(1500, 1500)  // 0.5 (50%)
 */
export function eloWinProbability(eloA: number, eloB: number): number {
  return 1 / (1 + Math.pow(10, (eloB - eloA) / 400));
}

/**
 * Convert futures odds to Elo ratings for all participants
 *
 * Complete pipeline:
 * 1. Convert odds to probabilities
 * 2. Normalize probabilities (remove vig)
 * 3. Decompress to strength values
 * 4. Map to Elo scale
 *
 * @param futuresOdds Array of {participantId, odds} objects
 * @param oddsFormat Format of the odds ('american' or 'decimal')
 * @param params Calibration parameters
 * @returns Map of participantId to Elo rating
 *
 * @example
 * const odds = [
 *   { participantId: '1', odds: 550 },   // Colorado +550
 *   { participantId: '2', odds: 15000 }  // NY Islanders +15000
 * ];
 * const elos = convertFuturesToElo(odds, 'american');
 * // Map { '1' => 1648, '2' => 1324 }
 */
export function convertFuturesToElo(
  futuresOdds: Array<{ participantId: string; odds: number }>,
  oddsFormat: OddsFormat = 'american',
  params: EloCalibrationParams = DEFAULT_CALIBRATION
): Map<string, number> {
  if (futuresOdds.length === 0) {
    return new Map();
  }

  // Step 1: Convert odds to probabilities
  const converter = oddsFormat === 'american'
    ? convertAmericanOddsToProbability
    : convertDecimalOddsToProbability;

  const probabilities = futuresOdds.map(({ odds }) => converter(odds));

  // Step 2: Normalize (remove vig)
  const normalized = normalizeProbabilities(probabilities);

  // Step 3: Decompress to strength values
  const strengths = normalized.map(p => decompressProbability(p, params.exponent));

  // Step 4: Find min/max for normalization
  const minStrength = Math.min(...strengths);
  const maxStrength = Math.max(...strengths);

  // Step 5: Map to Elo scale
  const eloRatings = new Map<string, number>();

  futuresOdds.forEach(({ participantId }, index) => {
    const elo = mapToElo(strengths[index], minStrength, maxStrength, params);
    eloRatings.set(participantId, Math.round(elo)); // Round to integer
  });

  return eloRatings;
}

/**
 * Calculate prediction error for calibration
 *
 * Compares predicted win probabilities against actual betting lines
 * to measure calibration accuracy.
 *
 * @param predicted Predicted probabilities
 * @param actual Actual betting line probabilities
 * @returns Object with mean absolute error and max error
 *
 * @example
 * calculatePredictionError([0.828, 0.565], [0.730, 0.630])
 * // { meanError: 0.081, maxError: 0.098 }
 */
export function calculatePredictionError(
  predicted: number[],
  actual: number[]
): { meanError: number; maxError: number } {
  if (predicted.length !== actual.length) {
    throw new Error('Predicted and actual arrays must have same length');
  }

  const errors = predicted.map((p, i) => Math.abs(p - actual[i]));
  const meanError = errors.reduce((sum, e) => sum + e, 0) / errors.length;
  const maxError = Math.max(...errors);

  return { meanError, maxError };
}
