// Substat roll validation (the accuracy trick yas / Genshin Optimizer use).
// Every artifact substat value is the SUM of discrete per-roll increments: a
// substat rolls once when it appears and again at every +4 level, up to 6 rolls
// total. So a displayed value is only legitimate if it equals round(sum of N
// rolls) for some N in 1..6 from that stat's roll table. An OCR value that fits
// no combination is a misread (e.g. a dropped/extra digit) and should go to
// review instead of being stored as fact.
//
// Roll tables are keyed by the app's display stat names. 5-star values are the
// full set; 4-star values are included for the stats used to tell rarities
// apart. Flat 4-star tables are intentionally omitted (kept conservative).

const MAX_ROLLS = 6;

const ROLLS_5STAR: Record<string, number[]> = {
  HP: [209.13, 239.0, 268.88, 298.75],
  ATK: [13.62, 15.56, 17.51, 19.45],
  DEF: [16.2, 18.52, 20.83, 23.15],
  "HP%": [4.08, 4.66, 5.25, 5.83],
  "ATK%": [4.08, 4.66, 5.25, 5.83],
  "DEF%": [5.1, 5.83, 6.56, 7.29],
  "Elemental Mastery": [16.32, 18.65, 20.98, 23.31],
  "Energy Recharge": [4.53, 5.18, 5.83, 6.48],
  "CRIT Rate": [2.72, 3.11, 3.5, 3.89],
  "CRIT DMG": [5.44, 6.22, 6.99, 7.77],
};

const ROLLS_4STAR: Record<string, number[]> = {
  "HP%": [3.06, 3.5, 3.93, 4.37],
  "ATK%": [3.06, 3.5, 3.93, 4.37],
  "DEF%": [3.83, 4.37, 4.92, 5.47],
  "Elemental Mastery": [12.25, 13.99, 15.74, 17.48],
  "Energy Recharge": [3.4, 3.89, 4.37, 4.86],
  "CRIT Rate": [2.04, 2.33, 2.62, 2.91],
  "CRIT DMG": [4.08, 4.66, 5.25, 5.83],
};

const PERCENT_STATS = new Set([
  "HP%",
  "ATK%",
  "DEF%",
  "Energy Recharge",
  "CRIT Rate",
  "CRIT DMG",
]);

function isPercent(stat: string) {
  return PERCENT_STATS.has(stat);
}

function displayRound(value: number, percent: boolean) {
  return percent ? Math.round(value * 10) / 10 : Math.round(value);
}

// All displayed values reachable by summing 1..MAX_ROLLS rolls from `rolls`.
function buildValidSet(rolls: number[], percent: boolean): Set<number> {
  const results = new Set<number>();
  let sums = new Set<number>([0]);
  for (let n = 1; n <= MAX_ROLLS; n++) {
    const next = new Set<number>();
    for (const sum of sums) {
      for (const roll of rolls) next.add(Math.round((sum + roll) * 1000) / 1000);
    }
    sums = next;
    for (const sum of sums) results.add(displayRound(sum, percent));
  }
  return results;
}

const VALID_CACHE = new Map<string, Set<number>>();
const ROLL_COUNT_CACHE = new Map<string, Map<number, Set<number>>>();

function validSet(stat: string, table: Record<string, number[]>, tag: string): Set<number> | null {
  const rolls = table[stat];
  if (!rolls) return null;
  const key = `${tag}:${stat}`;
  let cached = VALID_CACHE.get(key);
  if (!cached) {
    cached = buildValidSet(rolls, isPercent(stat));
    VALID_CACHE.set(key, cached);
  }
  return cached;
}

function rollCountMap(stat: string, table: Record<string, number[]>, tag: string): Map<number, Set<number>> | null {
  const rolls = table[stat];
  if (!rolls) return null;
  const key = `${tag}:${stat}`;
  let cached = ROLL_COUNT_CACHE.get(key);
  if (cached) return cached;
  cached = new Map();
  let sums = new Set<number>([0]);
  for (let count = 1; count <= MAX_ROLLS; count++) {
    const next = new Set<number>();
    for (const sum of sums) {
      for (const roll of rolls) next.add(Math.round((sum + roll) * 1000) / 1000);
    }
    sums = next;
    for (const sum of sums) {
      const displayed = displayRound(sum, isPercent(stat));
      const counts = cached.get(displayed) ?? new Set<number>();
      counts.add(count);
      cached.set(displayed, counts);
    }
  }
  ROLL_COUNT_CACHE.set(key, cached);
  return cached;
}

export function parseSubstatEntry(entry: string): { stat: string; value: number; percent: boolean } | null {
  const plusIndex = entry.indexOf("+");
  if (plusIndex <= 0) return null;
  const stat = entry.slice(0, plusIndex).trim();
  const raw = entry.slice(plusIndex + 1).replace(/,/g, "").replace("%", "").trim();
  const value = Number.parseFloat(raw);
  if (!Number.isFinite(value)) return null;
  return { stat, value, percent: entry.includes("%") };
}

/** A displayed value is plausible if it matches a roll sum for 5-star or 4-star. */
export function isPlausibleSubstatValue(stat: string, value: number, rarity?: 4 | 5): boolean {
  const rounded = displayRound(value, isPercent(stat));
  const five = validSet(stat, ROLLS_5STAR, "5");
  const four = validSet(stat, ROLLS_4STAR, "4");
  // Unknown stat (no table) -> do not claim it is implausible.
  if (!five && !four) return true;
  if (rarity === 5) return Boolean(five?.has(rounded));
  if (rarity === 4) return Boolean(four?.has(rounded));
  return Boolean(five?.has(rounded)) || Boolean(four?.has(rounded));
}

export function isPlausibleSubstat(entry: string, rarity?: 4 | 5): boolean {
  const parsed = parseSubstatEntry(entry);
  if (!parsed) return true;
  return isPlausibleSubstatValue(parsed.stat, parsed.value, rarity);
}

export function possibleSubstatRollCounts(entry: string, rarity?: 4 | 5): number[] {
  const parsed = parseSubstatEntry(entry);
  if (!parsed) return [];
  const rounded = displayRound(parsed.value, isPercent(parsed.stat));
  const counts = new Set<number>();
  const maps = rarity === 5
    ? [rollCountMap(parsed.stat, ROLLS_5STAR, "5")]
    : rarity === 4
      ? [rollCountMap(parsed.stat, ROLLS_4STAR, "4")]
      : [
          rollCountMap(parsed.stat, ROLLS_5STAR, "5"),
          rollCountMap(parsed.stat, ROLLS_4STAR, "4"),
        ];
  for (const map of maps) {
    for (const count of map?.get(rounded) ?? []) counts.add(count);
  }
  return [...counts].sort((left, right) => left - right);
}

/** The substat entries whose value fits no legal roll combination. */
export function implausibleSubstats(substats: readonly string[], rarity?: 4 | 5): string[] {
  return substats.filter((entry) => !isPlausibleSubstat(entry, rarity));
}

/**
 * Best-effort rarity from level + which roll table the substats fit. Conservative:
 * only returns 4 when a substat clearly fits the 4-star table and not 5-star;
 * otherwise defaults to 5 (the common case and the previous hardcoded value).
 */
export function inferRarity(level: number, substats: readonly string[]): number {
  if (level > 16) return 5; // only 5-star artifacts level past +16
  let fitsFiveOnly = 0;
  let fitsFourOnly = 0;
  for (const entry of substats) {
    const parsed = parseSubstatEntry(entry);
    if (!parsed) continue;
    const rounded = displayRound(parsed.value, isPercent(parsed.stat));
    const five = validSet(parsed.stat, ROLLS_5STAR, "5");
    const four = validSet(parsed.stat, ROLLS_4STAR, "4");
    if (!five || !four) continue;
    const inFive = five.has(rounded);
    const inFour = four.has(rounded);
    if (inFive && !inFour) fitsFiveOnly++;
    else if (inFour && !inFive) fitsFourOnly++;
  }
  if (fitsFiveOnly > 0) return 5;
  if (fitsFourOnly > 0) return 4;
  return 5;
}
