using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Application.Channels;

public class PreviewAutoTuneChannelsHandler(IDbContextFactory<TvContext> dbContextFactory)
    : IRequestHandler<PreviewAutoTuneChannels, Either<BaseError, List<AutoTuneProposal>>>
{
    public async Task<Either<BaseError, List<AutoTuneProposal>>> Handle(
        PreviewAutoTuneChannels request,
        CancellationToken cancellationToken)
    {
        if (request.Axes is null || request.Axes.Count == 0)
        {
            return BaseError.New("At least one axis is required");
        }

        if (request.MinItems < 1)
        {
            return BaseError.New("Minimum items must be at least 1");
        }

        if (request.StartingNumber < 1)
        {
            return BaseError.New("Starting channel number must be at least 1");
        }

        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        // Enumerate (axis, value, count) triples per requested axis, preserving axis order.
        var raw = new List<(AutoTuneAxis Axis, string Value, int Count)>();
        foreach (AutoTuneAxis axis in request.Axes.Distinct())
        {
            raw.AddRange(await EnumerateAxis(dbContext, axis, request.MinItems, cancellationToken));
        }

        System.Collections.Generic.HashSet<string> existingNumbers = (await dbContext.Channels.AsNoTracking()
                .Select(c => c.Number).ToListAsync(cancellationToken))
            .ToHashSet();
        System.Collections.Generic.HashSet<string> existingNames = (await dbContext.Channels.AsNoTracking()
                .Select(c => c.Name).ToListAsync(cancellationToken))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        // Drop entries whose generated name would be rejected at create time (Channel name <= 50
        // chars) before number allocation, so numbers aren't wasted on proposals that can never
        // be created.
        List<(AutoTuneAxis Axis, string Value, int Count, string Name)> survivors = raw
            .Select(r => (r.Axis, r.Value, r.Count, Name: AutoTuneAxisMap.GenerateName(r.Axis, r.Value)))
            .Where(r => r.Name.Length <= 50)
            .ToList();

        List<string> numbers = AutoTuneNumberAllocator.Allocate(
            request.StartingNumber, survivors.Count, existingNumbers);

        var proposals = new List<AutoTuneProposal>(survivors.Count);
        for (int i = 0; i < survivors.Count; i++)
        {
            (AutoTuneAxis axis, string value, int count, string name) = survivors[i];
            proposals.Add(new AutoTuneProposal(
                axis, value, name, numbers[i], count, existingNames.Contains(name)));
        }

        return proposals;
    }

    private static async Task<List<(AutoTuneAxis, string, int)>> EnumerateAxis(
        TvContext dbContext, AutoTuneAxis axis, int minItems, CancellationToken cancellationToken) =>
        axis switch
        {
            AutoTuneAxis.TvShow => await EnumerateTvShows(dbContext, minItems, cancellationToken),
            AutoTuneAxis.TvGenre => await EnumerateEpisodeGenres(dbContext, minItems, cancellationToken),
            AutoTuneAxis.MovieGenre => await EnumerateMovieGenres(dbContext, minItems, cancellationToken),
            _ => []
        };

    private static async Task<List<(AutoTuneAxis, string, int)>> EnumerateTvShows(
        TvContext dbContext, int minItems, CancellationToken cancellationToken)
    {
        // Episode count per show id (Episode -> Season -> ShowId). Proven query style from LibraryBrowseItemMapper.
        Dictionary<int, int> episodeCounts = await dbContext.Episodes.AsNoTracking()
            .GroupBy(e => e.Season.ShowId)
            .Select(g => new { ShowId = g.Key, Count = g.Count() })
            .ToDictionaryAsync(g => g.ShowId, g => g.Count, cancellationToken);

        var showTitles = await dbContext.ShowMetadata.AsNoTracking()
            .Select(sm => new { sm.ShowId, sm.Title })
            .ToListAsync(cancellationToken);

        // Collapse shows that share a title (the generated show_title query matches them together).
        var byTitle = new Dictionary<string, int>();
        foreach (var row in showTitles)
        {
            if (string.IsNullOrWhiteSpace(row.Title))
            {
                continue;
            }

            episodeCounts.TryGetValue(row.ShowId, out int count);
            byTitle[row.Title] = byTitle.GetValueOrDefault(row.Title) + count;
        }

        return byTitle
            .Where(kv => kv.Value >= minItems)
            .OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)
            .Select(kv => (AutoTuneAxis.TvShow, kv.Key, kv.Value))
            .ToList();
    }

    private static async Task<List<(AutoTuneAxis, string, int)>> EnumerateEpisodeGenres(
        TvContext dbContext, int minItems, CancellationToken cancellationToken)
    {
        var counts = await dbContext.EpisodeMetadata.AsNoTracking()
            .SelectMany(m => m.Genres)
            .GroupBy(g => g.Name)
            .Select(grp => new { Name = grp.Key, Count = grp.Count() })
            .ToListAsync(cancellationToken);

        return counts
            .Where(c => !string.IsNullOrWhiteSpace(c.Name) && c.Count >= minItems)
            .OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
            .Select(c => (AutoTuneAxis.TvGenre, c.Name, c.Count))
            .ToList();
    }

    private static async Task<List<(AutoTuneAxis, string, int)>> EnumerateMovieGenres(
        TvContext dbContext, int minItems, CancellationToken cancellationToken)
    {
        var counts = await dbContext.MovieMetadata.AsNoTracking()
            .SelectMany(m => m.Genres)
            .GroupBy(g => g.Name)
            .Select(grp => new { Name = grp.Key, Count = grp.Count() })
            .ToListAsync(cancellationToken);

        return counts
            .Where(c => !string.IsNullOrWhiteSpace(c.Name) && c.Count >= minItems)
            .OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
            .Select(c => (AutoTuneAxis.MovieGenre, c.Name, c.Count))
            .ToList();
    }
}
