/* eslint-disable @typescript-eslint/no-unused-vars */
import {
  SpotGame,
  SpotPlayer,
  RangeGame,
  RangePlayer,
  PotDistribution,
  AllocationPercentages,
  SpotPlayerVarianceResult,
  RangePlayerVarianceResult,
  ComputedData,
} from './types';
import {
  DEFAULT_RANGE_ALLOCATION_PERCENTAGES,
  DEFAULT_SPOT_ALLOCATION_PERCENTAGES,
  serializeRangePlayer,
  serializeSpotPlayer,
} from './utils';

// RANGE CALCULATION
/**
 * Calculates the pot distribution for a game based on players' contributions
 * and given allocation percentages.
 *
 * @param players - List of players participating in the game
 * @param allocations - Optional custom allocation percentages (defaults to predefined values)
 * @returns An object containing the breakdown of the total pot into different allocations
 */
function calculateRangePotDistribution(
  data: RangePlayer[],
  allocations: AllocationPercentages = DEFAULT_RANGE_ALLOCATION_PERCENTAGES
): PotDistribution {
  // If there are no players, return an empty result immediately
  if (data.length === 0) {
    return {
      total_pot: 0,
      metafide_rake: 0,
      streak_pot_5: 0,
      streak_pot_10: 0,
      streak_pot_25: 0,
      daily_reward_pot: 0,
      monthly_reward_pot: 0,
      burn: 0,
      winning_pot: 0,
    };
  }

  const players = data.map(serializeRangePlayer);
  const total_pot = players.reduce((sum, player) => sum + player.f, 0);

  if (data.length === 1) {
    return {
      total_pot: total_pot,
      metafide_rake: 0,
      streak_pot_5: 0,
      streak_pot_10: 0,
      streak_pot_25: 0,
      daily_reward_pot: 0,
      monthly_reward_pot: 0,
      burn: 0,
      winning_pot: total_pot,
    };
  }

  // Compute all allocations using the provided percentages
  const metafide_rake = parseFloat(
    (total_pot * allocations.metafide_rake).toFixed(2)
  );
  const streak_pot_5 = parseFloat(
    (total_pot * allocations.streak_pot_5).toFixed(2)
  );
  const streak_pot_10 = parseFloat(
    (total_pot * allocations.streak_pot_10).toFixed(2)
  );
  const streak_pot_25 = parseFloat(
    (total_pot * allocations.streak_pot_25).toFixed(2)
  );
  const daily_reward_pot = parseFloat(
    (total_pot * allocations.daily_reward_pot).toFixed(2)
  );
  const monthly_reward_pot = parseFloat(
    (total_pot * allocations.monthly_reward_pot).toFixed(2)
  );
  const burn = parseFloat((total_pot * allocations.burn).toFixed(2));

  // Sum all deductions
  const totalDeductions =
    metafide_rake +
    streak_pot_5 +
    streak_pot_10 +
    streak_pot_25 +
    daily_reward_pot +
    monthly_reward_pot +
    burn;

  // Compute the winning pot (remaining after deductions)
  const winning_pot = parseFloat((total_pot - totalDeductions).toFixed(2));

  // Return a structured breakdown
  return {
    total_pot: parseFloat(total_pot.toFixed(2)),
    metafide_rake,
    streak_pot_5,
    streak_pot_10,
    streak_pot_25,
    daily_reward_pot,
    monthly_reward_pot,
    burn,
    winning_pot,
  };
}

/**
 * Computes the variance between players' predicted price ranges and the actual game price range.
 *
 * @param players - List of players participating in the game
 * @param game - The game data including actual price boundaries and timestamps
 * @returns An array of PlayerVarianceResult containing each player's variance data and total variance sum
 */
function computeRangePlayerVariance(
  players: RangePlayer[],
  game: RangeGame
): RangePlayerVarianceResult[] {
  // If there are no players, return an empty result immediately
  if (players.length === 0) return [];

  // Prepare a simplified game data object for easy access
  const rangeGameData: Record<string, number> = {
    end_timestamp: Math.floor(new Date(game.t3).getTime() / 1000),
    high_actual: game.ha as number, // high actual price (guaranteed non-null)
    low_actual: game.la as number, // low actual price (guaranteed non-null)
    gid: game.gid, // game ID
  };

  // Calculate each player's variance based on their predictions vs actual prices
  const varianceData = players.map(p => {
    let total_variance = 0;

    // Only compute variance if the player's predicted high is above actual high
    // AND predicted low is below actual low (player's range fully covers the actual range)
    if (p.hp > rangeGameData.high_actual && p.lp < rangeGameData.low_actual) {
      total_variance = Math.pow(
        Math.max(p.hp - rangeGameData.high_actual, 0) +
          Math.max(rangeGameData.low_actual - p.lp, 0),
        1.5
      );
    }

    // Return the basic variance data for this player
    return {
      ...p,
      varianceInput: {
        high_actual: rangeGameData.high_actual,
        low_actual: rangeGameData.low_actual,
        total_variance, // computed variance
      },
    };
  });

  // Compute the total variance across all players
  const sumTotalVariance = varianceData.reduce(
    (sum, v) => sum + v.varianceInput.total_variance,
    0
  );

  // Merge the total variance into each player's result and return the final array
  return varianceData.map(v => ({
    ...v,
    varianceInput: {
      high_actual: v.varianceInput.high_actual,
      low_actual: v.varianceInput.low_actual,
      total_variance: v.varianceInput.total_variance,
      sum_total_variance: parseFloat(sumTotalVariance.toFixed(6)),
    },
  }));
}

/**
 * Handles the full computation of player winnings based on their variance and the game's pot distribution.
 *
 * @param players - List of players participating in the game
 * @param games - The game data including actual price boundaries and timestamps
 * @param distribution - The Range Game Distribution
 * @returns An object containing detailed winnings per player, highest winnings, and largest returns
 */
function computeRangeWinnings(
  data: RangePlayer[],
  games: RangeGame,
  distribution: PotDistribution
): ComputedData {
  // If there are no players, immediately return empty results
  if (data.length === 0) {
    return {
      players: [],
      streak: {
        winnings: [],
        returns: [],
      },
    };
  }

  // If only one player, assign full winnings and return 1
  if (data.length === 1) {
    const player = serializeRangePlayer(data[0]);
    return {
      players: [
        {
          ...player,
          w: player.f,
          r: 1,
        },
      ],
      streak: {
        winnings: [],
        returns: [],
      },
    };
  }

  // Step 1: Compute total pot distribution and player variances
  const players = data.map(serializeRangePlayer);
  const computedPlayers = computeRangePlayerVariance(players, games);

  // Step 2: Calculate the total sum of variances
  const sumTotalVariance = computedPlayers.reduce(
    (sum, player) => sum + player.varianceInput.total_variance,
    0
  );

  // Step 4: Handle the special case where no player has any variance
  if (sumTotalVariance === 0) {
    return {
      players: computedPlayers.map(player => ({
        ...player,
        w: 0,
        r: 0,
      })),
      streak: {
        winnings: [],
        returns: [],
      },
    };
  }

  // Step 5: Precompute the sum of (shareTokensToVariance) for normalization
  const sumShareTokensToVariance = computedPlayers.reduce((sum, player) => {
    if (player.varianceInput.total_variance === 0) return sum; // Ignore players with zero variance
    const shareVariance =
      player.varianceInput.total_variance / sumTotalVariance;
    const shareTokens = player.f / distribution.total_pot;
    const shareTokensToVariance = shareTokens / shareVariance;
    return sum + shareTokensToVariance;
  }, 0);

  // Step 6: Compute each player's normalized winnings and percent return
  const updatedPlayers = computedPlayers.map(player => {
    if (player.varianceInput.total_variance === 0) {
      return { ...player, w: 0, r: 0 };
    }

    const shareVariance =
      player.varianceInput.total_variance / sumTotalVariance;
    const shareTokens = player.f / distribution.total_pot;
    const shareTokensToVariance = shareTokens / shareVariance;
    const normalizedTokensToVariance =
      shareTokensToVariance / sumShareTokensToVariance;

    // Raw winnings in system's units, then converted down
    const winningsRaw = normalizedTokensToVariance * distribution.winning_pot;

    // Percent return relative to player's initial stake
    const percentReturn = ((winningsRaw - player.f) / player.f) * 100;

    const { varianceInput, ...rest } = player;
    return { ...rest, w: winningsRaw, r: percentReturn };
  });

  // Step 7: Determine the number of top players (2% of total players, rounded up)
  const totalPlayers = updatedPlayers.length;
  const topPlayersCount = Math.ceil(totalPlayers * 0.02);

  // Step 8: Find top players by highest winnings
  const highestWinnings = [...updatedPlayers]
    .sort((a, b) => b.w - a.w) // Sort descending by winnings
    .slice(0, topPlayersCount);

  // Step 9: Find top players by highest percent return
  const largestReturns = [...updatedPlayers]
    .sort((a, b) => b.r - a.r) // Sort descending by percent return
    .slice(0, topPlayersCount);

  // Step 10: Return the full computation result
  return {
    players: updatedPlayers,
    streak: {
      winnings: highestWinnings,
      returns: largestReturns,
    },
  };
}

// SPOT COMPUTATION
function calculateSpotPotDistribution(
  data: SpotPlayer[],
  allocations: AllocationPercentages = DEFAULT_SPOT_ALLOCATION_PERCENTAGES
): PotDistribution {
  if (data.length === 0) {
    return {
      total_pot: 0,
      metafide_rake: 0,
      range_rake: 0,
      streak_pot_5: 0,
      streak_pot_10: 0,
      streak_pot_25: 0,
      daily_reward_pot: 0,
      monthly_reward_pot: 0,
      burn: 0,
      winning_pot: 0,
    };
  }

  const players = data.map(serializeSpotPlayer);
  const total_pot = players.reduce((sum, player) => sum + player.f, 0);

  if (data.length === 1) {
    return {
      total_pot: total_pot,
      metafide_rake: 0,
      range_rake: 0,
      streak_pot_5: 0,
      streak_pot_10: 0,
      streak_pot_25: 0,
      daily_reward_pot: 0,
      monthly_reward_pot: 0,
      burn: 0,
      winning_pot: total_pot,
    };
  }

  const metafide_rake = parseFloat(
    (total_pot * allocations.metafide_rake).toFixed(2)
  );
  const streak_pot_5 = parseFloat(
    (total_pot * allocations.streak_pot_5).toFixed(2)
  );
  const streak_pot_10 = parseFloat(
    (total_pot * allocations.streak_pot_10).toFixed(2)
  );
  const streak_pot_25 = parseFloat(
    (total_pot * allocations.streak_pot_25).toFixed(2)
  );
  const daily_reward_pot = parseFloat(
    (total_pot * allocations.daily_reward_pot).toFixed(2)
  );
  const monthly_reward_pot = parseFloat(
    (total_pot * allocations.monthly_reward_pot).toFixed(2)
  );
  const burn = parseFloat((total_pot * allocations.burn).toFixed(2));

  const range_rake = allocations.range_rake
    ? parseFloat((total_pot * allocations.range_rake).toFixed(2))
    : 0;

  // Sum all deductions including optional range rake
  const totalDeductions =
    metafide_rake +
    streak_pot_5 +
    streak_pot_10 +
    streak_pot_25 +
    daily_reward_pot +
    monthly_reward_pot +
    burn +
    range_rake;

  const winning_pot = parseFloat((total_pot - totalDeductions).toFixed(2));

  return {
    total_pot: parseFloat(total_pot.toFixed(2)),
    metafide_rake,
    streak_pot_5,
    streak_pot_10,
    streak_pot_25,
    daily_reward_pot,
    monthly_reward_pot,
    burn,
    winning_pot,
    range_rake,
  };
}
/**
 * Computes the spot variance between players' predicted price ranges and the actual game price range.
 *
 * @param players - List of players participating in the game
 * @param game - The game data including actual price boundaries and timestamps
 * @returns An array of PlayerVarianceResult containing each player's variance data and total variance sum
 */
function computeSpotPlayerVariance(
  players: SpotPlayer[],
  game: SpotGame
): SpotPlayerVarianceResult[] {
  if (players.length === 0) return [];

  const closedTimestamp = Number(game.t3); // Already in seconds
  const actualPrice = Number(game.ca);

  const spotVariance = players.map(player => {
    const spotTimestamp = Math.floor(Number(player.t) / 1000);
    const timestampDiff = closedTimestamp - spotTimestamp;
    const absSpotVar = Math.abs(actualPrice - Number(player.sp));
    const tokenMilliseconds =
      Math.pow(51250 - timestampDiff, 1.15) * Number(player.f);

    return {
      ...player,
      varianceInput: {
        actual_price: actualPrice,
        abs_spot_var: absSpotVar,
        token_milliseconds: tokenMilliseconds,
      },
    };
  });

  const sumAbsSpotVar = spotVariance.reduce(
    (sum, p) => sum + p.varianceInput.abs_spot_var,
    0
  );
  const sumTokenMilliseconds = spotVariance.reduce(
    (sum, p) => sum + p.varianceInput.token_milliseconds,
    0
  );

  const shareVarianceData = spotVariance.map(p => {
    const shareTkmToVar =
      p.varianceInput.abs_spot_var === 0 || sumAbsSpotVar === 0
        ? 0
        : p.varianceInput.token_milliseconds /
          sumTokenMilliseconds /
          (p.varianceInput.abs_spot_var / sumAbsSpotVar);

    return {
      ...p,
      varianceInput: {
        actual_price: p.varianceInput.actual_price,
        abs_spot_var: p.varianceInput.abs_spot_var,
        token_milliseconds: p.varianceInput.token_milliseconds,
        share_tkm_to_var: shareTkmToVar,
      },
    };
  });

  const sumShareTkmToVar = shareVarianceData.reduce(
    (sum, p) => sum + Number(p.varianceInput.share_tkm_to_var),
    0
  );

  return shareVarianceData.map(p => ({
    ...p,
    varianceInput: {
      actual_price: p.varianceInput.actual_price,
      abs_spot_var: p.varianceInput.abs_spot_var,
      token_milliseconds: p.varianceInput.token_milliseconds,
      share_tkm_to_var: p.varianceInput.share_tkm_to_var,
      sum_share_tkm_to_var: sumShareTkmToVar,
    },
  }));
}

/**
 * Computes the final spot game results for players, including
 * winnings, top winners, and highest returns based on their predictions.
 *
 * @param data - List of SpotPlayer entries participating in the game
 * @param game - SpotGame containing game closing data
 * @param distribution - The SpotGame distribution
 * @returns An object containing player winnings, highest winnings, and largest returns
 */
function computeSpotWinnings(
  data: SpotPlayer[],
  game: SpotGame,
  distribution: PotDistribution
): ComputedData {
  // If no players, immediately return empty results
  if (data.length === 0) {
    return {
      players: [],
      streak: {
        winnings: [],
        returns: [],
      },
    };
  }

  // If only one player, assign full winnings and return 1
  if (data.length === 1) {
    const player = serializeSpotPlayer(data[0]);
    return {
      players: [
        {
          ...player,
          w: player.f,
          r: 1,
        },
      ],
      streak: {
        winnings: [],
        returns: [],
      },
    };
  }

  // Step 1: Calculate total pot distribution and each player's variance and share based on spot prediction
  const players = data.map(serializeSpotPlayer);
  const computedPlayers = computeSpotPlayerVariance(players, game);

  // Step 3: Calculate winnings and percent return for each player
  const updatedPlayers = computedPlayers.map(player => {
    // Calculate normalized share of time/variance
    const shareTotalTimeVariance =
      player.varianceInput.sum_share_tkm_to_var > 0
        ? player.varianceInput.share_tkm_to_var /
          player.varianceInput.sum_share_tkm_to_var
        : 0;
    const winningsRaw = shareTotalTimeVariance * distribution.winning_pot;
    const percentReturn =
      player.f > 0 ? ((winningsRaw - player.f) / player.f) * 100 : 0;

    const { varianceInput, ...rest } = player;
    return {
      ...rest,
      w: winningsRaw,
      r: percentReturn,
    };
  });

  // Step 4: Identify top 2% of players by winnings
  const totalPlayers = updatedPlayers.length;
  const topPlayersCount = Math.max(1, Math.ceil(totalPlayers * 0.02)); // Always at least 1 player
  const highestWinnings = [...updatedPlayers]
    .sort((a, b) => b.w - a.w)
    .slice(0, topPlayersCount);
  // Step 5: Identify top 2% of players by percent return
  const largestReturns = [...updatedPlayers]
    .sort((a, b) => b.r - a.r)
    .slice(0, topPlayersCount);

  // Step 6: Return results
  return {
    players: updatedPlayers,
    streak: {
      winnings: highestWinnings,
      returns: largestReturns,
    },
  };
}

export {
  computeRangeWinnings,
  computeSpotWinnings,
  computeRangePlayerVariance,
  computeSpotPlayerVariance,
  calculateSpotPotDistribution,
  calculateRangePotDistribution,
};
