using System.Text.RegularExpressions;

namespace NATS.Server.Auth.Jwt;

/// <summary>
/// Expands mustache-style template strings in NATS JWT permission subjects.
/// When a user connects with a JWT, template strings in their permissions are
/// expanded using claim values from the user and account JWTs.
/// </summary>
/// <remarks>
/// Reference: Go auth.go:424-520 — processUserPermissionsTemplate()
///
/// Supported template functions:
///   {{name()}}             — user's Name claim
///   {{subject()}}          — user's Subject (NKey public key)
///   {{tag(tagname)}}       — user tags matching "tagname:" prefix (multi-value → cartesian product)
///   {{account-name()}}     — account display name
///   {{account-subject()}}  — account NKey public key
///   {{account-tag(tagname)}} — account tags matching "tagname:" prefix (multi-value → cartesian product)
///
/// When a template resolves to multiple values (e.g. a user with two "dept:" tags),
/// the cartesian product of all expanded subjects is returned. If any template
/// resolves to zero values, the entire pattern is dropped (returns empty list).
/// </remarks>
public static partial class PermissionTemplates
{
    [GeneratedRegex(@"\{\{([^}]+)\}\}")]
    private static partial Regex TemplateRegex();

    /// <summary>
    /// Expands a single permission pattern containing zero or more template expressions.
    /// Returns the list of concrete subjects after substitution.
    /// Returns an empty list if any template resolves to no values (tag not found).
    /// Returns a single-element list containing the original pattern if no templates are present.
    /// </summary>
    public static List<string> Expand(
        string pattern,
        string name, string subject,
        string accountName, string accountSubject,
        string[] userTags, string[] accountTags)
    {
        var matches = TemplateRegex().Matches(pattern);
        if (matches.Count == 0)
            return [pattern];

        var replacements = new List<(string Placeholder, string[] Values)>();
        foreach (Match match in matches)
        {
            var expr = match.Groups[1].Value.Trim();
            var values = ResolveTemplate(expr, name, subject, accountName, accountSubject, userTags, accountTags);
            if (values.Length == 0)
                return [];
            replacements.Add((match.Value, values));
        }

        // Compute cartesian product across all multi-value replacements.
        // Start with the full pattern and iteratively replace each placeholder.
        var results = new List<string> { pattern };
        foreach (var (placeholder, values) in replacements)
        {
            var next = new List<string>();
            foreach (var current in results)
                foreach (var value in values)
                    next.Add(current.Replace(placeholder, value));
            results = next;
        }
        return results;
    }

    /// <summary>
    /// Expands all patterns in a permission list, flattening multi-value expansions
    /// into the result. Patterns that resolve to no values are omitted entirely.
    /// </summary>
    public static List<string> ExpandAll(
        IEnumerable<string> patterns,
        string name, string subject,
        string accountName, string accountSubject,
        string[] userTags, string[] accountTags)
    {
        var result = new List<string>();
        foreach (var pattern in patterns)
            result.AddRange(Expand(pattern, name, subject, accountName, accountSubject, userTags, accountTags));
        return result;
    }

    private static string[] ResolveTemplate(
        string expr,
        string name, string subject,
        string accountName, string accountSubject,
        string[] userTags, string[] accountTags)
    {
        return expr.ToLowerInvariant() switch
        {
            "name()" => [name],
            "subject()" => [subject],
            "account-name()" => [accountName],
            "account-subject()" => [accountSubject],
            _ when expr.StartsWith("tag(", StringComparison.OrdinalIgnoreCase) => ResolveTags(expr, userTags),
            _ when expr.StartsWith("account-tag(", StringComparison.OrdinalIgnoreCase) => ResolveTags(expr, accountTags),
            _ => []
        };
    }

    /// <summary>
    /// Extracts the tag name from a tag() or account-tag() expression and returns
    /// all matching tag values from the provided tags array.
    /// Tags are stored in "key:value" format; this method returns the value portion.
    /// </summary>
    private static string[] ResolveTags(string expr, string[] tags)
    {
        var openParen = expr.IndexOf('(');
        var closeParen = expr.IndexOf(')');
        if (openParen < 0 || closeParen < 0)
            return [];

        var tagName = expr[(openParen + 1)..closeParen].Trim();
        var prefix = tagName + ":";
        return tags
            .Where(t => t.StartsWith(prefix, StringComparison.Ordinal))
            .Select(t => t[prefix.Length..])
            .ToArray();
    }
}
