// Copyright 2023-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/subject_transform.go in the NATS server Go source.

using System.Text.RegularExpressions;

namespace ZB.MOM.NatsNet.Server.Internal;

// -------------------------------------------------------------------------
// Subject token constants (mirrors const block in server/sublist.go)
// -------------------------------------------------------------------------

internal static class SubjectTokens
{
    internal const char Pwc = '*';    // partial wildcard character
    internal const string Pwcs = "*"; // partial wildcard string
    internal const char Fwc = '>';    // full wildcard character
    internal const string Fwcs = ">";  // full wildcard string
    internal const string Tsep = "."; // token separator string
    internal const char Btsep = '.';  // token separator character
    internal const string Empty = ""; // _EMPTY_
}

// -------------------------------------------------------------------------
// Transform type constants (mirrors enum in subject_transform.go)
// -------------------------------------------------------------------------

internal static class TransformType
{
    internal const short NoTransform = 0;
    internal const short BadTransform = 1;
    internal const short Partition = 2;
    internal const short Wildcard = 3;
    internal const short SplitFromLeft = 4;
    internal const short SplitFromRight = 5;
    internal const short SliceFromLeft = 6;
    internal const short SliceFromRight = 7;
    internal const short Split = 8;
    internal const short Left = 9;
    internal const short Right = 10;
    internal const short Random = 11;
}

// -------------------------------------------------------------------------
// ISubjectTransformer interface (mirrors SubjectTransformer in Go)
// -------------------------------------------------------------------------

/// <summary>
/// Transforms NATS subjects according to a source-to-destination mapping.
/// Mirrors <c>SubjectTransformer</c> in server/subject_transform.go.
/// </summary>
public interface ISubjectTransformer
{
    (string result, Exception? err) Match(string subject);
    string TransformSubject(string subject);
    string TransformTokenizedSubject(string[] tokens);
}

// -------------------------------------------------------------------------
// SubjectTransform class
// -------------------------------------------------------------------------

/// <summary>
/// Subject mapping and transform engine.
/// Mirrors <c>subjectTransform</c> in server/subject_transform.go.
/// </summary>
public sealed class SubjectTransform : ISubjectTransformer
{
    private readonly string _src;
    private readonly string _dest;
    private readonly string[] _dtoks; // destination tokens
    private readonly string[] _stoks; // source tokens
    private readonly short[] _dtokmftypes;
    private readonly int[][] _dtokmftokindexesargs;
    private readonly int[] _dtokmfintargs;
    private readonly string[] _dtokmfstringargs;

    // Subject mapping function regexes (mirrors var block in Go).
    private static readonly Regex CommaSep = new(@",\s*", RegexOptions.Compiled);
    private static readonly Regex PartitionRe = new(@"\{\{\s*[pP]artition\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex WildcardRe = new(@"\{\{\s*[wW]ildcard\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex SplitFromLeftRe = new(@"\{\{\s*[sS]plit[fF]rom[lL]eft\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex SplitFromRightRe = new(@"\{\{\s*[sS]plit[fF]rom[rR]ight\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex SliceFromLeftRe = new(@"\{\{\s*[sS]lice[fF]rom[lL]eft\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex SliceFromRightRe = new(@"\{\{\s*[sS]lice[fF]rom[rR]ight\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex SplitRe = new(@"\{\{\s*[sS]plit\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex LeftRe = new(@"\{\{\s*[lL]eft\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex RightRe = new(@"\{\{\s*[rR]ight\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);
    private static readonly Regex RandomRe = new(@"\{\{\s*[rR]andom\s*\((.*)\)\s*\}\}", RegexOptions.Compiled);

    private SubjectTransform(
        string src, string dest,
        string[] dtoks, string[] stoks,
        short[] dtokmftypes, int[][] dtokmftokindexesargs,
        int[] dtokmfintargs, string[] dtokmfstringargs)
    {
        _src = src;
        _dest = dest;
        _dtoks = dtoks;
        _stoks = stoks;
        _dtokmftypes = dtokmftypes;
        _dtokmftokindexesargs = dtokmftokindexesargs;
        _dtokmfintargs = dtokmfintargs;
        _dtokmfstringargs = dtokmfstringargs;
    }

    /// <summary>
    /// Creates a new transform with optional strict mode.
    /// Returns (null, null) when dest is empty (no transform needed).
    /// Mirrors <c>NewSubjectTransformWithStrict</c>.
    /// </summary>
    public static (SubjectTransform? transform, Exception? err) NewWithStrict(
        string src, string dest, bool strict)
    {
        if (dest == SubjectTokens.Empty)
            return (null, null);

        if (src == SubjectTokens.Empty)
            src = SubjectTokens.Fwcs;

        var (sv, stokens, npwcs, hasFwc) = SubjectInfo(src);
        var (dv, dtokens, dnpwcs, dHasFwc) = SubjectInfo(dest);

        if (!sv || !dv || dnpwcs > 0 || hasFwc != dHasFwc)
            return (null, ServerErrors.ErrBadSubject);

        var dtokMfTypes = new List<short>();
        var dtokMfIndexes = new List<int[]>();
        var dtokMfIntArgs = new List<int>();
        var dtokMfStringArgs = new List<string>();

        if (npwcs > 0 || hasFwc)
        {
            // Build source-token index map for partial wildcards.
            var sti = new Dictionary<int, int>();
            for (var i = 0; i < stokens.Length; i++)
            {
                if (stokens[i].Length == 1 && stokens[i][0] == SubjectTokens.Pwc)
                    sti[sti.Count + 1] = i;
            }

            var nphs = 0;
            foreach (var token in dtokens)
            {
                var (tt, tidxs, tint, tstr, terr) = IndexPlaceHolders(token);
                if (terr != null) return (null, terr);

                if (strict && tt != TransformType.NoTransform && tt != TransformType.Wildcard)
                    return (null, new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotSupportedForImport));

                if (tt == TransformType.NoTransform)
                {
                    dtokMfTypes.Add(TransformType.NoTransform);
                    dtokMfIndexes.Add([-1]);
                    dtokMfIntArgs.Add(-1);
                    dtokMfStringArgs.Add(SubjectTokens.Empty);
                }
                else if (tt == TransformType.Random)
                {
                    dtokMfTypes.Add(TransformType.Random);
                    dtokMfIndexes.Add([]);
                    dtokMfIntArgs.Add(tint);
                    dtokMfStringArgs.Add(SubjectTokens.Empty);
                }
                else
                {
                    nphs += tidxs.Length;
                    var stis = new List<int>();
                    foreach (var wildcardIndex in tidxs)
                    {
                        if (wildcardIndex > npwcs)
                            return (null, new MappingDestinationException(
                                $"{token}: [{wildcardIndex}]",
                                ServerErrors.ErrMappingDestinationIndexOutOfRange));
                        stis.Add(sti.GetValueOrDefault(wildcardIndex, 0));
                    }
                    dtokMfTypes.Add(tt);
                    dtokMfIndexes.Add([.. stis]);
                    dtokMfIntArgs.Add(tint);
                    dtokMfStringArgs.Add(tstr);
                }
            }

            if (strict && nphs < npwcs)
                return (null, new MappingDestinationException(dest, ServerErrors.ErrMappingDestinationNotUsingAllWildcards));
        }
        else
        {
            foreach (var token in dtokens)
            {
                var (tt, _, tint, _, terr) = IndexPlaceHolders(token);
                if (terr != null) return (null, terr);

                if (tt == TransformType.NoTransform)
                {
                    dtokMfTypes.Add(TransformType.NoTransform);
                    dtokMfIndexes.Add([-1]);
                    dtokMfIntArgs.Add(-1);
                    dtokMfStringArgs.Add(SubjectTokens.Empty);
                }
                else if (tt == TransformType.Random || tt == TransformType.Partition)
                {
                    dtokMfTypes.Add(tt);
                    dtokMfIndexes.Add([]);
                    dtokMfIntArgs.Add(tint);
                    dtokMfStringArgs.Add(SubjectTokens.Empty);
                }
                else
                {
                    return (null, new MappingDestinationException(token, ServerErrors.ErrMappingDestinationIndexOutOfRange));
                }
            }
        }

        return (new SubjectTransform(
            src, dest,
            dtokens, stokens,
            [.. dtokMfTypes], [.. dtokMfIndexes],
            [.. dtokMfIntArgs], [.. dtokMfStringArgs]), null);
    }

    /// <summary>
    /// Creates a non-strict transform. Mirrors <c>NewSubjectTransform</c>.
    /// </summary>
    public static (SubjectTransform? transform, Exception? err) New(string src, string dest) =>
        NewWithStrict(src, dest, false);

    /// <summary>
    /// Creates a strict transform (only Wildcard function allowed).
    /// Mirrors <c>NewSubjectTransformStrict</c>.
    /// </summary>
    public static (SubjectTransform? transform, Exception? err) NewStrict(string src, string dest) =>
        NewWithStrict(src, dest, true);

    /// <summary>
    /// Validates a subject mapping destination. Checks each token for valid syntax,
    /// validates mustache-style mapping functions against known regexes, then verifies
    /// the full transform can be created. Mirrors Go's <c>ValidateMapping</c>.
    /// </summary>
    public static Exception? ValidateMapping(string src, string dest)
    {
        if (string.IsNullOrEmpty(dest))
            return null;

        bool sfwc = false;
        foreach (var t in dest.Split(SubjectTokens.Btsep))
        {
            var length = t.Length;
            if (length == 0 || sfwc)
                return new MappingDestinationException(t, ServerErrors.ErrInvalidMappingDestinationSubject);

            // If it looks like a mapping function, validate against known patterns.
            if (length > 4 && t[0] == '{' && t[1] == '{' && t[length - 2] == '}' && t[length - 1] == '}')
            {
                if (!PartitionRe.IsMatch(t) &&
                    !WildcardRe.IsMatch(t) &&
                    !SplitFromLeftRe.IsMatch(t) &&
                    !SplitFromRightRe.IsMatch(t) &&
                    !SliceFromLeftRe.IsMatch(t) &&
                    !SliceFromRightRe.IsMatch(t) &&
                    !SplitRe.IsMatch(t) &&
                    !RandomRe.IsMatch(t))
                {
                    return new MappingDestinationException(t, ServerErrors.ErrUnknownMappingDestinationFunction);
                }
                continue;
            }

            if (length == 1 && t[0] == SubjectTokens.Fwc)
                sfwc = true;
            else if (t.AsSpan().ContainsAny("\t\n\f\r "))
                return ServerErrors.ErrInvalidMappingDestinationSubject;
        }

        // Verify that the transform can actually be created.
        var (_, err) = New(src, dest);
        return err;
    }

    /// <summary>
    /// Attempts to match a published subject against the source pattern.
    /// Returns the transformed subject or an error.
    /// Mirrors <c>subjectTransform.Match</c>.
    /// </summary>
    public (string result, Exception? err) Match(string subject)
    {
        if ((_src == SubjectTokens.Fwcs || _src == SubjectTokens.Empty) &&
            (_dest == SubjectTokens.Fwcs || _dest == SubjectTokens.Empty))
            return (subject, null);

        var tts = TokenizeSubject(subject);

        if (!IsValidLiteralSubject(tts))
            return (SubjectTokens.Empty, ServerErrors.ErrBadSubject);

        if (_src == SubjectTokens.Empty || _src == SubjectTokens.Fwcs ||
            IsSubsetMatch(tts, _src))
            return (TransformTokenizedSubject(tts), null);

        return (SubjectTokens.Empty, ServerErrors.ErrNoTransforms);
    }

    /// <summary>
    /// Transforms a dot-separated subject string.
    /// Mirrors <c>subjectTransform.TransformSubject</c>.
    /// </summary>
    public string TransformSubject(string subject) =>
        TransformTokenizedSubject(TokenizeSubject(subject));

    /// <summary>
    /// Core token-by-token transform engine.
    /// Mirrors <c>subjectTransform.TransformTokenizedSubject</c>.
    /// </summary>
    public string TransformTokenizedSubject(string[] tokens)
    {
        if (_dtokmftypes.Length == 0)
            return _dest;

        var b = new System.Text.StringBuilder();
        var li = _dtokmftypes.Length - 1;

        for (var i = 0; i < _dtokmftypes.Length; i++)
        {
            var mfType = _dtokmftypes[i];

            if (mfType == TransformType.NoTransform)
            {
                if (_dtoks[i].Length == 1 && _dtoks[i][0] == SubjectTokens.Fwc)
                    break;
                b.Append(_dtoks[i]);
            }
            else
            {
                switch (mfType)
                {
                    case TransformType.Partition:
                    {
                        byte[] keyBytes;
                        if (_dtokmftokindexesargs[i].Length > 0)
                        {
                            var sb = new System.Text.StringBuilder();
                            foreach (var srcTok in _dtokmftokindexesargs[i])
                                sb.Append(tokens[srcTok]);
                            keyBytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
                        }
                        else
                        {
                            keyBytes = System.Text.Encoding.UTF8.GetBytes(string.Join(".", tokens));
                        }
                        b.Append(GetHashPartition(keyBytes, _dtokmfintargs[i]));
                        break;
                    }
                    case TransformType.Wildcard:
                        if (_dtokmftokindexesargs.Length > i &&
                            _dtokmftokindexesargs[i].Length > 0 &&
                            tokens.Length > _dtokmftokindexesargs[i][0])
                        {
                            b.Append(tokens[_dtokmftokindexesargs[i][0]]);
                        }
                        break;
                    case TransformType.SplitFromLeft:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var pos = _dtokmfintargs[i];
                        if (pos > 0 && pos < src.Length)
                        {
                            b.Append(src[..pos]);
                            b.Append(SubjectTokens.Tsep);
                            b.Append(src[pos..]);
                        }
                        else
                        {
                            b.Append(src);
                        }
                        break;
                    }
                    case TransformType.SplitFromRight:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var pos = _dtokmfintargs[i];
                        if (pos > 0 && pos < src.Length)
                        {
                            b.Append(src[..(src.Length - pos)]);
                            b.Append(SubjectTokens.Tsep);
                            b.Append(src[(src.Length - pos)..]);
                        }
                        else
                        {
                            b.Append(src);
                        }
                        break;
                    }
                    case TransformType.SliceFromLeft:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var sz = _dtokmfintargs[i];
                        if (sz > 0 && sz < src.Length)
                        {
                            var j = 0;
                            while (j + sz <= src.Length)
                            {
                                if (j != 0) b.Append(SubjectTokens.Tsep);
                                b.Append(src[j..(j + sz)]);
                                if (j + sz != src.Length && j + sz + sz > src.Length)
                                {
                                    b.Append(SubjectTokens.Tsep);
                                    b.Append(src[(j + sz)..]);
                                    break;
                                }
                                j += sz;
                            }
                        }
                        else
                        {
                            b.Append(src);
                        }
                        break;
                    }
                    case TransformType.SliceFromRight:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var sz = _dtokmfintargs[i];
                        if (sz > 0 && sz < src.Length)
                        {
                            var rem = src.Length % sz;
                            if (rem > 0)
                            {
                                b.Append(src[..rem]);
                                b.Append(SubjectTokens.Tsep);
                            }
                            var j = rem;
                            while (j + sz <= src.Length)
                            {
                                b.Append(src[j..(j + sz)]);
                                if (j + sz < src.Length) b.Append(SubjectTokens.Tsep);
                                j += sz;
                            }
                        }
                        else
                        {
                            b.Append(src);
                        }
                        break;
                    }
                    case TransformType.Split:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var parts = src.Split(_dtokmfstringargs[i]);
                        for (var j = 0; j < parts.Length; j++)
                        {
                            if (parts[j] != SubjectTokens.Empty)
                                b.Append(parts[j]);
                            if (j < parts.Length - 1 &&
                                parts[j + 1] != SubjectTokens.Empty &&
                                !(j == 0 && parts[j] == SubjectTokens.Empty))
                                b.Append(SubjectTokens.Tsep);
                        }
                        break;
                    }
                    case TransformType.Left:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var sz = _dtokmfintargs[i];
                        b.Append(sz > 0 && sz < src.Length ? src[..sz] : src);
                        break;
                    }
                    case TransformType.Right:
                    {
                        var src = tokens[_dtokmftokindexesargs[i][0]];
                        var sz = _dtokmfintargs[i];
                        b.Append(sz > 0 && sz < src.Length ? src[(src.Length - sz)..] : src);
                        break;
                    }
                    case TransformType.Random:
                        b.Append(GetRandomPartition(_dtokmfintargs[i]));
                        break;
                }
            }

            if (i < li)
                b.Append(SubjectTokens.Btsep);
        }

        // Append remaining source tokens when destination ends with ">".
        if (_dtoks.Length > 0 && _dtoks[^1] == SubjectTokens.Fwcs)
        {
            var stokLen = _stoks.Length;
            for (var i = stokLen - 1; i < tokens.Length; i++)
            {
                b.Append(tokens[i]);
                if (i < tokens.Length - 1)
                    b.Append(SubjectTokens.Btsep);
            }
        }

        return b.ToString();
    }

    /// <summary>
    /// Reverses this transform (src ↔ dest).
    /// Mirrors <c>subjectTransform.reverse</c>.
    /// </summary>
    internal SubjectTransform? Reverse()
    {
        if (_dtokmftokindexesargs.Length == 0)
        {
            var (rtr, _) = NewStrict(_dest, _src);
            return rtr;
        }

        var (nsrc, phs) = TransformUntokenize(_dest);
        var nda = new List<string>();
        foreach (var token in _stoks)
        {
            if (token == SubjectTokens.Pwcs)
            {
                if (phs.Length == 0) return null;
                nda.Add(phs[0]);
                phs = phs[1..];
            }
            else
            {
                nda.Add(token);
            }
        }
        var ndest = string.Join(SubjectTokens.Tsep, nda);
        var (rtrFinal, _) = NewStrict(nsrc, ndest);
        return rtrFinal;
    }

    // -------------------------------------------------------------------------
    // Static helpers exposed internally
    // -------------------------------------------------------------------------

    /// <summary>
    /// Returns the args extracted from a mapping-function token using the given regex.
    /// Mirrors <c>getMappingFunctionArgs</c>.
    /// </summary>
    internal static string[]? GetMappingFunctionArgs(Regex functionRegex, string token)
    {
        var m = functionRegex.Match(token);
        if (m.Success && m.Groups.Count > 1)
            return CommaSep.Split(m.Groups[1].Value);
        return null;
    }

    /// <summary>
    /// Helper for transform functions that take (wildcardIndex, int) args.
    /// Mirrors <c>transformIndexIntArgsHelper</c>.
    /// </summary>
    internal static (short tt, int[] indexes, int intArg, string strArg, Exception? err)
        TransformIndexIntArgsHelper(string token, string[] args, short transformType)
    {
        if (args.Length < 2)
            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotEnoughArgs));
        if (args.Length > 2)
            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationTooManyArgs));

        if (!int.TryParse(args[0].Trim(), out var idx))
            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));

        if (!int.TryParse(args[1].Trim(), out var intVal))
            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));

        return (transformType, [idx], intVal, SubjectTokens.Empty, null);
    }

    /// <summary>
    /// Parses a destination token and returns its transform type and arguments.
    /// Mirrors <c>indexPlaceHolders</c>.
    /// </summary>
    internal static (short tt, int[] indexes, int intArg, string strArg, Exception? err)
        IndexPlaceHolders(string token)
    {
        var length = token.Length;
        if (length > 1)
        {
            if (token[0] == '$')
            {
                if (!int.TryParse(token[1..], out var tp))
                    return (TransformType.NoTransform, [-1], -1, SubjectTokens.Empty, null);
                return (TransformType.Wildcard, [tp], -1, SubjectTokens.Empty, null);
            }

            if (length > 4 && token[0] == '{' && token[1] == '{' &&
                token[length - 2] == '}' && token[length - 1] == '}')
            {
                // {{wildcard(n)}}
                var args = GetMappingFunctionArgs(WildcardRe, token);
                if (args != null)
                {
                    if (args.Length == 1 && args[0] == SubjectTokens.Empty)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotEnoughArgs));
                    if (args.Length == 1)
                    {
                        if (!int.TryParse(args[0].Trim(), out var ti))
                            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                        return (TransformType.Wildcard, [ti], -1, SubjectTokens.Empty, null);
                    }
                    return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                        new MappingDestinationException(token, ServerErrors.ErrMappingDestinationTooManyArgs));
                }

                // {{partition(n[,t1,t2,...])}}
                args = GetMappingFunctionArgs(PartitionRe, token);
                if (args != null)
                {
                    if (args.Length < 1)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotEnoughArgs));
                    if (!int.TryParse(args[0].Trim(), out var partN) || (long)partN > int.MaxValue)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                    if (args.Length == 1)
                        return (TransformType.Partition, [], partN, SubjectTokens.Empty, null);

                    var tidxs = new List<int>();
                    foreach (var t in args[1..])
                    {
                        if (!int.TryParse(t.Trim(), out var ti2))
                            return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                                new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                        tidxs.Add(ti2);
                    }
                    return (TransformType.Partition, [.. tidxs], partN, SubjectTokens.Empty, null);
                }

                // {{SplitFromLeft(t, n)}}
                args = GetMappingFunctionArgs(SplitFromLeftRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.SplitFromLeft);

                // {{SplitFromRight(t, n)}}
                args = GetMappingFunctionArgs(SplitFromRightRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.SplitFromRight);

                // {{SliceFromLeft(t, n)}}
                args = GetMappingFunctionArgs(SliceFromLeftRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.SliceFromLeft);

                // {{SliceFromRight(t, n)}}
                args = GetMappingFunctionArgs(SliceFromRightRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.SliceFromRight);

                // {{right(t, n)}}
                args = GetMappingFunctionArgs(RightRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.Right);

                // {{left(t, n)}}
                args = GetMappingFunctionArgs(LeftRe, token);
                if (args != null) return TransformIndexIntArgsHelper(token, args, TransformType.Left);

                // {{split(t, delim)}}
                args = GetMappingFunctionArgs(SplitRe, token);
                if (args != null)
                {
                    if (args.Length < 2)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotEnoughArgs));
                    if (args.Length > 2)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationTooManyArgs));
                    if (!int.TryParse(args[0].Trim(), out var splitIdx))
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                    if (args[1].Contains(' ') || args[1].Contains(SubjectTokens.Tsep))
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                    return (TransformType.Split, [splitIdx], -1, args[1], null);
                }

                // {{random(n)}}
                args = GetMappingFunctionArgs(RandomRe, token);
                if (args != null)
                {
                    if (args.Length != 1)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationNotEnoughArgs));
                    if (!int.TryParse(args[0].Trim(), out var randN) || (long)randN > int.MaxValue)
                        return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                            new MappingDestinationException(token, ServerErrors.ErrMappingDestinationInvalidArg));
                    return (TransformType.Random, [], randN, SubjectTokens.Empty, null);
                }

                return (TransformType.BadTransform, [], -1, SubjectTokens.Empty,
                    new MappingDestinationException(token, ServerErrors.ErrUnknownMappingDestinationFunction));
            }
        }

        return (TransformType.NoTransform, [-1], -1, SubjectTokens.Empty, null);
    }

    /// <summary>
    /// Tokenises a subject with wildcards into a formal transform destination.
    /// e.g. "foo.*.*" → "foo.$1.$2".
    /// Mirrors <c>transformTokenize</c>.
    /// </summary>
    public static string TransformTokenize(string subject)
    {
        var i = 1;
        var parts = new List<string>();
        foreach (var token in subject.Split(SubjectTokens.Btsep))
        {
            if (token == SubjectTokens.Pwcs)
            {
                parts.Add($"${i++}");
            }
            else
            {
                parts.Add(token);
            }
        }
        return string.Join(SubjectTokens.Tsep, parts);
    }

    /// <summary>
    /// Converts a transform destination back to a wildcard subject + placeholder list.
    /// Mirrors <c>transformUntokenize</c>.
    /// </summary>
    public static (string subject, string[] placeholders) TransformUntokenize(string subject)
    {
        var phs = new List<string>();
        var nda = new List<string>();

        foreach (var token in subject.Split(SubjectTokens.Btsep))
        {
            var args = GetMappingFunctionArgs(WildcardRe, token);
            var isWildcardPlaceholder =
                (token.Length > 1 && token[0] == '$' && token[1] >= '1' && token[1] <= '9') ||
                (args?.Length == 1 && args[0] != SubjectTokens.Empty);

            if (isWildcardPlaceholder)
            {
                phs.Add(token);
                nda.Add(SubjectTokens.Pwcs);
            }
            else
            {
                nda.Add(token);
            }
        }

        return (string.Join(SubjectTokens.Tsep, nda), [.. phs]);
    }

    /// <summary>
    /// Tokenises a subject into an array of dot-separated tokens.
    /// Mirrors <c>tokenizeSubject</c>.
    /// </summary>
    public static string[] TokenizeSubject(string subject) =>
        subject.Split(SubjectTokens.Btsep);

    /// <summary>
    /// Returns (valid, tokens, numPwcs, hasFwc) for a subject string.
    /// Mirrors <c>subjectInfo</c>.
    /// </summary>
    public static (bool valid, string[] tokens, int npwcs, bool hasFwc) SubjectInfo(string subject)
    {
        if (subject == string.Empty)
            return (false, [], 0, false);

        var npwcs = 0;
        var sfwc = false;
        var tokens = subject.Split(SubjectTokens.Tsep);
        foreach (var t in tokens)
        {
            if (t.Length == 0 || sfwc)
                return (false, [], 0, false);
            if (t.Length > 1) continue;
            switch (t[0])
            {
                case SubjectTokens.Fwc:
                    sfwc = true;
                    break;
                case SubjectTokens.Pwc:
                    npwcs++;
                    break;
            }
        }
        return (true, tokens, npwcs, sfwc);
    }

    // -------------------------------------------------------------------------
    // Internal helpers used by Match
    // -------------------------------------------------------------------------

    /// <summary>
    /// Returns true if all tokens are literal (no wildcards).
    /// Mirrors <c>isValidLiteralSubject</c> in server/sublist.go.
    /// </summary>
    internal static bool IsValidLiteralSubject(string[] tokens)
    {
        foreach (var t in tokens)
        {
            if (t.Length == 0) return false;
            if (t.Length == 1 && (t[0] == SubjectTokens.Pwc || t[0] == SubjectTokens.Fwc))
                return false;
        }
        return true;
    }

    /// <summary>
    /// Returns true if <paramref name="tokens"/> match the pattern <paramref name="test"/>.
    /// Mirrors <c>isSubsetMatch</c> in server/sublist.go.
    /// </summary>
    internal static bool IsSubsetMatch(string[] tokens, string test)
    {
        var testToks = TokenizeSubjectIntoSlice(test);
        return IsSubsetMatchTokenized(tokens, testToks);
    }

    private static string[] TokenizeSubjectIntoSlice(string subject)
    {
        var result = new List<string>();
        var start = 0;
        for (var i = 0; i < subject.Length; i++)
        {
            if (subject[i] == SubjectTokens.Btsep)
            {
                result.Add(subject[start..i]);
                start = i + 1;
            }
        }
        result.Add(subject[start..]);
        return [.. result];
    }

    private static bool IsSubsetMatchTokenized(string[] tokens, string[] test)
    {
        for (var i = 0; i < test.Length; i++)
        {
            if (i >= tokens.Length) return false;
            var t2 = test[i];
            if (t2.Length == 0) return false;
            if (t2.Length == 1 && t2[0] == SubjectTokens.Fwc) return true;

            var t1 = tokens[i];
            if (t1.Length == 0) return false;
            if (t1.Length == 1 && t1[0] == SubjectTokens.Fwc) return false;

            if (t1.Length == 1 && t1[0] == SubjectTokens.Pwc)
            {
                if (!(t2.Length == 1 && t2[0] == SubjectTokens.Pwc)) return false;
                if (i >= test.Length) return true;
                continue;
            }

            if (!(t2.Length == 1 && t2[0] == SubjectTokens.Pwc) &&
                string.Compare(t1, t2, StringComparison.Ordinal) != 0)
                return false;
        }
        return tokens.Length == test.Length;
    }

    private string GetRandomPartition(int ceiling)
    {
        if (ceiling == 0) return "0";
        return (Random.Shared.Next() % ceiling).ToString();
    }

    private static string GetHashPartition(byte[] key, int numBuckets)
    {
        if (numBuckets == 0) return "0";
        // FNV-1a 32-bit hash — mirrors fnv.New32a() in Go.
        const uint FnvPrime = 16777619;
        const uint FnvOffset = 2166136261;
        var hash = FnvOffset;
        foreach (var b in key) { hash ^= b; hash *= FnvPrime; }
        return ((int)(hash % (uint)numBuckets)).ToString();
    }
}
