using System.Buffers;
using System.Globalization;
using System.Runtime.InteropServices.JavaScript;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

public static partial class RegexBridge
{
    private const int AbiVersion = 1;
    private const int MaximumCaptureGroups = 1_000;
    private static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(9);
    private static readonly Lazy<bool> ReplacementParity = new(
        ReplacementParitySelfTest
    );

    [JSExport]
    public static string Identity()
    {
        if (!ReplacementParity.Value)
            throw new InvalidOperationException(
                "The bounded replacement parser failed its native parity self-test."
            );
        return "dotnet-regex|10.0.10|System.Text.RegularExpressions|utf16|culture-invariant|replacement-parity";
    }

    [JSExport]
    public static string Run(string requestJson)
    {
        try
        {
            using var document = JsonDocument.Parse(
                requestJson,
                new JsonDocumentOptions
                {
                    AllowTrailingCommas = false,
                    CommentHandling = JsonCommentHandling.Disallow,
                    MaxDepth = 16,
                }
            );
            var root = document.RootElement;
            if (RequiredInt(root, "abi") != AbiVersion)
                return Failure("bridge", "unsupported-abi", "Unsupported bridge ABI.");

            var operation = RequiredString(root, "operation", 16);
            if (operation is not ("execute" or "replace"))
                return Failure(
                    "bridge",
                    "invalid-operation",
                    "Unsupported bridge operation."
                );
            var pattern = RequiredString(root, "pattern", 65_536);
            var subject = RequiredString(root, "subject", 16 * 1024 * 1024);
            var flags = RequiredString(root, "flags", 32);
            var maximumMatches = RequiredInt(root, "maximumMatches");
            var maximumCaptureRows = RequiredInt(root, "maximumCaptureRows");
            if (
                maximumMatches is < 1 or > 10_000
                || maximumCaptureRows is < 1 or > 100_000
            )
                return Failure(
                    "bridge",
                    "invalid-limit",
                    "Result limits are outside the bridge contract."
                );
            var replacement =
                operation == "replace"
                    ? RequiredString(root, "replacement", 65_536)
                    : string.Empty;
            var maximumOutputBytes =
                operation == "replace" ? RequiredInt(root, "maximumOutputBytes") : 1;
            if (
                operation == "replace"
                && maximumOutputBytes is < 1 or > 64 * 1024 * 1024
            )
                return Failure(
                    "bridge",
                    "invalid-limit",
                    "Output limit is outside the bridge contract."
                );

            Regex expression;
            try
            {
                expression = new Regex(
                    pattern,
                    RegexOptionsFor(flags) | RegexOptions.CultureInvariant,
                    MatchTimeout
                );
            }
            catch (RegexParseException error)
            {
                return Failure(
                    "compile",
                    "regex-parse-error",
                    error.Message,
                    error.Offset
                );
            }
            catch (ArgumentException error)
            {
                return Failure("compile", "regex-parse-error", error.Message);
            }

            var numbers = expression.GetGroupNumbers();
            var groupCount = numbers.Length == 0 ? 0 : numbers.Max();
            if (groupCount > MaximumCaptureGroups)
                return Failure(
                    "compile",
                    "capture-group-limit",
                    "The pattern exceeds the 1,000 capture-group bridge limit."
                );
            var presentNumbers = numbers.ToHashSet();
            var groupNames = expression
                .GetGroupNames()
                .Select(name => (Name: name, Number: expression.GroupNumberFromName(name)))
                .Where(item =>
                    item.Number > 0
                    && !int.TryParse(
                        item.Name,
                        NumberStyles.None,
                        CultureInfo.InvariantCulture,
                        out _
                    )
                )
                .OrderBy(item => item.Number)
                .ToArray();
            ReplacementProgram? replacementProgram = null;
            if (operation == "replace")
            {
                try
                {
                    replacementProgram = ReplacementProgram.Parse(
                        replacement,
                        expression,
                        presentNumbers
                    );
                }
                catch (ArgumentException error)
                {
                    return Failure(
                        "replacement",
                        "replacement-error",
                        error.Message
                    );
                }
            }

            var records = new List<MatchRecord>();
            var captureRows = 0;
            var resultsTruncated = false;
            var global = flags.Contains('g');
            try
            {
                for (var match = expression.Match(subject); match.Success; match = match.NextMatch())
                {
                    var rows = Math.Max(1, groupCount);
                    if (
                        records.Count >= maximumMatches
                        || captureRows + rows > maximumCaptureRows
                    )
                    {
                        resultsTruncated = true;
                        break;
                    }
                    var captures = new NativeSpan?[groupCount];
                    for (var number = 1; number <= groupCount; number++)
                    {
                        if (!presentNumbers.Contains(number))
                            continue;
                        var group = match.Groups[number];
                        if (group.Success)
                            captures[number - 1] = new NativeSpan(
                                group.Index,
                                group.Index + group.Length
                            );
                    }
                    records.Add(
                        new MatchRecord(
                            new NativeSpan(match.Index, match.Index + match.Length),
                            captures,
                            match
                        )
                    );
                    captureRows += rows;
                    if (!global)
                        break;
                }
            }
            catch (RegexMatchTimeoutException error)
            {
                return Failure("match", "match-timeout", error.Message);
            }

            BoundedUtf8Output? output = null;
            if (operation == "replace")
            {
                output = new BoundedUtf8Output(maximumOutputBytes);
                var sourcePosition = 0;
                try
                {
                    foreach (var record in records.OrderBy(record => record.Span.Start))
                    {
                        output.Append(
                            subject.AsSpan(
                                sourcePosition,
                                record.Span.Start - sourcePosition
                            )
                        );
                        AppendReplacement(
                            output,
                            subject,
                            record.Match,
                            replacementProgram!
                        );
                        sourcePosition = record.Span.End;
                    }
                    output.Append(subject.AsSpan(sourcePosition));
                }
                catch (ArgumentException error)
                {
                    return Failure(
                        "replacement",
                        "replacement-error",
                        error.Message
                    );
                }
            }

            return Success(
                groupCount,
                groupNames,
                records,
                resultsTruncated,
                output
            );
        }
        catch (JsonException error)
        {
            return Failure("bridge", "invalid-json", error.Message);
        }
        catch (Exception error)
        {
            return Failure("bridge", "bridge-error", error.Message);
        }
    }

    private static RegexOptions RegexOptionsFor(string flags)
    {
        var options = RegexOptions.None;
        foreach (var flag in flags)
            options |= flag switch
            {
                'g' => RegexOptions.None,
                'i' => RegexOptions.IgnoreCase,
                'm' => RegexOptions.Multiline,
                's' => RegexOptions.Singleline,
                'n' => RegexOptions.ExplicitCapture,
                'x' => RegexOptions.IgnorePatternWhitespace,
                'r' => RegexOptions.RightToLeft,
                'c' => RegexOptions.CultureInvariant,
                'b' => RegexOptions.NonBacktracking,
                _ => throw new ArgumentException($"Unsupported .NET flag: {flag}."),
            };
        return options;
    }

    private static string RequiredString(JsonElement root, string name, int maximumLength)
    {
        if (
            !root.TryGetProperty(name, out var property)
            || property.ValueKind != JsonValueKind.String
        )
            throw new JsonException($"{name} must be a string.");
        var value = property.GetString()!;
        if (value.Length > maximumLength)
            throw new JsonException($"{name} exceeds its bounded contract.");
        return value;
    }

    private static int RequiredInt(JsonElement root, string name)
    {
        if (
            !root.TryGetProperty(name, out var property)
            || !property.TryGetInt32(out var value)
        )
            throw new JsonException($"{name} must be a 32-bit integer.");
        return value;
    }

    private static string Failure(
        string phase,
        string code,
        string message,
        int? position = null
    )
    {
        using var stream = new MemoryStream();
        using (var writer = new Utf8JsonWriter(stream))
        {
            writer.WriteStartObject();
            writer.WriteNumber("abi", AbiVersion);
            writer.WriteBoolean("ok", false);
            writer.WriteString("phase", phase);
            writer.WriteString("code", code);
            writer.WriteString(
                "message",
                message.Length <= 1_024 ? message : message[..1_024]
            );
            if (position >= 0)
                writer.WriteNumber("position", position.Value);
            writer.WriteEndObject();
        }
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    private static string Success(
        int groupCount,
        IEnumerable<(string Name, int Number)> groupNames,
        IEnumerable<MatchRecord> records,
        bool resultsTruncated,
        BoundedUtf8Output? output
    )
    {
        using var stream = new MemoryStream();
        using (var writer = new Utf8JsonWriter(stream))
        {
            writer.WriteStartObject();
            writer.WriteNumber("abi", AbiVersion);
            writer.WriteBoolean("ok", true);
            writer.WriteNumber("groupCount", groupCount);
            writer.WriteStartArray("groupNames");
            foreach (var group in groupNames)
            {
                writer.WriteStartArray();
                writer.WriteNumberValue(group.Number);
                writer.WriteStringValue(group.Name);
                writer.WriteEndArray();
            }
            writer.WriteEndArray();
            writer.WriteStartArray("matches");
            foreach (var record in records)
            {
                writer.WriteStartObject();
                WriteSpan(writer, "span", record.Span);
                writer.WriteStartArray("captures");
                foreach (var capture in record.Captures)
                {
                    if (capture is { } span)
                        WriteSpan(writer, null, span);
                    else
                        writer.WriteNullValue();
                }
                writer.WriteEndArray();
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
            writer.WriteBoolean("resultsTruncated", resultsTruncated);
            if (output is not null)
            {
                writer.WriteBase64String("outputBase64", output.Bytes);
                writer.WriteNumber("outputBytes", output.ByteCount);
                writer.WriteBoolean("outputTruncated", output.Truncated);
            }
            writer.WriteEndObject();
        }
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    private static void WriteSpan(
        Utf8JsonWriter writer,
        string? propertyName,
        NativeSpan span
    )
    {
        if (propertyName is null)
            writer.WriteStartArray();
        else
            writer.WriteStartArray(propertyName);
        writer.WriteNumberValue(span.Start);
        writer.WriteNumberValue(span.End);
        writer.WriteEndArray();
    }

    private readonly record struct NativeSpan(int Start, int End);

    private sealed record MatchRecord(
        NativeSpan Span,
        NativeSpan?[] Captures,
        Match Match
    );

    private static void AppendReplacement(
        BoundedUtf8Output output,
        string subject,
        Match match,
        ReplacementProgram replacement
    )
    {
        foreach (var token in replacement.Tokens)
        {
            switch (token.Kind)
            {
                case ReplacementTokenKind.Literal:
                    output.Append(
                        replacement.Template.AsSpan(token.Start, token.Length)
                    );
                    break;
                case ReplacementTokenKind.Group:
                {
                    var group = match.Groups[token.GroupNumber];
                    if (group.Success)
                        output.Append(subject.AsSpan(group.Index, group.Length));
                    break;
                }
                case ReplacementTokenKind.LeftPortion:
                    output.Append(subject.AsSpan(0, match.Index));
                    break;
                case ReplacementTokenKind.RightPortion:
                {
                    var start = match.Index + match.Length;
                    output.Append(subject.AsSpan(start));
                    break;
                }
                case ReplacementTokenKind.LastGroup:
                {
                    var group = match.Groups[replacement.LastGroupNumber];
                    if (group.Success)
                        output.Append(subject.AsSpan(group.Index, group.Length));
                    break;
                }
                case ReplacementTokenKind.WholeString:
                    output.Append(subject.AsSpan());
                    break;
            }
        }
    }

    private static bool ReplacementParitySelfTest()
    {
        try
        {
            var expression = new Regex(
                "(?<word>a)(b)?(?<tail>c)?",
                RegexOptions.CultureInvariant,
                MatchTimeout
            );
            var presentNumbers = expression.GetGroupNumbers().ToHashSet();
            var templates = new[]
            {
                "$2:$1/${word}/${3}/$$/$&/$`/$'/$+/$_/$99/${missing}/$",
                "${1}$01-$0",
                "literal",
            };
            for (
                var match = expression.Match("zab a");
                match.Success;
                match = match.NextMatch()
            )
            {
                foreach (var template in templates)
                {
                    var replacement = ReplacementProgram.Parse(
                        template,
                        expression,
                        presentNumbers
                    );
                    var bounded = new BoundedUtf8Output(16 * 1024);
                    AppendReplacement(
                        bounded,
                        "zab a",
                        match,
                        replacement
                    );
                    if (
                        bounded.Truncated
                        || Encoding.UTF8.GetString(bounded.Bytes)
                            != match.Result(template)
                    )
                        return false;
                }
            }

            try
            {
                _ = ReplacementProgram.Parse(
                    "$2147483648",
                    expression,
                    presentNumbers
                );
                return false;
            }
            catch (ArgumentException)
            {
                try
                {
                    _ = expression.Match("a").Result("$2147483648");
                    return false;
                }
                catch (ArgumentException)
                {
                    // Both parsers reject an overflowing numeric reference.
                }
            }

            var largeSubject = new string('a', 256 * 1024);
            var largeExpression = new Regex(
                "(a+)",
                RegexOptions.CultureInvariant,
                MatchTimeout
            );
            var amplifyingTemplate = string.Concat(
                Enumerable.Repeat("$1", 32_768)
            );
            var amplifyingProgram = ReplacementProgram.Parse(
                amplifyingTemplate,
                largeExpression,
                largeExpression.GetGroupNumbers().ToHashSet()
            );
            var adversarial = new BoundedUtf8Output(5);
            AppendReplacement(
                adversarial,
                largeSubject,
                largeExpression.Match(largeSubject),
                amplifyingProgram
            );
            return adversarial.Truncated
                && Encoding.UTF8.GetString(adversarial.Bytes) == "aaaaa";
        }
        catch
        {
            return false;
        }
    }

    private enum ReplacementTokenKind
    {
        Literal,
        Group,
        LeftPortion,
        RightPortion,
        LastGroup,
        WholeString,
    }

    private readonly record struct ReplacementToken(
        ReplacementTokenKind Kind,
        int Start = 0,
        int Length = 0,
        int GroupNumber = 0
    );

    private sealed record ReplacementProgram(
        string Template,
        ReplacementToken[] Tokens,
        int LastGroupNumber
    )
    {
        public static ReplacementProgram Parse(
            string template,
            Regex expression,
            HashSet<int> presentNumbers
        )
        {
            var tokens = new List<ReplacementToken>();
            var literalStart = 0;
            var cursor = 0;

            void AddLiteral(int end)
            {
                if (end > literalStart)
                    tokens.Add(
                        new ReplacementToken(
                            ReplacementTokenKind.Literal,
                            literalStart,
                            end - literalStart
                        )
                    );
            }

            void AddRecognized(
                int dollar,
                int end,
                ReplacementToken token
            )
            {
                AddLiteral(dollar);
                tokens.Add(token);
                cursor = end;
                literalStart = end;
            }

            while (cursor < template.Length)
            {
                var dollar = template.IndexOf('$', cursor);
                if (dollar < 0)
                    break;
                if (dollar + 1 >= template.Length)
                {
                    cursor = dollar + 1;
                    continue;
                }

                var next = template[dollar + 1];
                if (next == '$')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.Literal,
                            dollar + 1,
                            1
                        )
                    );
                    continue;
                }
                if (next == '&')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.Group,
                            GroupNumber: 0
                        )
                    );
                    continue;
                }
                if (next == '`')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.LeftPortion
                        )
                    );
                    continue;
                }
                if (next == '\'')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.RightPortion
                        )
                    );
                    continue;
                }
                if (next == '+')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.LastGroup
                        )
                    );
                    continue;
                }
                if (next == '_')
                {
                    AddRecognized(
                        dollar,
                        dollar + 2,
                        new ReplacementToken(
                            ReplacementTokenKind.WholeString
                        )
                    );
                    continue;
                }

                var angled = next == '{' && dollar + 2 < template.Length;
                var referenceStart = dollar + (angled ? 2 : 1);
                if (
                    referenceStart < template.Length
                    && template[referenceStart] is >= '0' and <= '9'
                )
                {
                    var number = 0;
                    var end = referenceStart;
                    while (
                        end < template.Length
                        && template[end] is >= '0' and <= '9'
                    )
                    {
                        var digit = template[end] - '0';
                        if (
                            number > int.MaxValue / 10
                            || (
                                number == int.MaxValue / 10
                                && digit > int.MaxValue % 10
                            )
                        )
                            throw new ArgumentException(
                                "A replacement capture number exceeds Int32.MaxValue."
                            );
                        number = number * 10 + digit;
                        end++;
                    }
                    var syntaxComplete =
                        !angled
                        || (
                            end < template.Length
                            && template[end] == '}'
                        );
                    if (syntaxComplete && presentNumbers.Contains(number))
                    {
                        AddRecognized(
                            dollar,
                            angled ? end + 1 : end,
                            new ReplacementToken(
                                ReplacementTokenKind.Group,
                                GroupNumber: number
                            )
                        );
                        continue;
                    }
                }
                else if (
                    angled
                    && referenceStart < template.Length
                    && IsBoundaryWordCharacter(template[referenceStart])
                )
                {
                    var end = referenceStart + 1;
                    while (
                        end < template.Length
                        && IsBoundaryWordCharacter(template[end])
                    )
                        end++;
                    if (end < template.Length && template[end] == '}')
                    {
                        var name = template[
                            referenceStart..end
                        ];
                        var number = expression.GroupNumberFromName(name);
                        if (number >= 0)
                        {
                            AddRecognized(
                                dollar,
                                end + 1,
                                new ReplacementToken(
                                    ReplacementTokenKind.Group,
                                    GroupNumber: number
                                )
                            );
                            continue;
                        }
                    }
                }

                cursor = dollar + 1;
            }

            AddLiteral(template.Length);
            var numbers = expression.GetGroupNumbers();
            return new ReplacementProgram(
                template,
                tokens.ToArray(),
                numbers.Length == 0 ? 0 : numbers[^1]
            );
        }

        private static bool IsBoundaryWordCharacter(char value)
        {
            if (value is '\u200c' or '\u200d')
                return true;
            return CharUnicodeInfo.GetUnicodeCategory(value) switch
            {
                UnicodeCategory.UppercaseLetter => true,
                UnicodeCategory.LowercaseLetter => true,
                UnicodeCategory.TitlecaseLetter => true,
                UnicodeCategory.ModifierLetter => true,
                UnicodeCategory.OtherLetter => true,
                UnicodeCategory.NonSpacingMark => true,
                UnicodeCategory.DecimalDigitNumber => true,
                UnicodeCategory.ConnectorPunctuation => true,
                _ => false,
            };
        }
    }

    private sealed class BoundedUtf8Output(int maximumBytes)
    {
        private readonly ArrayBufferWriter<byte> _value = new();
        public int ByteCount => _value.WrittenCount;
        public bool Truncated { get; private set; }
        public ReadOnlySpan<byte> Bytes => _value.WrittenSpan;

        public void Append(ReadOnlySpan<char> value)
        {
            if (Truncated)
                return;
            while (!value.IsEmpty)
            {
                var status = Rune.DecodeFromUtf16(value, out var rune, out var consumed);
                if (status != OperationStatus.Done)
                {
                    rune = Rune.ReplacementChar;
                    consumed = 1;
                }
                var width = rune.Utf8SequenceLength;
                if (ByteCount + width > maximumBytes)
                {
                    Truncated = true;
                    return;
                }
                var destination = _value.GetSpan(width);
                var written = rune.EncodeToUtf8(destination);
                _value.Advance(written);
                value = value[consumed..];
            }
        }
    }
}
