using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Broadcast;

namespace TeleWave.Application.Streaming.GetIptvGuide;

public sealed class GetIptvGuideQueryHandler(IAppDbContext dbContext)
    : IQueryHandler<GetIptvGuideQuery, IReadOnlyList<IptvChannelDto>>
{
    public async Task<IReadOnlyList<IptvChannelDto>> Handle(
        GetIptvGuideQuery query,
        CancellationToken cancellationToken
    )
    {
        var channels = await dbContext
            .Channels.AsNoTracking()
            .Where(c => c.IsEnabled)
            .OrderBy(c => c.Number == null)
            .ThenBy(c => c.Number)
            .ThenBy(c => c.Name)
            .Select(c => new
            {
                c.Id,
                c.Slug,
                c.Name,
                c.Number,
                c.LogoImageId,
            })
            .ToListAsync(cancellationToken);
        if (channels.Count == 0)
            return [];

        // Записи всех каналов одним запросом: каналов десятки, окно — сутки-неделя, и ходить
        // за каждым отдельно значило бы разослать столько же одинаковых запросов.
        var channelIds = channels.Select(c => c.Id).ToList();
        var entries = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e =>
                channelIds.Contains(e.ChannelId)
                && e.Kind == ScheduleEntryKind.Program
                && e.ShowId != null
                && e.StartsAtUtc < query.ToUtc
                && e.EndsAtUtc > query.FromUtc
            )
            .OrderBy(e => e.StartsAtUtc)
            .Select(e => new
            {
                e.ChannelId,
                e.StartsAtUtc,
                e.EndsAtUtc,
                ShowId = e.ShowId!.Value,
                e.MediaAssetId,
            })
            .ToListAsync(cancellationToken);

        var showIds = entries.Select(e => e.ShowId).Distinct().ToList();
        var shows = await dbContext
            .Shows.AsNoTracking()
            .Where(s => showIds.Contains(s.Id))
            .Select(s => new
            {
                s.Id,
                s.Name,
                s.Description,
                s.PosterImageId,
            })
            .ToDictionaryAsync(s => s.Id, cancellationToken);

        // Серии — за названием и описанием конкретного выпуска; ключ тот же, что в публичном EPG.
        var assetIds = entries.Select(e => e.MediaAssetId).Distinct().ToList();
        var episodes = await dbContext
            .Shows.AsNoTracking()
            .SelectMany(s => s.Episodes)
            .Where(e => showIds.Contains(e.ShowId) && assetIds.Contains(e.MediaAssetId))
            .Select(e => new
            {
                e.ShowId,
                e.MediaAssetId,
                e.Title,
                e.Overview,
                e.StillImageId,
            })
            .ToListAsync(cancellationToken);
        var episodeByKey = episodes
            .GroupBy(e => (e.ShowId, e.MediaAssetId))
            .ToDictionary(g => g.Key, g => g.First());

        var byChannel = entries.ToLookup(e => e.ChannelId);
        var result = new List<IptvChannelDto>(channels.Count);

        foreach (var channel in channels)
        {
            var programmes = new List<IptvProgrammeDto>();
            Guid? lastShowId = null;

            foreach (var entry in byChannel[channel.Id].OrderBy(e => e.StartsAtUtc))
            {
                var show = shows.GetValueOrDefault(entry.ShowId);
                var episode = episodeByKey.GetValueOrDefault((entry.ShowId, entry.MediaAssetId));

                // Серии одного шоу встык — это один блок программы. Склеиваем по id, а не по
                // названию: у разных шоу оно может совпасть. Разрыв во времени блок рвёт — между
                // выходами шла реклама, и объявлять это одной передачей нельзя.
                //
                // Непустоту списка не проверяем: lastShowId выставляется только вместе с добавленной
                // передачей, и совпасть с ним на пустом списке нечему.
                if (lastShowId == entry.ShowId && programmes[^1].EndsAtUtc == entry.StartsAtUtc)
                {
                    programmes[^1] = programmes[^1] with { EndsAtUtc = entry.EndsAtUtc };
                    continue;
                }

                lastShowId = entry.ShowId;
                programmes.Add(
                    new IptvProgrammeDto(
                        entry.StartsAtUtc,
                        entry.EndsAtUtc,
                        show?.Name ?? channel.Name,
                        episode?.Title,
                        episode?.Overview ?? show?.Description,
                        episode?.StillImageId ?? show?.PosterImageId
                    )
                );
            }

            result.Add(
                new IptvChannelDto(
                    channel.Slug,
                    channel.Name,
                    channel.Number,
                    channel.LogoImageId,
                    programmes
                )
            );
        }

        return result;
    }
}
