export const minimumCorrectedProbabilityPercent = 0.0001;

const ppmScale = 1_000_000;

export function correctRTPProbabilities(tiers, targetRTPPercent, options = {}) {
    const enabled = options.enabled || ((item) => item?.enabled !== false);
    const multiplier = options.multiplier || ((item) => item?.multiplier);
    const participatesInRTP = options.participatesInRTP || (() => true);
    const fallback = options.fallback || "unchanged";
    const targetRTP = decimal(targetRTPPercent);
    const enabledTiers = (tiers || [])
        .map((item, index) => ({ index, multiplier: decimal(multiplier(item, index)), tier: item }))
        .filter((item) => enabled(item.tier, item.index));
    const baseTiers = enabledTiers.filter(
        (item) => participatesInRTP(item.tier, item.index) && Number.isFinite(item.multiplier),
    );
    const positiveTiers = baseTiers
        .filter((item) => item.multiplier > 0)
        .sort((left, right) => left.multiplier - right.multiplier);
    const zeroTiers = baseTiers.filter((item) => item.multiplier === 0);

    if (enabledTiers.length === 0) {
        return new Map();
    }
    if (positiveTiers.length === 0) {
        return fallback === "equal" ? equalProbabilities(enabledTiers) : null;
    }

    const probabilityByIndex = baselineProbabilities(enabledTiers, {
        baseTiers,
        baselineProbability: options.baselineProbability,
        positiveTiers,
    });
    const floorWeights = floorWeightsFromProbabilities(probabilityByIndex);
    const fixedProbability = sumProbabilities(probabilityByIndex);
    if (fixedProbability >= 100) {
        return probabilityByIndex;
    }

    const fixedExpectedRTP = positiveTiers.reduce(
        (sum, item) => sum + item.multiplier * (probabilityByIndex.get(item.index) || 0),
        0,
    );
    const remainingProbability = 100 - fixedProbability;
    const targetExtraRTP = targetRTP - fixedExpectedRTP;
    const contributionPlan = balancedRTPContributionPlan(positiveTiers, targetExtraRTP, remainingProbability);

    addContributionProbabilities(probabilityByIndex, contributionPlan);
    assignResidualProbability(probabilityByIndex, zeroTiers, positiveTiers);
    alignExpectedRTPByWeightPPM(probabilityByIndex, baseTiers, targetRTP, floorWeights);
    return probabilityByIndex;
}

export function applyProbabilityMap(tiers, probabilityByIndex, format = formatDecimal) {
    if (!probabilityByIndex) {
        return tiers;
    }
    return (tiers || []).map((item, index) => ({
        ...item,
        probabilityPercent: format(probabilityByIndex.get(index) || 0),
    }));
}

export function probabilityTotal(tiers, options = {}) {
    const enabled = options.enabled || ((item) => item?.enabled !== false);
    return (tiers || []).reduce((sum, item, index) => {
        if (!enabled(item, index)) {
            return sum;
        }
        return sum + decimal(options.probability ? options.probability(item, index) : item?.probabilityPercent);
    }, 0);
}

export function expectedRTPPercent(tiers, options = {}) {
    const enabled = options.enabled || ((item) => item?.enabled !== false);
    const multiplier = options.multiplier || ((item) => item?.multiplier);
    const participatesInRTP = options.participatesInRTP || (() => true);
    return (tiers || []).reduce((sum, item, index) => {
        if (!enabled(item, index) || !participatesInRTP(item, index)) {
            return sum;
        }
        return sum + expectedContributionPercent(multiplier(item, index), item?.probabilityPercent);
    }, 0);
}

export function tierRTPContributionPercent(tier, options = {}) {
    const enabled = options.enabled || ((item) => item?.enabled !== false);
    const multiplier = options.multiplier || ((item) => item?.multiplier);
    const participatesInRTP = options.participatesInRTP || (() => true);
    if (!enabled(tier, 0) || !participatesInRTP(tier, 0)) {
        return 0;
    }
    return expectedContributionPercent(multiplier(tier, 0), tier?.probabilityPercent);
}

export function percentToWeightPPM(value) {
    return Math.round(decimal(value) * 10_000);
}

export function formatDecimal(value) {
    const number = Number(value || 0);
    if (!Number.isFinite(number)) {
        return "0";
    }
    return Number.isInteger(number) ? String(number) : number.toFixed(6).replace(/\.?0+$/, "");
}

export function decimal(value) {
    const number = Number(value || 0);
    return Number.isFinite(number) ? number : 0;
}

function equalProbabilities(enabledTiers) {
    const probabilities = new Map();
    const base = Math.floor(ppmScale / enabledTiers.length);
    let remainder = ppmScale - base * enabledTiers.length;
    for (const item of enabledTiers) {
        const ppm = base + (remainder > 0 ? 1 : 0);
        remainder -= 1;
        probabilities.set(item.index, roundPercent(ppm / 10_000));
    }
    return probabilities;
}

function baselineProbabilities(enabledTiers, context) {
    const probabilities = new Map();
    const defaultFloor =
        enabledTiers.length * minimumCorrectedProbabilityPercent <= 100
            ? minimumCorrectedProbabilityPercent
            : 100 / enabledTiers.length;
    for (const item of enabledTiers) {
        const customFloor = context.baselineProbability?.(item.tier, item.index, context);
        const floor = Math.max(defaultFloor, decimal(customFloor || 0));
        probabilities.set(item.index, roundPercent(floor));
    }
    return sumProbabilities(probabilities) <= 100 ? probabilities : equalProbabilities(enabledTiers);
}

function floorWeightsFromProbabilities(probabilities) {
    const weights = new Map();
    for (const [index, probability] of probabilities.entries()) {
        weights.set(index, percentToWeightPPM(probability));
    }
    return weights;
}

function balancedRTPContributionPlan(positiveTiers, targetExtraRTP, remainingProbability) {
    if (targetExtraRTP <= 0) {
        return [];
    }
    const maxMultiplier = positiveTiers[positiveTiers.length - 1].multiplier;
    if (targetExtraRTP >= remainingProbability * maxMultiplier) {
        return [{ tier: positiveTiers[positiveTiers.length - 1], probability: remainingProbability }];
    }
    const alpha = minimumFeasibleContributionAlpha(positiveTiers, targetExtraRTP, remainingProbability);
    const weighted = contributionWeights(positiveTiers, alpha);
    return positiveTiers.map((tier, index) => ({
        tier,
        probability: roundPercent((targetExtraRTP * weighted[index]) / tier.multiplier),
    }));
}

function minimumFeasibleContributionAlpha(positiveTiers, targetExtraRTP, remainingProbability) {
    if (contributionProbabilityTotal(positiveTiers, targetExtraRTP, 0) <= remainingProbability) {
        return 0;
    }
    let low = 0;
    let high = 1;
    while (contributionProbabilityTotal(positiveTiers, targetExtraRTP, high) > remainingProbability && high < 64) {
        high *= 2;
    }
    for (let step = 0; step < 48; step += 1) {
        const mid = (low + high) / 2;
        if (contributionProbabilityTotal(positiveTiers, targetExtraRTP, mid) > remainingProbability) {
            low = mid;
        } else {
            high = mid;
        }
    }
    return high;
}

function contributionProbabilityTotal(positiveTiers, targetExtraRTP, alpha) {
    const weights = contributionWeights(positiveTiers, alpha);
    return positiveTiers.reduce((sum, tier, index) => sum + (targetExtraRTP * weights[index]) / tier.multiplier, 0);
}

function contributionWeights(positiveTiers, alpha) {
    const rawWeights = positiveTiers.map((tier) => Math.max(0, tier.multiplier) ** alpha);
    const total = rawWeights.reduce((sum, value) => sum + value, 0);
    if (total <= 0) {
        return positiveTiers.map(() => 1 / positiveTiers.length);
    }
    return rawWeights.map((value) => value / total);
}

function addContributionProbabilities(probabilities, contributionPlan) {
    for (const item of contributionPlan) {
        probabilities.set(item.tier.index, roundPercent((probabilities.get(item.tier.index) || 0) + item.probability));
    }
}

function assignResidualProbability(probabilities, zeroTiers, positiveTiers) {
    const residual = roundPercent(100 - sumProbabilities(probabilities));
    if (residual <= 0) {
        normalizeProbabilityTotal(probabilities, zeroTiers[0] || positiveTiers[0]);
        return;
    }
    const receiver = zeroTiers[0] || positiveTiers[0];
    if (!receiver) {
        return;
    }
    probabilities.set(receiver.index, roundPercent((probabilities.get(receiver.index) || 0) + residual));
    normalizeProbabilityTotal(probabilities, receiver);
}

function normalizeProbabilityTotal(probabilities, receiver) {
    if (!receiver) {
        return;
    }
    const diff = roundPercent(100 - sumProbabilities(probabilities));
    if (diff !== 0) {
        probabilities.set(receiver.index, roundPercent((probabilities.get(receiver.index) || 0) + diff));
    }
}

function alignExpectedRTPByWeightPPM(probabilities, baseTiers, targetRTP, floorWeights) {
    const targetRTPPPM = percentToWeightPPM(targetRTP);
    const weights = new Map();
    for (const [index, probability] of probabilities.entries()) {
        weights.set(index, percentToWeightPPM(probability));
    }
    normalizeWeightTotal(weights, baseTiers[0]);
    let currentRTPPPM = expectedRTPPPMByWeights(weights, baseTiers);

    for (let step = 0; step < 20_000 && currentRTPPPM !== targetRTPPPM; step += 1) {
        const diff = targetRTPPPM - currentRTPPPM;
        const move = bestExpectedRTPMove(weights, baseTiers, diff, floorWeights);
        if (!move || Math.abs(diff - move.delta) >= Math.abs(diff)) {
            break;
        }
        weights.set(move.donor.index, (weights.get(move.donor.index) || 0) - 1);
        weights.set(move.receiver.index, (weights.get(move.receiver.index) || 0) + 1);
        currentRTPPPM += move.delta;
    }

    for (const [index, weight] of weights.entries()) {
        probabilities.set(index, roundPercent(weight / 10_000));
    }
}

function normalizeWeightTotal(weights, receiver) {
    if (!receiver) {
        return;
    }
    let total = 0;
    for (const weight of weights.values()) {
        total += weight;
    }
    const diff = ppmScale - total;
    if (diff !== 0) {
        weights.set(receiver.index, (weights.get(receiver.index) || 0) + diff);
    }
}

function bestExpectedRTPMove(weights, baseTiers, diff, floorWeights) {
    let best = null;
    for (const donor of baseTiers) {
        const donorWeight = weights.get(donor.index) || 0;
        const floorWeight = floorWeights.get(donor.index) ?? percentToWeightPPM(minimumCorrectedProbabilityPercent);
        if (donorWeight <= floorWeight) {
            continue;
        }
        for (const receiver of baseTiers) {
            if (receiver.index === donor.index) {
                continue;
            }
            const delta = expectedMoveDeltaPPM(weights, donor, receiver);
            if ((diff > 0 && delta <= 0) || (diff < 0 && delta >= 0)) {
                continue;
            }
            if (!best || betterMove(delta, best.delta, diff)) {
                best = { delta, donor, receiver };
            }
        }
    }
    return best;
}

function betterMove(candidateDelta, bestDelta, diff) {
    const candidateAbs = Math.abs(candidateDelta);
    const bestAbs = Math.abs(bestDelta);
    const diffAbs = Math.abs(diff);
    const candidateUnder = candidateAbs <= diffAbs;
    const bestUnder = bestAbs <= diffAbs;
    if (candidateUnder !== bestUnder) {
        return candidateUnder;
    }
    return candidateUnder ? candidateAbs > bestAbs : candidateAbs < bestAbs;
}

function expectedMoveDeltaPPM(weights, donor, receiver) {
    const donorWeight = weights.get(donor.index) || 0;
    const receiverWeight = weights.get(receiver.index) || 0;
    return (
        contributionPPM(receiver.multiplier, receiverWeight + 1) -
        contributionPPM(receiver.multiplier, receiverWeight) +
        contributionPPM(donor.multiplier, donorWeight - 1) -
        contributionPPM(donor.multiplier, donorWeight)
    );
}

function expectedRTPPPMByWeights(weights, baseTiers) {
    return baseTiers.reduce((sum, tier) => sum + contributionPPM(tier.multiplier, weights.get(tier.index) || 0), 0);
}

function sumProbabilities(probabilities) {
    let total = 0;
    for (const value of probabilities.values()) {
        total += value;
    }
    return total;
}

function roundPercent(value) {
    return Math.round(value * 10_000) / 10_000;
}

function expectedContributionPercent(multiplier, probabilityPercent) {
    return contributionPPM(decimal(multiplier), percentToWeightPPM(probabilityPercent)) / 10_000;
}

function contributionPPM(multiplier, weightPPM) {
    return Math.trunc((multiplierToPPM(multiplier) * weightPPM) / ppmScale);
}

function multiplierToPPM(value) {
    return Math.round(decimal(value) * ppmScale);
}
