using System.Security.Claims;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.VisualBasic;

namespace Fast.Shared.Logic;

public static partial class Util
{
    [GeneratedRegex(@"\$(orderby=.*?)&")]
    private static partial Regex OrderbyRegex();

    [GeneratedRegex(@"\$(filter=.*?)&")]
    private static partial Regex DealNumFilterRegex();

    [GeneratedRegex(@"DealNum,'(.+?)'|DealNum eq '(.+?)'")]
    private static partial Regex DealNumRegex();

    [GeneratedRegex(@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z")]
    private static partial Regex DateTimeRegex();

    [GeneratedRegex(@"'(.+?)'")]
    private static partial Regex SingleQuotesRegex();

    public const string AppUserIdClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

    public static string GetDbChanges(ChangeTracker tracker)
    {
        string changesMessage;
        List<string> messages = new();
        int added = 0;
        int modified = 0;
        int unchanged = 0;
        int deleted = 0;

        var entries = tracker.Entries().ToList();
        foreach (var entry in entries)
        {
            if (!entry.Metadata.ClrType.Name.Contains("Security"))
            {
                if (entry.State == EntityState.Added)
                    added++;
                else if (entry.State == EntityState.Modified)
                    modified++;
                else if (entry.State == EntityState.Unchanged)
                    unchanged++;
                else if (entry.State == EntityState.Deleted)
                    deleted++;
            }
        }

        if (added > 0)
            messages.Add(added + " record" + (added == 1 ? "" : "s") + " added");

        if (modified > 0)
            messages.Add(modified + " record" + (modified == 1 ? "" : "s") + " modified");

        if (unchanged > 0)
            messages.Add(unchanged + " record" + (unchanged == 1 ? "" : "s") + " unchanged");

        if (deleted > 0)
            messages.Add(deleted + " record" + (deleted == 1 ? "" : "s") + " deleted");

        if (messages.Count > 0)
            changesMessage = string.Join(", ", messages);
        else
            changesMessage = "No records were changed";

        return changesMessage;
    }

    public static async Task<AppUser> GetAppUserAsync(ClaimsPrincipal user)
    {
        var appUserId = GetAppUserId(user);
        var result = (await Main.AuthCache.GetAppUsers()).Where(x => x.Id == appUserId).First();
        return result;
    }

    public static int GetAppUserId(ClaimsPrincipal user)
    {
        if (!ProgramShared.IsAuthenticationEnabled)
            return FastValues.MasterAppUserId;

        var claimStringValue = user.Claims.FirstOrDefault(c => c.Type == AppUserIdClaimType)?.Value;
        var isValidClaim = int.TryParse(claimStringValue, out int appUserId);
        if (!isValidClaim)
            return FastValues.MasterAppUserId;

        return appUserId;
    }

    public static async Task<string> GetNewDealNumAsync(int transactionTypeId, MyDbContext db)
    {
        // this method should always be used within a db transaction so that it can be rolled back on error
        var ticket = await db.Tickets.Where(x => x.TransactionTypeId == transactionTypeId).FirstAsync();
        ticket.TicketNumber += 1;
        await db.SaveChangesAsync();
        string ticketNum = ticket.TicketPrefix + ticket.TicketNumber.ToString();
        return ticketNum;
    }

    public static ODataQueryOptions<T> GetQueryOptionsWithConvertedDates<T>(ODataQueryOptions<T> queryOptions)
    {
        //this method may need to be updated to handle date-time columns as well since it is currently converting all date-time values to date-only values
        //the method might no longer be needed if the Kendo controls are updated to use date-only values instead of date-time values
        //it might also be related to general Odata or EF core issues: https://github.com/OData/AspNetCoreOData/issues/1473#issuecomment-2856880616
        //or https://feedback.telerik.com/aspnet-core-ui/1674242-dateonly-field-cannot-be-filtered-in-a-grid-configured-for-odata-binding

        var queryString = queryOptions.Request.QueryString;
        string queryStringVal = queryString.HasValue && queryString.Value != null ? Uri.UnescapeDataString(queryString.Value) : "";

        // Replace each date-time string with its date-only part
        queryStringVal = DateTimeRegex().Replace(queryStringVal, match =>
        {
            if (DateTime.TryParse(match.Value, out DateTime parsedDate))
                return parsedDate.ToString("yyyy-MM-dd");

            return match.Value;
        });

        queryOptions = GetNewOdataOptionsFromQueryString(queryOptions, queryStringVal);
        return queryOptions;
    }


    public static ODataQueryOptions<T> GetQueryOptionsWithTicketSort<T>(ODataQueryOptions<T> queryOptions, out bool hasDealNumFilter)
    {
        var queryString = queryOptions.Request.QueryString;
        string queryStringVal = queryString.HasValue && queryString.Value != null ? Uri.UnescapeDataString(queryString.Value) : "";
        string oldOrderbyClause = OrderbyRegex().Match(queryStringVal).Groups[1].Value;
        string? newOrderbyClause = null;
        if (oldOrderbyClause.Contains("DealNum desc"))
            newOrderbyClause = oldOrderbyClause.Replace("DealNum desc", "TicketPrefix desc,TicketNumber desc");
        else if (oldOrderbyClause.Contains("DealNum"))
            newOrderbyClause = oldOrderbyClause.Replace("DealNum", "TicketPrefix,TicketNumber");

        //check if there is any DealNum filter and if so then append a new 'OR' condition with the specified DealNum
        string oldFilterClause = DealNumFilterRegex().Match(queryStringVal).Groups[1].Value;
        string? newFilterClause = null;
        string? filteredDealNum = null;
        foreach (Match match in DealNumRegex().Matches(oldFilterClause).Cast<Match>())
        {
            if (filteredDealNum == null)
            {
                if (!string.IsNullOrWhiteSpace(match.Groups[1].Value))
                    filteredDealNum = match.Groups[1].Value;

                if (!string.IsNullOrWhiteSpace(match.Groups[2].Value))
                    filteredDealNum = match.Groups[2].Value;
            }
        }

        if (filteredDealNum != null)
        {
            newFilterClause = $"{oldFilterClause} or DealNum eq '{filteredDealNum}'";
            hasDealNumFilter = true;
        }
        else
            hasDealNumFilter = false;

        if (newFilterClause != null || newOrderbyClause != null)
        {
            if (newFilterClause != null)
                queryStringVal = queryStringVal.Replace(oldFilterClause, newFilterClause);

            if (newOrderbyClause != null)
                queryStringVal = queryStringVal.Replace(oldOrderbyClause, newOrderbyClause);
        }

        queryOptions = GetNewOdataOptionsFromQueryString(queryOptions, queryStringVal);
        return queryOptions;
    }

    private static ODataQueryOptions<T> GetNewOdataOptionsFromQueryString<T>(ODataQueryOptions<T> queryOptions, string queryString)
    {
        //encode all data inside single quote marks
        foreach (Match match in SingleQuotesRegex().Matches(queryString).Cast<Match>())
        {
            var matchGroupStr = match.Groups[1].Value;
            if (!string.IsNullOrWhiteSpace(matchGroupStr))
                queryString = queryString.Replace(matchGroupStr, Uri.EscapeDataString(matchGroupStr));
        }

        queryOptions.Request.QueryString = new QueryString(queryString);
        queryOptions = new ODataQueryOptions<T>(queryOptions.Context, queryOptions.Request);

        return queryOptions;
    }

    public static double Nz(double? o)
    {
        double ReturnValue = 0;
        try
        {
            if ((o == null || Information.IsDBNull(o) || o?.ToString().Length == 0))
                return ReturnValue;
            else
                return o.GetValueOrDefault();
        }
        catch (Exception)
        {
            return ReturnValue;
        }
    }

    public static int Nz(int? o)
    {
        int ReturnValue = 0;
        try
        {
            if ((o == null || Information.IsDBNull(o) || o?.ToString().Length == 0))
                return ReturnValue;
            else
                return o.GetValueOrDefault();
        }
        catch (Exception)
        {
            return ReturnValue;
        }
    }

    public static double Nz(string? o)
    {
        double ReturnValue = 0;
        try
        {
            if ((o == null || Information.IsDBNull(o) || o.ToString().Length == 0))
                return ReturnValue;
            else
                return Convert.ToDouble(o);
        }
        catch (Exception)
        {
            return ReturnValue;
        }
    }

    public static byte[] GetExceptionFilesBytes(Exception exception, string messagePrefix = "")
    {
        if (!string.IsNullOrEmpty(messagePrefix))
            messagePrefix += Environment.NewLine;

        string errorMsg = messagePrefix + String.GetExceptionMessage(exception);

        byte[]? bytes = null;
        using (var ms = new MemoryStream())
        {
            TextWriter tw = new StreamWriter(ms);
            tw.Write(errorMsg);
            tw.Flush();
            ms.Position = 0;
            bytes = ms.ToArray();
        }

        return bytes;
    }
}
