const fs = require("fs");
const path = require("path");

const { repoRoot } = require("./meals");

const eloPath = path.join(repoRoot, "data", "elo.json");

function validateEloData(eloData) {
  if (!eloData || typeof eloData !== "object" || Array.isArray(eloData)) {
    throw new Error("data/elo.json must contain an object");
  }

  for (const field of ["defaultRating", "kFactor"]) {
    if (
      typeof eloData[field] !== "number" ||
      !Number.isFinite(eloData[field]) ||
      eloData[field] <= 0
    ) {
      throw new Error(`data/elo.json "${field}" must be a positive number`);
    }
  }

  if (!Array.isArray(eloData.entries)) {
    throw new Error('data/elo.json "entries" must be an array');
  }

  const ids = new Set();

  for (const [index, entry] of eloData.entries.entries()) {
    if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
      throw new Error(`Elo entry ${index} must be an object`);
    }

    if (typeof entry.id !== "string" || !/^\d+$/.test(entry.id)) {
      throw new Error(`Elo entry ${index} has an invalid id`);
    }

    if (ids.has(entry.id)) {
      throw new Error(`Duplicate Elo entry id "${entry.id}" found in data/elo.json`);
    }

    ids.add(entry.id);

    if (
      typeof entry.rating !== "number" ||
      !Number.isFinite(entry.rating) ||
      entry.rating <= 0
    ) {
      throw new Error(`Elo entry ${index} must have a positive numeric rating`);
    }

    for (const field of ["wins", "losses"]) {
      if (!Number.isInteger(entry[field]) || entry[field] < 0) {
        throw new Error(`Elo entry ${index} field "${field}" must be a non-negative integer`);
      }
    }
  }
}

function loadEloData() {
  const eloData = JSON.parse(fs.readFileSync(eloPath, "utf8"));

  validateEloData(eloData);

  return eloData;
}

function saveEloData(eloData) {
  validateEloData(eloData);
  fs.writeFileSync(eloPath, `${JSON.stringify(eloData, null, 2)}\n`);
}

function createDefaultEntry(id, defaultRating) {
  return {
    id,
    rating: defaultRating,
    wins: 0,
    losses: 0,
  };
}

function syncEloWithMeals(meals) {
  const eloData = loadEloData();
  const entryById = new Map(eloData.entries.map((entry) => [entry.id, entry]));
  const syncedData = {
    ...eloData,
    entries: meals.map((meal) => {
      const existingEntry = entryById.get(meal.id);
      return existingEntry
        ? { ...existingEntry }
        : createDefaultEntry(meal.id, eloData.defaultRating);
    }),
  };

  if (JSON.stringify(syncedData) !== JSON.stringify(eloData)) {
    saveEloData(syncedData);
  }

  return syncedData;
}

function getEloAlignmentReport(meals, eloData) {
  const mealIds = new Set(meals.map((meal) => meal.id));
  const eloIds = new Set(eloData.entries.map((entry) => entry.id));

  return {
    missingEntryIds: meals
      .map((meal) => meal.id)
      .filter((mealId) => !eloIds.has(mealId)),
    unexpectedEntryIds: eloData.entries
      .map((entry) => entry.id)
      .filter((entryId) => !mealIds.has(entryId)),
  };
}

function compareRankedMeals(left, right) {
  if (right.rating !== left.rating) {
    return right.rating - left.rating;
  }

  if (right.matches !== left.matches) {
    return right.matches - left.matches;
  }

  return Number.parseInt(left.id, 10) - Number.parseInt(right.id, 10);
}

function getRankedMeals(meals, eloData) {
  const entryById = new Map(eloData.entries.map((entry) => [entry.id, entry]));

  return meals
    .map((meal) => {
      const entry = entryById.get(meal.id) || createDefaultEntry(meal.id, eloData.defaultRating);
      const matches = entry.wins + entry.losses;

      return {
        ...meal,
        rating: entry.rating,
        wins: entry.wins,
        losses: entry.losses,
        matches,
      };
    })
    .sort(compareRankedMeals);
}

module.exports = {
  eloPath,
  getEloAlignmentReport,
  getRankedMeals,
  loadEloData,
  saveEloData,
  syncEloWithMeals,
};
