using System.Text.RegularExpressions;

namespace RolemasterDb.CriticalParsing;

public static class AffixEffectParser
{
    private const string FoeTarget = "foe";

    private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
    private static readonly Regex PowerPointModifierRegex = new(@"(?<sign>[+-])\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
    private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);
    private static readonly Regex QuickDirectHitsRegex = new(@"^\+(?<value>\d+)(?:H)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickStunnedRegex = new(@"^(?<value>\d+)s$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickMustParryRegex = new(@"^(?<value>\d+)mp$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickNoParryRegex = new(@"^(?<value>\d+)np$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickBleedRegex = new(@"^(?<value>\d+)hpr$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickFoePenaltyRegex = new(@"^-(?<value>\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickAttackerBonusRegex = new(@"^\+(?<value>\d+)b$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex QuickPowerPointRegex = new(@"^(?<sign>[+-])(?<expression>.+)pp$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
    {
        if (string.IsNullOrWhiteSpace(rawAffixText))
        {
            return [];
        }

        var effects = new List<ParsedCriticalEffect>();

        foreach (var rawLine in rawAffixText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            ParseLine(CriticalCellParserSupport.CollapseWhitespace(rawLine), affixLegend, effects);
        }

        return effects;
    }

    internal static AffixTokenParseResult ParseAffixToken(
        string token,
        AffixLegend affixLegend)
    {
        var normalizedToken = CriticalCellParserSupport.CollapseWhitespace(token);
        if (string.IsNullOrWhiteSpace(normalizedToken) || normalizedToken is "-" or "–" or "—")
        {
            return new AffixTokenParseResult([], normalizedToken, []);
        }

        if (TryParseQuickToken(normalizedToken, out var quickEffects, out var canonicalToken))
        {
            return new AffixTokenParseResult(quickEffects, canonicalToken, []);
        }

        return ParseLegacyToken(normalizedToken, affixLegend);
    }

    private static void ParseLine(string line, AffixLegend affixLegend, List<ParsedCriticalEffect> effects)
    {
        ParseLine(line, affixLegend, effects, []);
    }

    private static void ParseLine(
        string line,
        AffixLegend affixLegend,
        List<ParsedCriticalEffect> effects,
        List<(int Start, int End)> consumedRanges)
    {
        if (string.IsNullOrWhiteSpace(line) || line is "-" or "–" or "—")
        {
            return;
        }

        var matchedEffects = new List<(int Index, ParsedCriticalEffect Effect)>();

        AddMatches(
            DirectHitsRegex.Matches(line),
            matchedEffects,
            consumedRanges,
            match =>
            {
                var hits = ParseSignedInteger(match.Value);
                return new ParsedCriticalEffect(
                    CriticalEffectCodes.DirectHits,
                    FoeTarget,
                    hits,
                    null,
                    null,
                    null,
                    null,
                    null,
                    false,
                    "symbol",
                    NormalizeToken(match.Value));
            });

        if (affixLegend.SupportsPowerPointModifier)
        {
            AddMatches(
                PowerPointModifierRegex.Matches(line),
                matchedEffects,
                consumedRanges,
                match => new ParsedCriticalEffect(
                    CriticalEffectCodes.PowerPointModifier,
                    FoeTarget,
                    null,
                    PowerPointModifierNotation.NormalizeExpression(
                        match.Groups["expression"].Value,
                        match.Groups["sign"].Value[0]),
                    null,
                    null,
                    null,
                    null,
                    false,
                    "symbol",
                    NormalizeToken(match.Value)));
        }

        AddMatches(
            ModifierRegex.Matches(line),
            matchedEffects,
            consumedRanges,
            match =>
            {
                var modifier = BuildModifier(match);
                if (modifier is null)
                {
                    return null;
                }

                if (modifier.Value < 0 && affixLegend.SupportsFoePenalty)
                {
                    return new ParsedCriticalEffect(
                        CriticalEffectCodes.FoePenalty,
                        FoeTarget,
                        null,
                        null,
                        null,
                        null,
                        modifier.Value,
                        null,
                        false,
                        "symbol",
                        NormalizeToken(match.Value));
                }

                if (modifier.Value > 0 && affixLegend.SupportsAttackerBonus)
                {
                    return new ParsedCriticalEffect(
                        CriticalEffectCodes.AttackerBonusNextRound,
                        null,
                        null,
                        null,
                        null,
                        null,
                        modifier.Value,
                        null,
                        false,
                        "symbol",
                        NormalizeToken(match.Value));
                }

                return null;
            });

        var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
        if (symbolClusterRegex is null)
        {
            if (matchedEffects.Count > 0)
            {
                effects.AddRange(matchedEffects
                    .OrderBy(item => item.Index)
                    .Select(item => item.Effect));
            }

            return;
        }

        foreach (Match match in symbolClusterRegex.Matches(line))
        {
            if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
            {
                continue;
            }

            var effectCountBeforeMatch = matchedEffects.Count;
            var magnitude = match.Groups["count"].Success
                ? int.Parse(match.Groups["count"].Value)
                : 1;
            var matchedText = NormalizeToken(match.Value);

            foreach (var symbol in match.Groups["symbols"].Value.Select(character => character.ToString()))
            {
                var effectCode = affixLegend.ResolveEffectCode(symbol);
                if (effectCode is null)
                {
                    continue;
                }

                matchedEffects.Add((match.Index, CreateSymbolEffect(effectCode, magnitude, matchedText)));
            }

            if (matchedEffects.Count > effectCountBeforeMatch)
            {
                consumedRanges.Add((match.Index, match.Index + match.Length));
            }
        }

        if (matchedEffects.Count > 0)
        {
            effects.AddRange(matchedEffects
                .OrderBy(item => item.Index)
                .Select(item => item.Effect));
        }
    }

    private static bool TryParseQuickToken(
        string token,
        out IReadOnlyList<ParsedCriticalEffect> effects,
        out string canonicalToken)
    {
        canonicalToken = token;

        if (TryMatchSingle(QuickDirectHitsRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.DirectHits,
                FoeTarget,
                int.Parse(match.Groups["value"].Value),
                null,
                null,
                null,
                null,
                null,
                false,
                "quick",
                $"+{match.Groups["value"].Value}"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickStunnedRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.StunnedRounds,
                FoeTarget,
                null,
                null,
                int.Parse(match.Groups["value"].Value),
                null,
                null,
                null,
                false,
                "quick",
                $"{match.Groups["value"].Value}s"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickMustParryRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.MustParryRounds,
                FoeTarget,
                null,
                null,
                int.Parse(match.Groups["value"].Value),
                null,
                null,
                null,
                false,
                "quick",
                $"{match.Groups["value"].Value}mp"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickNoParryRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.NoParryRounds,
                FoeTarget,
                null,
                null,
                int.Parse(match.Groups["value"].Value),
                null,
                null,
                null,
                false,
                "quick",
                $"{match.Groups["value"].Value}np"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickBleedRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.BleedPerRound,
                FoeTarget,
                null,
                null,
                null,
                int.Parse(match.Groups["value"].Value),
                null,
                null,
                false,
                "quick",
                $"{match.Groups["value"].Value}hpr"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickFoePenaltyRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.FoePenalty,
                FoeTarget,
                null,
                null,
                null,
                null,
                -int.Parse(match.Groups["value"].Value),
                null,
                false,
                "quick",
                $"-{match.Groups["value"].Value}"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickAttackerBonusRegex, token, match =>
            new ParsedCriticalEffect(
                CriticalEffectCodes.AttackerBonusNextRound,
                null,
                null,
                null,
                null,
                null,
                int.Parse(match.Groups["value"].Value),
                null,
                false,
                "quick",
                $"+{match.Groups["value"].Value}b"), out effects, out canonicalToken))
        {
            return true;
        }

        if (TryMatchSingle(QuickPowerPointRegex, token, match =>
        {
            var normalizedExpression = PowerPointModifierNotation.NormalizeExpression(
                match.Groups["expression"].Value,
                match.Groups["sign"].Value[0])!;

            return new ParsedCriticalEffect(
                CriticalEffectCodes.PowerPointModifier,
                FoeTarget,
                null,
                normalizedExpression,
                null,
                null,
                null,
                null,
                false,
                "quick",
                $"{normalizedExpression}pp");
        }, out effects, out canonicalToken))
        {
            return true;
        }

        effects = [];
        return false;
    }

    private static bool TryMatchSingle(
        Regex regex,
        string token,
        Func<Match, ParsedCriticalEffect> createEffect,
        out IReadOnlyList<ParsedCriticalEffect> effects,
        out string canonicalToken)
    {
        var match = regex.Match(token);
        if (!match.Success)
        {
            effects = [];
            canonicalToken = token;
            return false;
        }

        var effect = createEffect(match);
        effects = [effect];
        canonicalToken = effect.SourceText ?? token;
        return true;
    }

    private static ParsedCriticalEffect CreateSymbolEffect(string effectCode, int magnitude, string sourceText) =>
        effectCode switch
        {
            CriticalEffectCodes.MustParryRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.NoParryRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.StunnedRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.BleedPerRound => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                null,
                magnitude,
                null,
                null,
                false,
                "symbol",
                sourceText),
            _ => throw new InvalidOperationException($"Unsupported symbol effect code '{effectCode}'.")
        };

    private static Regex? CreateSymbolClusterRegex(IReadOnlySet<string> symbols)
    {
        if (symbols.Count == 0)
        {
            return null;
        }

        var escapedSymbols = string.Concat(symbols.Select(Regex.Escape));
        return new Regex(
            $@"(?<![A-Za-z0-9])(?:(?<count>\d+)\s*)?(?<symbols>[{escapedSymbols}]+)",
            RegexOptions.Compiled);
    }

    private static void AddMatches(
        MatchCollection matches,
        List<(int Index, ParsedCriticalEffect Effect)> matchedEffects,
        List<(int Start, int End)> consumedRanges,
        Func<Match, ParsedCriticalEffect?> createEffect)
    {
        foreach (Match match in matches)
        {
            if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
            {
                continue;
            }

            var effect = createEffect(match);
            if (effect is not null)
            {
                consumedRanges.Add((match.Index, match.Index + match.Length));
                matchedEffects.Add((match.Index, effect));
            }
        }
    }

    private static AffixTokenParseResult ParseLegacyToken(string normalizedToken, AffixLegend affixLegend)
    {
        var effects = new List<ParsedCriticalEffect>();
        var consumedRanges = new List<(int Start, int End)>();
        ParseLine(normalizedToken, affixLegend, effects, consumedRanges);

        var unresolvedFragments = ExtractUnresolvedFragments(normalizedToken, consumedRanges);
        var canonicalToken = effects
            .Select(effect => effect.SourceText)
            .Where(sourceText => !string.IsNullOrWhiteSpace(sourceText))
            .Distinct(StringComparer.Ordinal)
            .ToList();

        return new AffixTokenParseResult(
            effects,
            canonicalToken.Count == 0 ? normalizedToken : string.Join(", ", canonicalToken),
            unresolvedFragments);
    }

    private static IReadOnlyList<string> ExtractUnresolvedFragments(
        string normalizedToken,
        IReadOnlyList<(int Start, int End)> consumedRanges)
    {
        if (string.IsNullOrWhiteSpace(normalizedToken))
        {
            return [];
        }

        if (consumedRanges.Count == 0)
        {
            return [normalizedToken];
        }

        var unresolved = new List<string>();
        var orderedRanges = consumedRanges
            .OrderBy(range => range.Start)
            .ToList();
        var currentIndex = 0;

        foreach (var range in orderedRanges)
        {
            if (currentIndex < range.Start)
            {
                AddUnresolvedFragment(normalizedToken[currentIndex..range.Start], unresolved);
            }

            currentIndex = Math.Max(currentIndex, range.End);
        }

        if (currentIndex < normalizedToken.Length)
        {
            AddUnresolvedFragment(normalizedToken[currentIndex..], unresolved);
        }

        return unresolved;
    }

    private static void AddUnresolvedFragment(string candidate, List<string> unresolved)
    {
        var normalizedFragment = candidate
            .Trim()
            .Trim(',', ';', '/', '\\');

        if (!string.IsNullOrWhiteSpace(normalizedFragment))
        {
            unresolved.Add(normalizedFragment);
        }
    }

    private static bool OverlapsConsumedRange(Match match, IReadOnlyList<(int Start, int End)> consumedRanges) =>
        consumedRanges.Any(range => match.Index < range.End && range.Start < match.Index + match.Length);

    private static int ParseSignedInteger(string value) =>
        int.Parse(value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("H", string.Empty, StringComparison.OrdinalIgnoreCase));

    private static int? BuildModifier(Match match)
    {
        if (!int.TryParse(match.Groups["value"].Value, out var absoluteValue))
        {
            return null;
        }

        return string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal)
            ? -absoluteValue
            : absoluteValue;
    }

    private static string NormalizeToken(string value) =>
        CriticalCellParserSupport.CollapseWhitespace(value)
            .Replace(" +", "+", StringComparison.Ordinal)
            .Replace("( ", "(", StringComparison.Ordinal)
            .Replace(" )", ")", StringComparison.Ordinal);
}
