using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;

namespace TeleWave.Application.Broadcast.GetSchedule;

public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
    : IQueryHandler<GetChannelScheduleQuery, Result<IReadOnlyList<ScheduleEntryDto>>>
{
    public async Task<Result<IReadOnlyList<ScheduleEntryDto>>> Handle(
        GetChannelScheduleQuery query,
        CancellationToken cancellationToken
    )
    {
        var channelExists = await dbContext.Channels.AnyAsync(
            c => c.Id == query.ChannelId,
            cancellationToken
        );
        if (!channelExists)
            return Result.Failure<IReadOnlyList<ScheduleEntryDto>>(ChannelErrors.NotFound);

        // Пересекающиеся с окном [from, to) записи.
        var entries = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e =>
                e.ChannelId == query.ChannelId
                && e.StartsAtUtc < query.ToUtc
                && e.EndsAtUtc > query.FromUtc
            )
            .OrderBy(e => e.StartsAtUtc)
            .ToListAsync(cancellationToken);

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

        // Заставки в окне: берём их из кэша по ассету — там лежит ровно тот текст, который играл,
        // с уже подставленными плейсхолдерами. Собирать его заново из подблока значило бы гадать.
        var bumperAssetIds = entries
            .Where(e => e.Kind == Domain.Broadcast.ScheduleEntryKind.Bumper)
            .Select(e => e.MediaAssetId)
            .Distinct()
            .ToList();
        var bumpers =
            bumperAssetIds.Count == 0
                ? []
                : await (
                    from cache in dbContext.BumperAssets.AsNoTracking()
                    join variant in dbContext.BumperTextVariants.AsNoTracking()
                        on cache.VariantId equals variant.Id
                    where bumperAssetIds.Contains(cache.MediaAssetId)
                    select new
                    {
                        cache.MediaAssetId,
                        variant.Name,
                        cache.RenderedLinesJson,
                    }
                ).ToListAsync(cancellationToken);
        var bumpersByAsset = bumpers
            .GroupBy(b => b.MediaAssetId)
            .ToDictionary(g => g.Key, g => g.First());

        // Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки.
        var assetIds = entries
            .Where(e => e.Kind == Domain.Broadcast.ScheduleEntryKind.Program)
            .Select(e => e.MediaAssetId)
            .Distinct()
            .ToList();
        var assetNames = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a => assetIds.Contains(a.Id))
            .Select(a => new { a.Id, a.OriginalFileName })
            .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);

        // Франшизы записей: у фильма серия всегда одна, и «Серия 1» в расписании — пустая метка.
        // Осмысленно другое — какой частью коллекции он шёл.
        var collectionIds = entries
            .Where(e => e.CollectionId != null)
            .Select(e => e.CollectionId!.Value)
            .Distinct()
            .ToList();
        var collectionParts =
            collectionIds.Count == 0
                ? []
                : await (
                    from item in dbContext.CollectionItems.AsNoTracking()
                    join collection in dbContext.Collections.AsNoTracking()
                        on item.CollectionId equals collection.Id
                    where collectionIds.Contains(item.CollectionId)
                    select new CollectionPart(
                        item.CollectionId,
                        item.ShowId,
                        item.Position,
                        collection.Name
                    )
                ).ToListAsync(cancellationToken);
        var partsByCollection = collectionParts
            .GroupBy(p => p.CollectionId)
            .ToDictionary(g => g.Key, g => g.OrderBy(p => p.Position).ToList());

        var dtos = new List<ScheduleEntryDto>(entries.Count);
        foreach (var e in entries)
        {
            string? bumperName = null;
            string? bumperText = null;
            if (bumpersByAsset.TryGetValue(e.MediaAssetId, out var bumper))
            {
                bumperName = bumper.Name;
                bumperText = BumperText(bumper.RenderedLinesJson);
            }

            var franchise = FranchisePart(partsByCollection, e.CollectionId, e.ShowId);

            dtos.Add(
                new ScheduleEntryDto(
                    e.Id,
                    e.Kind,
                    e.MediaAssetId,
                    e.StartsAtUtc,
                    e.EndsAtUtc,
                    e.ShowId,
                    e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null,
                    e.EpisodeIndex,
                    assetNames.TryGetValue(e.MediaAssetId, out var name)
                        ? EpisodeName.ParseLabel(name)
                        : null,
                    bumperName,
                    bumperText,
                    franchise?.Part,
                    franchise?.Total,
                    franchise?.Name
                )
            );
        }

        return Result.Success<IReadOnlyList<ScheduleEntryDto>>(dtos);
    }

    /// <summary>Место записи во франшизе: «часть N из M» и её название.</summary>
    private sealed record FranchisePosition(int Part, int Total, string Name);

    /// <summary>
    /// Какой частью коллекции шла запись. Номер ищется по позиции шоу в текущем составе, а не по
    /// индексу записи: франшизу могли переупорядочить, и «часть 2» обязана означать вторую сейчас.
    /// </summary>
    private static FranchisePosition? FranchisePart(
        IReadOnlyDictionary<Guid, List<CollectionPart>> partsByCollection,
        Guid? collectionId,
        Guid? showId
    )
    {
        if (
            collectionId is not { } id
            || showId is not { } show
            || !partsByCollection.TryGetValue(id, out var ordered)
        )
            return null;

        var at = ordered.FindIndex(p => p.ShowId == show);
        return at < 0 ? null : new FranchisePosition(at + 1, ordered.Count, ordered[0].Name);
    }

    /// <summary>Позиция шоу в коллекции вместе с её названием — то, чем считается номер части.</summary>
    private sealed record CollectionPart(Guid CollectionId, Guid ShowId, int Position, string Name);

    /// <summary>Строки сыгравшей заставки одной меткой для расписания; null — показывать нечего.</summary>
    private static string? BumperText(string? renderedLinesJson)
    {
        var text = string.Join(
            " · ",
            BumperRenderedText
                .FromJson(renderedLinesJson)
                .Select(l => l.Text)
                .Where(t => !string.IsNullOrWhiteSpace(t))
        );
        return text.Length == 0 ? null : text;
    }
}
