using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Groups;

/// <summary>
/// Отличает группу роликов от группы контента.
///
/// Ролик — то же шоу с одной «серией» (см. 6.7), поэтому «Реклама» технически неотличима от
/// «Сериалов»: те же позиции, тот же разворот, то же остывание. В эфире разница огромная — слот
/// по роликам выдаёт полосу рекламы, подписанную как программа, а аварийная группа из роликов
/// превращает в рекламу каждую паузу. Оба места спрашивают одно и то же, поэтому признак живёт
/// здесь, а не в проверке каждого из них.
///
/// Признак — по позициям, а не по единицам: группа роликов состоит из роликов, и сотня коротких
/// единиц против одного длинного сериала здесь ничего не решает.
/// </summary>
public sealed class InterstitialGroups(IAppDbContext dbContext)
{
    /// <summary>
    /// Собрана ли группа из роликов. Группа, которой нет, роликовой не считается: её отсутствие —
    /// отдельная ошибка, и подменять её этой значило бы врать в сообщении.
    /// </summary>
    public async Task<bool> IsInterstitialAsync(Guid groupId, CancellationToken cancellationToken)
    {
        // Заведённая тут же группа ещё не в базе: импорт сетки приносит группы с собой и ставит
        // слоты в том же прогоне — так же, как это делает проверка «группа существует».
        var group =
            dbContext.Groups.Local.FirstOrDefault(g => g.Id == groupId)
            ?? await dbContext
                .Groups.AsNoTracking()
                .Include(g => g.Items)
                .FirstOrDefaultAsync(g => g.Id == groupId, cancellationToken);
        if (group is null)
            return false;

        // Правило без указанного типа означает «кино и сериалы»: ролики в динамический состав
        // сами не попадают (см. GroupFilterMatcher), и разбирать позиции незачем.
        if (group.Mode == GroupMode.Dynamic)
            return GroupFilter.FromJson(group.FilterJson) is { ShowKinds: { Count: > 0 } kinds }
                && kinds.All(kind => kind == ShowKind.Interstitial);

        var items = group.Items.Where(i => i.Role != GroupItemRole.Excluded).ToList();
        if (items.Count == 0)
            return false;

        var showIds = items
            .Where(i => i.ElementKind == GroupElementKind.Show)
            .Select(i => i.ElementId)
            .ToList();
        var collectionIds = items
            .Where(i => i.ElementKind == GroupElementKind.Collection)
            .Select(i => i.ElementId)
            .ToList();

        // Коллекции разворачиваются, а не пропускаются: рекламный блок собирают и коллекцией —
        // так у него получается фиксированный порядок роликов.
        var parts =
            collectionIds.Count == 0
                ? []
                : await dbContext
                    .CollectionItems.AsNoTracking()
                    .Where(i => collectionIds.Contains(i.CollectionId))
                    .Select(i => new { i.CollectionId, i.ShowId })
                    .ToListAsync(cancellationToken);

        var knownIds = showIds.Concat(parts.Select(p => p.ShowId)).Distinct().ToList();
        var interstitialIds = await dbContext
            .Shows.AsNoTracking()
            .Where(s => knownIds.Contains(s.Id) && s.Kind == ShowKind.Interstitial)
            .Select(s => s.Id)
            .ToListAsync(cancellationToken);
        var rollers = interstitialIds.ToHashSet();

        var byCollection = parts.ToLookup(p => p.CollectionId, p => p.ShowId);
        var interstitialItems =
            showIds.Count(rollers.Contains)
            // Коллекция считается роликовой, только если из роликов собрана целиком: одна серия
            // внутри франшизы делает её обычным контентом.
            + collectionIds.Count(id =>
                byCollection[id].Any() && byCollection[id].All(rollers.Contains)
            );

        // Ровно половина уже считается роликовой: смешанная группа в слоте ведёт себя так же плохо,
        // а собрать её осмысленно нельзя — это либо недоразумение, либо забытая позиция.
        return interstitialItems * 2 >= items.Count;
    }
}
