namespace NATS.Server.Subscriptions;

public static class SubjectMatch
{
    public const char Pwc = '*';   // partial wildcard
    public const char Fwc = '>';   // full wildcard
    public const char Sep = '.';   // token separator

    public static bool IsValidSubject(string subject)
    {
        if (string.IsNullOrEmpty(subject))
            return false;

        bool sawFwc = false;
        int start = 0;

        for (int i = 0; i <= subject.Length; i++)
        {
            if (i == subject.Length || subject[i] == Sep)
            {
                int tokenLen = i - start;
                if (tokenLen == 0 || sawFwc)
                    return false;

                if (tokenLen == 1)
                {
                    char c = subject[start];
                    if (c == Fwc)
                        sawFwc = true;
                    else if (c is ' ' or '\t' or '\n' or '\r' or '\f')
                        return false;
                }
                else
                {
                    for (int j = start; j < i; j++)
                    {
                        char c = subject[j];
                        if (c is ' ' or '\t' or '\n' or '\r' or '\f')
                            return false;
                    }
                }

                start = i + 1;
            }
        }

        return true;
    }

    public static bool IsLiteral(string subject)
    {
        for (int i = 0; i < subject.Length; i++)
        {
            char c = subject[i];
            if (c is Pwc or Fwc)
            {
                bool atStart = i == 0 || subject[i - 1] == Sep;
                bool atEnd = i + 1 == subject.Length || subject[i + 1] == Sep;
                if (atStart && atEnd)
                    return false;
            }
        }

        return true;
    }

    public static bool IsValidPublishSubject(string subject)
    {
        return IsValidSubject(subject) && IsLiteral(subject);
    }

    /// <summary>
    /// Match a literal subject against a pattern that may contain wildcards.
    /// </summary>
    public static bool MatchLiteral(string literal, string pattern)
    {
        int li = 0, pi = 0;

        while (pi < pattern.Length)
        {
            // Get next pattern token
            int pTokenStart = pi;
            while (pi < pattern.Length && pattern[pi] != Sep)
                pi++;
            int pTokenLen = pi - pTokenStart;
            if (pi < pattern.Length)
                pi++; // skip separator

            // Full wildcard -- matches everything remaining
            if (pTokenLen == 1 && pattern[pTokenStart] == Fwc)
                return li < literal.Length; // must have at least one token left

            // Get next literal token
            if (li >= literal.Length)
                return false;
            int lTokenStart = li;
            while (li < literal.Length && literal[li] != Sep)
                li++;
            int lTokenLen = li - lTokenStart;
            if (li < literal.Length)
                li++; // skip separator

            // Partial wildcard -- matches any single token
            if (pTokenLen == 1 && pattern[pTokenStart] == Pwc)
                continue;

            // Literal comparison
            if (pTokenLen != lTokenLen)
                return false;
            if (string.Compare(literal, lTokenStart, pattern, pTokenStart, pTokenLen, StringComparison.Ordinal) != 0)
                return false;
        }

        return li >= literal.Length; // both exhausted
    }

    /// <summary>Count dot-delimited tokens. Empty string returns 0.</summary>
    public static int NumTokens(string subject)
    {
        if (string.IsNullOrEmpty(subject))
            return 0;
        int count = 1;
        for (int i = 0; i < subject.Length; i++)
        {
            if (subject[i] == Sep)
                count++;
        }
        return count;
    }

    /// <summary>Return the 0-based nth token as a span. Returns empty if out of range.</summary>
    public static ReadOnlySpan<char> TokenAt(string subject, int index)
    {
        if (string.IsNullOrEmpty(subject))
            return default;

        var span = subject.AsSpan();
        int current = 0;
        int start = 0;
        for (int i = 0; i < span.Length; i++)
        {
            if (span[i] == Sep)
            {
                if (current == index)
                    return span[start..i];
                start = i + 1;
                current++;
            }
        }
        if (current == index)
            return span[start..];
        return default;
    }

    /// <summary>
    /// Determines if two subject patterns (possibly containing wildcards) can both
    /// match the same literal subject. Reference: Go sublist.go SubjectsCollide.
    /// </summary>
    public static bool SubjectsCollide(string subj1, string subj2)
    {
        if (subj1 == subj2)
            return true;

        bool lit1 = IsLiteral(subj1);
        bool lit2 = IsLiteral(subj2);

        if (lit1 && lit2)
            return false;

        if (lit1 && !lit2)
            return MatchLiteral(subj1, subj2);
        if (lit2 && !lit1)
            return MatchLiteral(subj2, subj1);

        // Both have wildcards — split once to avoid O(n²) TokenAt calls
        var tokens1 = subj1.Split(Sep);
        var tokens2 = subj2.Split(Sep);
        int n1 = tokens1.Length;
        int n2 = tokens2.Length;
        bool hasFwc1 = tokens1[^1] == ">";
        bool hasFwc2 = tokens2[^1] == ">";

        if (!hasFwc1 && !hasFwc2 && n1 != n2)
            return false;
        if (n1 < n2 && !hasFwc1)
            return false;
        if (n2 < n1 && !hasFwc2)
            return false;

        int stop = Math.Min(n1, n2);
        for (int i = 0; i < stop; i++)
        {
            if (!TokensCanMatch(tokens1[i], tokens2[i]))
                return false;
        }
        return true;
    }

    private static bool TokensCanMatch(ReadOnlySpan<char> t1, ReadOnlySpan<char> t2)
    {
        if (t1.Length == 1 && (t1[0] == Pwc || t1[0] == Fwc))
            return true;
        if (t2.Length == 1 && (t2[0] == Pwc || t2[0] == Fwc))
            return true;
        return t1.SequenceEqual(t2);
    }

    /// <summary>
    /// Validates subject. When checkRunes is true, also rejects null bytes.
    /// </summary>
    public static bool IsValidSubject(string subject, bool checkRunes)
    {
        if (!IsValidSubject(subject))
            return false;
        if (!checkRunes)
            return true;
        for (int i = 0; i < subject.Length; i++)
        {
            if (subject[i] == '\0')
                return false;
        }
        return true;
    }
}
