using JSMR.Application.Common.Search;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace JSMR.Infrastructure.Common.Queries;

public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> : ISearchProvider<TItem, TCriteria, TSortField>
    where TCriteria : new()
    where TSortField : struct, Enum
{
    protected abstract bool UseSelectIdQuery { get; }

    public async Task<SearchResult<TItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default)
    {
        IQueryable<TBaseQuery> baseQuery = GetBaseQuery();
        IQueryable<TBaseQuery> filteredQuery = ApplyFilters(baseQuery, options.Criteria);

        int total = await filteredQuery.CountAsync(cancellationToken);

        IOrderedQueryable<TBaseQuery> orderedQuery = ApplySorting(filteredQuery, options.SortOptions);
        TItem[] items = await GetItemsAsync(options, orderedQuery, cancellationToken);

        await PostLoadAsync(items, cancellationToken);

        return new SearchResult<TItem>()
        {
            Items = items,
            TotalItems = total
        };
    }

    protected abstract IQueryable<TBaseQuery> GetBaseQuery();
    protected abstract IQueryable<TBaseQuery> ApplyFilters(IQueryable<TBaseQuery> query, TCriteria criteria);

    private IOrderedQueryable<TBaseQuery> ApplySorting(IQueryable<TBaseQuery> query, SortOption<TSortField>[] sortOptions)
    {
        IOrderedQueryable<TBaseQuery>? ordered = null;

        for (int i = 0; i < sortOptions.Length; i++)
        {
            var (field, direction) = (sortOptions[i].Field, sortOptions[i].Direction);
            bool isDescending = direction == SortDirection.Descending;

            IOrderedQueryable<TBaseQuery> applyFirst(Expression<Func<TBaseQuery, object?>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
            IOrderedQueryable<TBaseQuery> applyNext(Expression<Func<TBaseQuery, object?>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);

            Expression<Func<TBaseQuery, object?>> selector = GetSortExpression(field);

            ordered = (i == 0) ? applyFirst(selector) : applyNext(selector);
        }

        //return ordered ?? GetDefaultSortExpression(query);

        // Always add the default as the final tiebreaker
        var chain = GetDefaultSortChain();

        if (ordered is null)
        {
            using var e = chain.GetEnumerator();
            if (!e.MoveNext()) throw new InvalidOperationException("No default sort provided.");

            var (Selector, Dir) = e.Current;
            var res = Dir == SortDirection.Descending
                ? query.OrderByDescending(Selector)
                : query.OrderBy(Selector);

            while (e.MoveNext())
                res = e.Current.Dir == SortDirection.Descending
                    ? res.ThenByDescending(e.Current.Selector)
                    : res.ThenBy(e.Current.Selector);

            return res;
        }
        else
        {
            var res = ordered;
            foreach (var (sel, dir) in chain)
                res = dir == SortDirection.Descending ? res.ThenByDescending(sel) : res.ThenBy(sel);
            return res;
        }
    }

    private async Task<TItem[]> GetItemsAsync(SearchOptions<TCriteria, TSortField> options, IOrderedQueryable<TBaseQuery> orderedQuery, CancellationToken cancellationToken)
    {
        if (UseSelectIdQuery)
        {
            int[] ids = await GetSelectIdQuery(orderedQuery)
                .Skip((options.PageNumber - 1) * options.PageSize)
                .Take(options.PageSize)
                .ToArrayAsync(cancellationToken);

            Dictionary<int, TItem> items = await GetItems(options.Criteria, ids);

            return [.. ids.Select(uniqueId => items[uniqueId])];
        }
        else
        {
            IQueryable<TItem> selectQuery = GetSelectQuery(options.Criteria, orderedQuery);

            return await selectQuery
                .Skip((options.PageNumber - 1) * options.PageSize)
                .Take(options.PageSize)
                .ToArrayAsync(cancellationToken);
        }
    }

    protected abstract Expression<Func<TBaseQuery, object?>> GetSortExpression(TSortField field);
    protected abstract IEnumerable<(Expression<Func<TBaseQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain();
    protected abstract IQueryable<int> GetSelectIdQuery(IOrderedQueryable<TBaseQuery> query);
    protected abstract IQueryable<TItem> GetSelectQuery(TCriteria criteria, IOrderedQueryable<TBaseQuery> query);
    protected abstract Task<Dictionary<int, TItem>> GetItems(TCriteria criteria, int[] ids);
    protected virtual Task PostLoadAsync(IList<TItem> items, CancellationToken cancellationToken) => Task.CompletedTask;
}