using RpgRoller.Contracts;
using RpgRoller.Domain;

namespace RpgRoller.Services;

public sealed class RolemasterRollEngine(IDiceRoller diceRoller)
{
    public (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) Roll(DiceExpression expression, int? fumbleRange, bool rolemasterAutoRetry, int situationalModifier = 0)
    {
        return expression.Kind switch
        {
            DiceExpressionKind.RolemasterOpenEndedPercentile => RollOpenEnded(expression, fumbleRange.GetValueOrDefault(), rolemasterAutoRetry, situationalModifier),
            _                                                => RollStandard(expression, situationalModifier)
        };
    }

    private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollStandard(DiceExpression expression, int situationalModifier, int? attempt = null)
    {
        var diceValues = new int[expression.DiceCount];
        var dice = new RollDieResult[expression.DiceCount];
        var total = expression.Modifier + situationalModifier;
        for (var i = 0; i < expression.DiceCount; i += 1)
        {
            var value = diceRoller.Roll(expression.Sides);
            diceValues[i] = value;
            dice[i] = CreateRolemasterDie(value, i + 1, RollDieKinds.RolemasterStandard, value, attempt);
            total += value;
        }

        return (total, RollBreakdownFormatter.BuildRolemasterModifierBreakdown(diceValues, expression.Modifier, situationalModifier, total), dice);
    }

    private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollOpenEnded(DiceExpression expression, int fumbleRange, bool rolemasterAutoRetry, int situationalModifier)
    {
        var firstAttempt = RollOpenEndedAttempt(expression, fumbleRange, situationalModifier);
        var retryBonus = rolemasterAutoRetry ? RolemasterRetryPolicy.ResolveAutoRetryBonus(firstAttempt.Total) : null;
        if (!retryBonus.HasValue)
            return firstAttempt;

        var retryAttempt = RollOpenEndedAttempt(expression, fumbleRange, situationalModifier, 2);
        var finalTotal = retryAttempt.Total + retryBonus.Value;
        var breakdown = RollBreakdownFormatter.BuildRolemasterRetryBreakdown(firstAttempt.Breakdown, retryBonus.Value, retryAttempt.Breakdown, finalTotal);
        var dice = AddAttemptMarker(firstAttempt.Dice, 1).Concat(retryAttempt.Dice).ToArray();

        return (finalTotal, breakdown, dice);
    }

    private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollOpenEndedAttempt(DiceExpression expression, int fumbleRange, int situationalModifier, int? attempt = null)
    {
        var initialRoll = diceRoller.Roll(expression.Sides);
        var followUpRolls = new List<int>();
        int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll;
        var dice = new List<RollDieResult> { CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution, attempt) };

        var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll;
        var subtractFollowUps = false;
        if (initialRoll >= 96)
        {
            followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, false, attempt));
            baseTotal += followUpRolls.Sum();
        }
        else if (initialRoll <= fumbleRange)
        {
            subtractFollowUps = true;
            followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, true, attempt));
            baseTotal -= followUpRolls.Sum();
        }

        var total = baseTotal + expression.Modifier + situationalModifier;
        var breakdown = RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(initialRoll, followUpRolls, subtractFollowUps, expression.Modifier, total, situationalModifier);
        return (total, breakdown, dice);
    }

    private IEnumerable<int> RollHighOpenEndedChain(List<RollDieResult> dice, int sequenceStart, bool subtract, int? attempt)
    {
        var followUpRolls = new List<int>();
        var sequence = sequenceStart;

        while (true)
        {
            var roll = diceRoller.Roll(100);
            followUpRolls.Add(roll);
            dice.Add(CreateRolemasterDie(roll, sequence, subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, subtract ? -roll : roll, attempt));

            sequence += 1;
            if (roll < 96)
                break;
        }

        return followUpRolls;
    }

    private static IReadOnlyList<RollDieResult> AddAttemptMarker(IReadOnlyList<RollDieResult> dice, int attempt)
    {
        return dice.Select(die => die with { Attempt = attempt }).ToArray();
    }

    private static RollDieResult CreateRolemasterDie(int roll, int sequence, string kind, int? signedContribution, int? attempt)
    {
        return new(roll, false, false, false, false, kind == RollDieKinds.RolemasterOpenEndedHigh, sequence, kind, signedContribution, attempt);
    }
}