using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;

namespace Processor;

internal static class Utils
{
    private static readonly Random RNG = new();

    /// <summary>
    /// Generates a code out of this name.
    /// </summary>
    /// <param name="name">The name where the code based on</param>
    /// <param name="db">The data class</param>
    /// <param name="selector">The selector to check, if that key already exists. The string parameter is the current code to check.</param>
    /// <returns>The generated code</returns>
    internal static async Task<string> GenerateCode<TDbSet>(string name, DbSet<TDbSet> db, Expression<Func<TDbSet, string, bool>> selector) where TDbSet : class
    {
        // Filter all non-alphanumeric characters from the name by allowing only A-Z, a-z, 0-9, and spaces from the ASCII table:
        name = new string(name.Where(c => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9' or ' ').ToArray());

        // Remove any whitespaces from the element name, regardless of how many e.g. spaces the user typed:
        var code = string.Join('_', name.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToUpperInvariant();
        
        //
        // The Any() query want's an Expression<T, bool>, but we have an Expression<T, string, bool>, though.
        // Therefore, we have to currying the string away:
        var typeDbSet = Expression.Parameter(typeof(TDbSet), null);
        var curriedSelector = Expression.Lambda<Func<TDbSet, bool>>(
            Expression.Invoke(selector, typeDbSet, Expression.Constant(code)),
            typeDbSet
        );
        
        // Check, if this key already exists. If so, add a random number to the end of the key:
        if (await db.AnyAsync(curriedSelector))
            while (await db.AnyAsync(curriedSelector))
            {
                code += $"_{RNG.Next(1, 10_000)}";
                
                // Due to the changed code & since the string is a constant, we have to re-currying the string away:
                curriedSelector = Expression.Lambda<Func<TDbSet, bool>>(
                    Expression.Invoke(selector, typeDbSet, Expression.Constant(code)),
                    typeDbSet
                );
            }

        return code;
    }
}