using JSMR.Application.Common.Search;
using JSMR.Application.Enums;
using JSMR.Application.VoiceWorks.Queries.Search;
using JSMR.Domain.Entities;
using JSMR.Domain.Enums;
using JSMR.Domain.ValueObjects;
using JSMR.Infrastructure.Common.Queries;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
using CircleStatus = JSMR.Application.VoiceWorks.Queries.Search.CircleStatus;

namespace JSMR.Infrastructure.Data.Repositories.VoiceWorks;

public class VoiceWorkQuery
{
    public required VoiceWork VoiceWork { get; init; }
    public EnglishVoiceWork? EnglishVoiceWork { get; init; }
    public required Circle Circle { get; init; }
    //public VoiceWorkLocalization? VoiceWorkLocalization { get; init; }
    //public VoiceWorkSearch? VoiceWorkSearch { get; init; }
}

public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSearch fullTextSearch) : SearchProvider<VoiceWorkSearchResult, VoiceWorkSearchCriteria, VoiceWorkSortField, VoiceWorkQuery>, IVoiceWorkSearchProvider
{
    protected override bool UseSelectIdQuery => true;

    protected override IQueryable<VoiceWorkQuery> GetBaseQuery()
    {
        return
            from voiceWork in context.VoiceWorks.AsNoTracking()
            join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps
            from englishVoiceWork in ps.DefaultIfEmpty()
            join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs
            from circle in cs.DefaultIfEmpty()
            //join voiceWorkLocalization in context.VoiceWorkLocalizations on voiceWork.VoiceWorkId equals voiceWorkLocalization.VoiceWorkId into vwl
            //from voiceWorkLocalization in vwl.DefaultIfEmpty()
            //join voiceWorkSearch in context.VoiceWorkSearches on voiceWork.VoiceWorkId equals voiceWorkSearch.VoiceWorkId into vws
            //from voiceWorkSearch in vws.DefaultIfEmpty()
            select new VoiceWorkQuery
            {
                VoiceWork = voiceWork,
                EnglishVoiceWork = englishVoiceWork,
                Circle = circle
            };
    }

    protected override IQueryable<VoiceWorkQuery> ApplyFilters(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        IQueryable<VoiceWorkQuery> filteredQuery = query;

        filteredQuery = ApplyKeywordsFilter(filteredQuery, criteria);

        switch (criteria.SaleStatus)
        {
            case SaleStatus.Available:
                filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate != null);
                break;
            case SaleStatus.Upcoming:
                filteredQuery = filteredQuery.Where(x => x.VoiceWork.ExpectedDate != null);
                break;
        }

        //if (criteria.SupportedLanguages.Length > 0)
        //    filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.AsEnumerable().Contains((Language)x.VoiceWork.SubtitleLanguage));

        filteredQuery = ApplySupportedLanguageFilter(filteredQuery, criteria);
        filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria);
        filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria);
        filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria);
        filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria);
        filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria);

        if (criteria.Status is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.Status == (byte)criteria.Status);

        if (criteria.ScheduledReleaseDateStart is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.PlannedReleaseDate >= criteria.ScheduledReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));

        if (criteria.ScheduledReleaseDateEnd is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.PlannedReleaseDate <= criteria.ScheduledReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue));

        if (criteria.ReleaseDateStart is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));

        if (criteria.ReleaseDateEnd is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue));

        if (criteria.AgeRatings.Length > 0)
            filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.AsEnumerable().Contains((AgeRating)x.VoiceWork.Rating));

        //if (criteria.SupportedLanguages.Length > 0)
        //    filteredQuery = filteredQuery.Where(x => criteria.SupportedLanguages.Contains((Language)x.VoiceWork.SubtitleLanguage));

        if (criteria.AIGenerationOptions.Length > 0)
            filteredQuery = filteredQuery.Where(x => criteria.AIGenerationOptions.AsEnumerable().Contains((AIGeneration)x.VoiceWork.AIGeneration));

        if (criteria.ShowFavoriteVoiceWorks)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.Favorite);

        if (criteria.ShowInvalidVoiceWorks)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.IsValid != true);

        if (criteria.MinDownloads is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads >= criteria.MinDownloads.Value);

        if (criteria.MaxDownloads is not null)
            filteredQuery = filteredQuery.Where(x => x.VoiceWork.Downloads <= criteria.MaxDownloads.Value);

        return filteredQuery;
    }

    private IQueryable<VoiceWorkQuery> ApplyKeywordsFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        if (string.IsNullOrWhiteSpace(criteria.Keywords))
            return query;

        var voiceWorkIds = fullTextSearch.MatchingIds(context, criteria.Keywords);

        return query.Where(x => voiceWorkIds.Contains(x.VoiceWork.VoiceWorkId));
    }


    private IQueryable<VoiceWorkQuery> ApplySupportedLanguageFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.SupportedLanguages.Length == 0)
            return query;

        List<string> languageCodes = [];

        foreach (Language language in criteria.SupportedLanguages)
        {
            if (SupportedLanguage.TryFromLanguage(language, out SupportedLanguage? supportedLanguage))
                languageCodes.Add(supportedLanguage.Code);
        }

        if (languageCodes.Count == 0)
            return query;

        return query.Where(q => context.VoiceWorkSupportedLanguages.Any(sl => sl.VoiceWorkId == q.VoiceWork.VoiceWorkId && languageCodes.Contains(sl.Language)));
    }

    private IQueryable<VoiceWorkQuery> ApplyCircleStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.CircleStatus is null)
            return query;

        switch (criteria.CircleStatus)
        {
            case CircleStatus.NotBlacklisted:
                query = query.Where(q =>
                    !context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted));
                break;

            case CircleStatus.Blacklisted:
                query = query.Where(q =>
                    context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Blacklisted));
                break;

            case CircleStatus.Favorited:
                query = query.Where(q =>
                    context.Circles.Any(c => c.CircleId == q.VoiceWork.CircleId && c.Favorite));
                break;
        }

        //switch (criteria.CircleStatus)
        //{
        //    case CircleStatus.NotBlacklisted:
        //        query = query.Where(x => x.Circle.Blacklisted == false);
        //        break;
        //    case CircleStatus.Favorited:
        //        query = query.Where(x => x.Circle.Favorite);
        //        break;
        //    case CircleStatus.Blacklisted:
        //        query = query.Where(x => x.Circle.Blacklisted);
        //        break;
        //}

        return query;
    }

    private IQueryable<VoiceWorkQuery> ApplyTagStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.TagStatus is null)
            return query;

        // Handy local predicates that translate to EXISTS subqueries
        bool HasFav(int voiceWorkId) =>
            context.VoiceWorkTags
                   .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                   .Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Favorite);

        bool HasBlk(int voiceWorkId) =>
            context.VoiceWorkTags
                   .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                   .Any(x => x.vwt.VoiceWorkId == voiceWorkId && x.t.Blacklisted);

        return criteria.TagStatus switch
        {
            TagStatus.NotBlacklisted =>
                query.Where(q => !context.VoiceWorkTags
                    .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                    .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)),

            TagStatus.Blacklisted =>
                query.Where(q => context.VoiceWorkTags
                    .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                    .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)),

            TagStatus.FavoriteIncludeBlacklist =>
                query.Where(q => context.VoiceWorkTags
                    .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                    .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite)),

            TagStatus.FavoriteExcludeBlacklist =>
                query.Where(q =>
                    context.VoiceWorkTags
                        .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                        .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Favorite)
                    &&
                    !context.VoiceWorkTags
                        .Join(context.Tags, vwt => vwt.TagId, t => t.TagId, (vwt, t) => new { vwt, t })
                        .Any(x => x.vwt.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.t.Blacklisted)
                ),

            _ => query
        };
    }

    private IQueryable<VoiceWorkQuery> ApplyCreatorStatusFilter(IQueryable<VoiceWorkQuery> query, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.CreatorStatus is null)
            return query;

        // Handy local predicates that translate to EXISTS subqueries
        bool HasFav(int voiceWorkId) =>
            context.VoiceWorkCreators
                   .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                   .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Favorite);

        bool HasBlk(int voiceWorkId) =>
            context.VoiceWorkCreators
                   .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                   .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Blacklisted);

        return criteria.CreatorStatus switch
        {
            CreatorStatus.NotBlacklisted =>
                query.Where(q => !context.VoiceWorkCreators
                    .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                    .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)),

            CreatorStatus.Blacklisted =>
                query.Where(q => context.VoiceWorkCreators
                    .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                    .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)),

            CreatorStatus.FavoriteIncludeBlacklist =>
                query.Where(q => context.VoiceWorkCreators
                    .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                    .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)),

            CreatorStatus.FavoriteExcludeBlacklist =>
                query.Where(q =>
                    context.VoiceWorkCreators
                        .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                        .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)
                    &&
                    !context.VoiceWorkCreators
                        .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c })
                        .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)
                ),

            _ => query
        };
    }

    private IQueryable<VoiceWorkQuery> ApplyTagIdsFilter(IQueryable<VoiceWorkQuery> filteredQuery, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.TagIds.Length == 0)
            return filteredQuery;

        if (criteria.IncludeAllTags == false)
        {
            var tagQuery =
                from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
                where criteria.TagIds.Contains(voiceWorkTag.TagId)
                select new { voiceWorkTag };

            var finalTagQuery = tagQuery.Select(x => x.voiceWorkTag.VoiceWorkId);

            filteredQuery = filteredQuery.Where(x => finalTagQuery.Contains(x.VoiceWork.VoiceWorkId));
        }
        else
        {
            foreach (int tagId in criteria.TagIds)
            {
                var tagIdQuery =
                    from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
                    where voiceWorkTag.TagId == tagId
                    select voiceWorkTag.VoiceWorkId;

                filteredQuery =
                    from query in filteredQuery
                    join voiceWorkId in tagIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId
                    select new VoiceWorkQuery
                    {
                        VoiceWork = query.VoiceWork,
                        EnglishVoiceWork = query.EnglishVoiceWork,
                        Circle = query.Circle
                    };
            }
        }

        return filteredQuery;
    }

    private IQueryable<VoiceWorkQuery> ApplyCreatorIdsFilter(IQueryable<VoiceWorkQuery> filteredQuery, VoiceWorkSearchCriteria criteria)
    {
        if (criteria.CreatorIds.Length == 0)
            return filteredQuery;

        if (criteria.IncludeAllCreators == false)
        {
            var creatorQuery =
                from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
                where criteria.CreatorIds.Contains(voiceWorkCreator.CreatorId)
                select new { voiceWorkCreator };

            var finalCreatorQuery = creatorQuery.Select(x => x.voiceWorkCreator.VoiceWorkId);

            filteredQuery = filteredQuery.Where(x => finalCreatorQuery.Contains(x.VoiceWork.VoiceWorkId));
        }
        else
        {
            foreach (int creatorId in criteria.CreatorIds)
            {
                var creatorIdQuery =
                    from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
                    where voiceWorkCreator.CreatorId == creatorId
                    select voiceWorkCreator.VoiceWorkId;

                filteredQuery =
                    from query in filteredQuery
                    join voiceWorkId in creatorIdQuery on query.VoiceWork.VoiceWorkId equals voiceWorkId
                    select new VoiceWorkQuery
                    {
                        VoiceWork = query.VoiceWork,
                        EnglishVoiceWork = query.EnglishVoiceWork,
                        Circle = query.Circle
                    };
            }
        }

        return filteredQuery;
    }

    protected override IEnumerable<(Expression<Func<VoiceWorkQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
    {
        yield return (x => x.VoiceWork.ProductId, SortDirection.Descending);
    }

    protected override Expression<Func<VoiceWorkQuery, object?>> GetSortExpression(VoiceWorkSortField field) => field switch
    {
        VoiceWorkSortField.ExpectedReleaseDate => x => x.VoiceWork.ExpectedDate,
        VoiceWorkSortField.ScheduledReleaseDate => x => x.VoiceWork.PlannedReleaseDate,
        VoiceWorkSortField.ReleaseDate => x => x.VoiceWork.SalesDate,
        VoiceWorkSortField.AnyReleaseDate => x => x.VoiceWork.SalesDate ?? x.VoiceWork.PlannedReleaseDate ?? x.VoiceWork.ExpectedDate,
        VoiceWorkSortField.Downloads => x => x.VoiceWork.Downloads ?? 0,
        VoiceWorkSortField.WishlistCount => x => x.VoiceWork.WishlistCount ?? 0,
        VoiceWorkSortField.StarRating => x => x.VoiceWork.StarRating ?? 0,
        VoiceWorkSortField.FavoriteCircle => x => !x.Circle.Favorite,
        _ => x => x.VoiceWork.ProductId
    };

    protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<VoiceWorkQuery> query)
    {
        return query.Select(x => x.VoiceWork.VoiceWorkId);
    }

    protected override IQueryable<VoiceWorkSearchResult> GetSelectQuery(VoiceWorkSearchCriteria criteria, IOrderedQueryable<VoiceWorkQuery> query)
    {
        var result =
            from q in query
            let voiceWork = q.VoiceWork
            let englishVoiceWork = q.EnglishVoiceWork
            let circle = q.Circle
            let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce"
            select new VoiceWorkSearchResult()
            {
                VoiceWorkId = voiceWork.VoiceWorkId,
                ProductId = voiceWork.ProductId,
                OriginalProductId = voiceWork.OriginalProductId,
                ProductName = criteria.Locale == Locale.English && englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName,
                ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html",
                Description = criteria.Locale == Locale.English && englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description,
                Favorite = voiceWork.Favorite,
                HasImage = voiceWork.HasImage,
                Maker = circle.Name,
                MakerId = circle.MakerId,
                Circle = new()
                {
                    Name = circle.Name,
                    MakerId = circle.MakerId,
                    IsFavorite = circle.Favorite,
                    IsBlacklisted = circle.Blacklisted
                },
                ExpectedDate = voiceWork.ExpectedDate,
                SalesDate = voiceWork.SalesDate,
                PlannedReleaseDate = voiceWork.PlannedReleaseDate,
                Downloads = voiceWork.Downloads,
                WishlistCount = voiceWork.WishlistCount,
                Status = voiceWork.Status,
                SubtitleLanguage = voiceWork.SubtitleLanguage,
                HasTrial = voiceWork.HasTrial,
                HasChobit = voiceWork.HasChobit,
                IsValid = voiceWork.IsValid
            };

        return result;
    }

    protected override async Task<Dictionary<int, VoiceWorkSearchResult>> GetItems(VoiceWorkSearchCriteria criteria, int[] ids)
    {
        var result =
            from voiceWork in context.VoiceWorks.AsNoTracking()
            join englishVoiceWork in context.EnglishVoiceWorks.AsNoTracking() on voiceWork.VoiceWorkId equals englishVoiceWork.VoiceWorkId into ps
            from englishVoiceWork in ps.DefaultIfEmpty()
            join circle in context.Circles.AsNoTracking() on voiceWork.CircleId equals circle.CircleId into cs
            from circle in cs.DefaultIfEmpty()
            let productLinkPage = voiceWork.SalesDate.HasValue ? "work" : "announce"
            where ids.Contains(voiceWork.VoiceWorkId)
            select new VoiceWorkSearchResult()
            {
                VoiceWorkId = voiceWork.VoiceWorkId,
                ProductId = voiceWork.ProductId,
                OriginalProductId = voiceWork.OriginalProductId,
                ProductName = criteria.Locale == Locale.English && englishVoiceWork != null ? englishVoiceWork.ProductName : voiceWork.ProductName,
                ProductUrl = "http://www.dlsite.com/maniax/" + productLinkPage + "/=/product_id/" + voiceWork.ProductId + ".html",
                Description = criteria.Locale == Locale.English && englishVoiceWork != null ? englishVoiceWork.Description : voiceWork.Description,
                Favorite = voiceWork.Favorite,
                HasImage = voiceWork.HasImage,
                Maker = circle.Name,
                MakerId = circle.MakerId,
                Circle = new()
                {
                    Name = circle.Name,
                    MakerId = circle.MakerId,
                    IsFavorite = circle.Favorite,
                    IsBlacklisted = circle.Blacklisted
                },
                ExpectedDate = voiceWork.ExpectedDate,
                SalesDate = voiceWork.SalesDate,
                PlannedReleaseDate = voiceWork.PlannedReleaseDate,
                Downloads = voiceWork.Downloads,
                WishlistCount = voiceWork.WishlistCount,
                Rating = (AgeRating)voiceWork.Rating,
                Status = voiceWork.Status,
                SubtitleLanguage = voiceWork.SubtitleLanguage,
                HasTrial = voiceWork.HasTrial,
                HasChobit = voiceWork.HasChobit,
                IsValid = voiceWork.IsValid
            };

        return await result.ToDictionaryAsync(x => x.VoiceWorkId);
    }

    protected override async Task PostLoadAsync(IList<VoiceWorkSearchResult> items, CancellationToken cancellationToken)
    {
        if (items.Count == 0)
            return;

        int[] voiceWorkIds = [.. items.Select(i => i.VoiceWorkId)];

        var tagsByVoiceWork = await GetTagsAsync(voiceWorkIds, cancellationToken);
        var creatorsByVoiceWork = await GetCreatorsAsync(voiceWorkIds, cancellationToken);
        var supportedLanguagesByVoiceWork = await GetSupportedLanguagesAsync(voiceWorkIds, cancellationToken);
        var originalCirclesByVoiceWork = await GetOriginalCircles(voiceWorkIds, cancellationToken);

        foreach (VoiceWorkSearchResult item in items)
        {
            if (tagsByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkTagItem[]? tags))
                item.Tags = tags;

            if (creatorsByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkCreatorItem[]? creators))
                item.Creators = creators;

            if (supportedLanguagesByVoiceWork.TryGetValue(item.VoiceWorkId, out string[]? supportedLanguages))
                item.SupportedLanguages = supportedLanguages;

            if (originalCirclesByVoiceWork.TryGetValue(item.VoiceWorkId, out VoiceWorkCircleItem? voiceWorkCircleItem))
                item.OriginalCircle = voiceWorkCircleItem;
        }
    }

    private async Task<Dictionary<int, VoiceWorkTagItem[]>> GetTagsAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
    {
        var tagRows = await (
            from voiceWorkTag in context.VoiceWorkTags.AsNoTracking()
            join tag in context.Tags.AsNoTracking() on voiceWorkTag.TagId equals tag.TagId
            join englishTag in context.EnglishTags.AsNoTracking() on tag.TagId equals englishTag.TagId into et
            from englishTag in et.DefaultIfEmpty()
            where voiceWorkIds.Contains(voiceWorkTag.VoiceWorkId)
            orderby voiceWorkTag.VoiceWorkId, voiceWorkTag.Position
            select new { voiceWorkTag.VoiceWorkId, voiceWorkTag.TagId, tag.Name, EnglishName = englishTag.Name, IsFavorite =  tag.Favorite, IsBlacklisted = tag.Blacklisted }
        ).ToListAsync(cancellationToken);

        return tagRows
            .GroupBy(r => r.VoiceWorkId)
            .ToDictionary(
                g => g.Key,
                g => g.Select(r => new VoiceWorkTagItem { TagId = r.TagId, Name = r.EnglishName ?? r.Name, IsFavorite = r.IsFavorite, IsBlacklisted = r.IsBlacklisted }).ToArray()
            );
    }

    private async Task<Dictionary<int, VoiceWorkCreatorItem[]>> GetCreatorsAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
    {
        var creatorRows = await (
            from voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking()
            join creator in context.Creators.AsNoTracking() on voiceWorkCreator.CreatorId equals creator.CreatorId
            where voiceWorkIds.Contains(voiceWorkCreator.VoiceWorkId)
            orderby voiceWorkCreator.VoiceWorkId, voiceWorkCreator.Position
            select new { voiceWorkCreator.VoiceWorkId, creator.CreatorId, creator.Name, creator.Favorite, creator.Blacklisted }
        ).ToListAsync(cancellationToken);

        return creatorRows
            .GroupBy(r => r.VoiceWorkId)
            .ToDictionary(
                g => g.Key,
                g => g.Select(r => new VoiceWorkCreatorItem { CreatorId = r.CreatorId, Name = r.Name, IsFavorite = r.Favorite, IsBlacklisted = r.Blacklisted }).ToArray()
            );
    }

    private async Task<Dictionary<int, string[]>> GetSupportedLanguagesAsync(int[] voiceWorkIds, CancellationToken cancellationToken)
    {
        var supportedLanguageRows = await (
            from voiceWorkSupportedLanguage in context.VoiceWorkSupportedLanguages.AsNoTracking()
            where voiceWorkIds.Contains(voiceWorkSupportedLanguage.VoiceWorkId)
            select new { voiceWorkSupportedLanguage.VoiceWorkId, voiceWorkSupportedLanguage.Language }
        ).ToListAsync(cancellationToken);

        return supportedLanguageRows
           .GroupBy(r => r.VoiceWorkId)
           .ToDictionary(
               g => g.Key,
               g => g.Select(r => r.Language).ToArray()
           );
    }

    private async Task<Dictionary<int, VoiceWorkCircleItem>> GetOriginalCircles(int[] voiceWorkIds, CancellationToken cancellationToken)
    {
        var originalCircleRows = await (
            from voiceWork in context.VoiceWorks.AsNoTracking()
            join orignalVoiceWork in context.VoiceWorks.AsNoTracking() on voiceWork.OriginalProductId equals orignalVoiceWork.ProductId
            join originalCircle in context.Circles.AsNoTracking() on orignalVoiceWork.CircleId equals originalCircle.CircleId
            where voiceWorkIds.Contains(voiceWork.VoiceWorkId)
            select new
            {
                voiceWork.VoiceWorkId,
                originalCircle.Name,
                originalCircle.MakerId,
                originalCircle.Favorite,
                originalCircle.Blacklisted
            }
        ).ToListAsync(cancellationToken);

        return originalCircleRows
           .GroupBy(r => r.VoiceWorkId)
           .ToDictionary(
               g => g.Key,
               g => g.Select(r => new VoiceWorkCircleItem()
               { 
                   Name = r.Name,
                   MakerId = r.MakerId,
                   IsFavorite = r.Favorite,
                   IsBlacklisted = r.Blacklisted
               }).First()
           );
    }
}