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

namespace e_suite.Utilities.Pagination;

public class PaginatedData<T> : IPaginatedData<T>
{
    public long Count { get; set; }
    public int PageSize { get; set; }
    public int Page { get; set; }
    public int TotalPages => Convert.ToInt32(Math.Ceiling(Count / Convert.ToDecimal(PageSize)));
    public IEnumerable<T> Data { get; set; } = null!;
}

public static class PaginatedData
{
    public static async Task<PaginatedData<T>> Paginate<T>(
        IQueryable<T> queryable,
        Paging paging,
        Func<string, Expression<Func<T, object>>> keySelector,
        CancellationToken cancellationToken
    )
    {
        var filteredData = ApplyFilters(queryable, paging.Filters, keySelector);

        var paginated = new PaginatedData<T>
        {
            Count = await filteredData.CountAsync(cancellationToken),
            Page = paging.Page,
            PageSize = paging.PageSize
        };

        if (paginated.Page > paginated.TotalPages) paginated.Page = paginated.TotalPages;
        if (paginated.Page < 1) paginated.Page = 1;

        var sortedData = ApplySort(filteredData, paging.SortKey, paging.SortAscending, keySelector);
        paginated.Data = paging.Page == 0
            ? sortedData
            : sortedData.Skip((paginated.Page - 1) * paginated.PageSize).Take(paginated.PageSize);
        return paginated;
    }

    public static async Task<PaginatedData<TOutput>> Paginate<TInput, TOutput>(
        IQueryable<TInput> queryable,
        Paging paging,
        Func<string, Expression<Func<TInput, object>>> keySelector,
        CancellationToken cancellationToken
    )
    {
        var paged = await Paginate(queryable, paging, keySelector, cancellationToken);
        
        var result = new PaginatedData<TOutput>
        {
            Count = paged.Count,
            Page = paged.Page,
            PageSize = paged.PageSize,
            Data = paged.Data.Select(item => (TOutput)Activator.CreateInstance(typeof(TOutput), item)!)
        };

        return result;
    }

    public static async Task<PaginatedData<TOutput>> Paginate<TInput, TOutput>(
        IQueryable<TInput> queryable,
        Paging paging,
        Func<string, Expression<Func<TInput, object>>> keySelector,
        CancellationToken cancellationToken,
        params object[] args
    )
    {
        var paged = await Paginate(queryable, paging, keySelector, cancellationToken);

        var result = new PaginatedData<TOutput>
        {
            Count = paged.Count,
            Page = paged.Page,
            PageSize = paged.PageSize,
            Data = paged.Data.Select(item =>
            {
                var allArgs = new List<object> { item! };
                allArgs.AddRange(args);

                return (TOutput)Activator.CreateInstance(typeof(TOutput), allArgs.ToArray())!;
            })
        };

        return result;
    }


    private static Expression<Func<T, bool>> BuildContainsFilter<T>(
        Expression<Func<T, object>> accessor,
        string value)
    {
        var param = accessor.Parameters[0];
        Expression body = accessor.Body;

        // Unwrap boxing: (object)x.Id → x.Id
        if (body is UnaryExpression unary && unary.NodeType == ExpressionType.Convert)
            body = unary.Operand;

        // If the accessor already returns a string, use it directly
        if (body.Type == typeof(string))
        {
            // x => x.Property != null && x.Property.ToLower().Contains(value)
            var notNull = Expression.NotEqual(body, Expression.Constant(null));
            var toLower = Expression.Call(body, nameof(string.ToLower), Type.EmptyTypes);
            var contains = Expression.Call(
                toLower,
                nameof(string.Contains),
                Type.EmptyTypes,
                Expression.Constant(value.ToLower())
            );

            var and = Expression.AndAlso(notNull, contains);
            return Expression.Lambda<Func<T, bool>>(and, param);
        }

        // Otherwise: convert to string safely
        var toString = Expression.Call(body, "ToString", Type.EmptyTypes);
        var toLowerCall = Expression.Call(toString, nameof(string.ToLower), Type.EmptyTypes);
        var containsCall = Expression.Call(
            toLowerCall,
            nameof(string.Contains),
            Type.EmptyTypes,
            Expression.Constant(value.ToLower())
        );

        return Expression.Lambda<Func<T, bool>>(containsCall, param);
    }

    private static IQueryable<T> ApplyFilters<T>(
        IQueryable<T> queryable,
        string filters,
        Func<string, Expression<Func<T, object>>> keySelector)
    {
        if (string.IsNullOrWhiteSpace(filters))
            return queryable;

        var filtersDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(filters);

        if (filtersDictionary == null)
            return queryable;

        var filteredQueryable = queryable;

        foreach (var filter in filtersDictionary)
        {
            if (!string.IsNullOrWhiteSpace(filter.Value))
            {
                var accessor = keySelector(filter.Key);
                var filterExpr = BuildContainsFilter(accessor, filter.Value);
                filteredQueryable = filteredQueryable.Where(filterExpr);
            }
        }

        return filteredQueryable;
    }


    private static IQueryable<T> ApplySort<T>(IQueryable<T> sequences, string sortKey, bool sortAscending,
        Func<string, Expression<Func<T, object>>> keySelector)
    {
        if (sortAscending)
            return sequences.OrderBy(keySelector(sortKey));
        return sequences.OrderByDescending(keySelector(sortKey));
    }
}