using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Programming.Groups;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;

namespace TeleWave.Application.Programming.Planning;

/// <summary>
/// Разворачивает группы в последовательности единиц воспроизведения для планировщика: сериал —
/// в свои серии, коллекция — в части по порядку (сериал внутри коллекции тоже разворачивается),
/// фильм — в одну единицу.
///
/// В эфир попадают только готовые ассеты с известной длительностью: поставить в ленту то, что ещё
/// обрабатывается, значит получить дыру в раздаче.
/// </summary>
public sealed class GroupExpander(IAppDbContext dbContext, DynamicGroupResolver dynamicResolver)
{
    /// <param name="historyFrom">С какого момента нужна история показов для потолка повторов;
    /// null — история не нужна и не загружается.</param>
    public async Task<IReadOnlyDictionary<Guid, IReadOnlyList<PlanningElement>>> ExpandAsync(
        IReadOnlyCollection<Guid> groupIds,
        Guid channelId,
        CancellationToken cancellationToken,
        DateTimeOffset? historyFrom = null
    )
    {
        var result = new Dictionary<Guid, IReadOnlyList<PlanningElement>>();
        if (groupIds.Count == 0)
            return result;

        var groups = await dbContext
            .Groups.AsNoTracking()
            .Include(g => g.Items)
            .Where(g => groupIds.Contains(g.Id))
            .ToListAsync(cancellationToken);

        // Состав считается здесь, перед самым разворотом: у динамической группы он живёт не в
        // списке позиций, а в правиле, и должен быть свежим на момент прогона.
        var composition = new Dictionary<Guid, IReadOnlyList<ResolvedElement>>();
        foreach (var group in groups)
            composition[group.Id] = await dynamicResolver.ResolveAsync(group, cancellationToken);

        var elements = composition.Values.SelectMany(c => c).ToList();

        var collectionIds = elements
            .Where(e => e.Kind == GroupElementKind.Collection)
            .Select(e => e.Id)
            .Distinct()
            .ToList();

        var collectionParts = await dbContext
            .CollectionItems.AsNoTracking()
            .Where(i => collectionIds.Contains(i.CollectionId))
            .OrderBy(i => i.Position)
            .Select(i => new { i.CollectionId, i.ShowId })
            .ToListAsync(cancellationToken);

        var showIds = elements
            .Where(e => e.Kind == GroupElementKind.Show)
            .Select(e => e.Id)
            .Concat(collectionParts.Select(p => p.ShowId))
            .Distinct()
            .ToList();

        var loaded = await dbContext
            .Shows.AsNoTracking()
            .Where(s => showIds.Contains(s.Id))
            .Select(s => new
            {
                s.Id,
                s.Audience,
                AssetIds = s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).ToList(),
            })
            .ToListAsync(cancellationToken);
        var shows = loaded.ToDictionary(s => s.Id, s => new ShowRow(s.Audience, s.AssetIds));

        var assetIds = shows.Values.SelectMany(s => s.AssetIds).Distinct().ToList();
        var durations = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a =>
                assetIds.Contains(a.Id) && a.Status == MediaAssetStatus.Ready && a.Duration != null
            )
            .Select(a => new { a.Id, a.Duration })
            .ToDictionaryAsync(a => a.Id, a => a.Duration!.Value, cancellationToken);

        var lastPlayed = await LoadLastPlayedAsync(channelId, showIds, cancellationToken);
        var recentPlays = await LoadRecentPlaysAsync(
            channelId,
            showIds,
            historyFrom,
            cancellationToken
        );

        var data = new ExpansionData(
            shows,
            durations,
            lastPlayed,
            recentPlays,
            collectionParts.ToLookup(p => p.CollectionId, p => p.ShowId)
        );

        foreach (var (groupId, resolved) in composition)
            result[groupId] =
            [
                .. resolved
                    .OrderBy(e => e.Position)
                    .Select(element =>
                        element.Kind == GroupElementKind.Show
                            ? data.ShowElement(element)
                            : data.CollectionElement(element)
                    ),
            ];

        return result;
    }

    /// <summary>Шоу в виде, нужном развороту: рейтинг и ассеты серий в порядке показа.</summary>
    private sealed record ShowRow(ShowAudience? Audience, IReadOnlyList<Guid> AssetIds);

    /// <summary>
    /// Справочники одного прогона. Сборка элементов вынесена сюда из <see cref="ExpandAsync"/>:
    /// там уже была цепочка запросов, и вместе с двойным циклом метод читался только целиком.
    /// В БД отсюда не ходят — всё уже загружено.
    /// </summary>
    private sealed record ExpansionData(
        IReadOnlyDictionary<Guid, ShowRow> Shows,
        IReadOnlyDictionary<Guid, TimeSpan> Durations,
        IReadOnlyDictionary<Guid, DateTimeOffset> LastPlayed,
        IReadOnlyDictionary<Guid, IReadOnlyList<DateTimeOffset>> RecentPlays,
        ILookup<Guid, Guid> CollectionParts
    )
    {
        public PlanningElement ShowElement(ResolvedElement element) =>
            new(
                element.Kind,
                element.Id,
                element.Weight,
                element.Position,
                UnitsOf(element.Id),
                LastPlayedOf(element.Id),
                AudienceOf(element.Id),
                RecentPlaysOf(element.Id)
            );

        /// <summary>
        /// Коллекция как один элемент: единицы всех частей по порядку; остывание — по самой свежей
        /// части (показ любой означает, что франшиза недавно была в эфире); рейтинг — строжайший
        /// (франшиза идёт целиком, и одна взрослая часть делает взрослой всю).
        /// </summary>
        public PlanningElement CollectionElement(ResolvedElement element)
        {
            var parts = CollectionParts[element.Id].ToList();
            return new PlanningElement(
                element.Kind,
                element.Id,
                element.Weight,
                element.Position,
                parts.SelectMany(UnitsOf).ToList(),
                parts.Select(LastPlayedOf).Where(p => p is not null).DefaultIfEmpty(null).Max(),
                parts.Select(AudienceOf).Where(a => a is not null).DefaultIfEmpty(null).Max(),
                parts.SelectMany(id => RecentPlaysOf(id) ?? []).ToList()
            );
        }

        /// <summary>Единицы воспроизведения шоу: только серии с готовым ассетом, в порядке показа.</summary>
        private List<PlanningUnit> UnitsOf(Guid showId) =>
            Shows.TryGetValue(showId, out var show)
                ? show
                    .AssetIds.Where(Durations.ContainsKey)
                    .Select(
                        (assetId, index) =>
                            new PlanningUnit(assetId, Durations[assetId], showId, index)
                    )
                    .ToList()
                : [];

        private DateTimeOffset? LastPlayedOf(Guid showId) =>
            LastPlayed.TryGetValue(showId, out var at) ? at : null;

        private ShowAudience? AudienceOf(Guid showId) =>
            Shows.TryGetValue(showId, out var show) ? show.Audience : null;

        private IReadOnlyList<DateTimeOffset>? RecentPlaysOf(Guid showId) =>
            RecentPlays.TryGetValue(showId, out var plays) ? plays : null;
    }

    /// <summary>
    /// Когда каждое шоу в последний раз выходило в этом канале. Источник — сама лента: отдельного
    /// журнала показов нет, поэтому глубина хранения расписания должна покрывать максимальное
    /// остывание среди правил.
    /// </summary>
    private async Task<Dictionary<Guid, DateTimeOffset>> LoadLastPlayedAsync(
        Guid channelId,
        IReadOnlyCollection<Guid> showIds,
        CancellationToken cancellationToken
    ) =>
        await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e =>
                e.ChannelId == channelId
                && e.Kind == ScheduleEntryKind.Program
                && e.ShowId != null
                && showIds.Contains(e.ShowId.Value)
            )
            .GroupBy(e => e.ShowId!.Value)
            .Select(g => new { ShowId = g.Key, LastPlayed = g.Max(e => e.StartsAtUtc) })
            .ToDictionaryAsync(x => x.ShowId, x => x.LastPlayed, cancellationToken);

    /// <summary>
    /// Старты показов за окно потолка повторов. Загружается только когда правило задано: без него
    /// это лишние сотни строк на каждый прогон.
    /// </summary>
    private async Task<Dictionary<Guid, IReadOnlyList<DateTimeOffset>>> LoadRecentPlaysAsync(
        Guid channelId,
        IReadOnlyCollection<Guid> showIds,
        DateTimeOffset? from,
        CancellationToken cancellationToken
    )
    {
        if (from is not { } since || showIds.Count == 0)
            return [];

        var plays = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e =>
                e.ChannelId == channelId
                && e.Kind == ScheduleEntryKind.Program
                && e.ShowId != null
                && showIds.Contains(e.ShowId.Value)
                && e.StartsAtUtc >= since
            )
            .Select(e => new { ShowId = e.ShowId!.Value, e.StartsAtUtc })
            .ToListAsync(cancellationToken);

        return plays
            .GroupBy(p => p.ShowId)
            .ToDictionary(
                g => g.Key,
                g => (IReadOnlyList<DateTimeOffset>)g.Select(p => p.StartsAtUtc).ToList()
            );
    }
}
