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

namespace TeleWave.Application.Programming.Templates.Generate;

/// <summary>
/// Группы канала глазами раскладки: сколько эфира каждая даёт, какой она природы и насколько строгий
/// в ней контент. Отдельный тип, а не метод планировщика: те же факты нужны и запросу к ИИ, а считать
/// их дважды значит однажды разойтись — сетка собиралась бы по одним числам, а ИИ предлагал по другим.
///
/// Группы роликов отсюда не выбрасываются: они существуют, их видно в запросе к ИИ, и молчаливое
/// исчезновение группы из списка выглядело бы потерей. Не годятся они только в слот — это решает
/// раскладка по <see cref="GroupCandidate.DominantKind"/>.
/// </summary>
public sealed class GroupCatalog(
    IAppDbContext dbContext,
    DynamicGroupResolver dynamicResolver,
    GroupElementResolver elementResolver
)
{
    public async Task<List<GroupCandidate>> LoadAsync(CancellationToken cancellationToken)
    {
        var groups = await dbContext
            .Groups.AsNoTracking()
            .Include(g => g.Items)
            .ToListAsync(cancellationToken);

        var candidates = new List<GroupCandidate>();

        foreach (var group in groups)
        {
            // Состав считается, а не читается из кэша: у группы по правилу кэш отстаёт от
            // библиотеки, и генератор поставил бы в эфир опустевшую группу.
            var composition = await dynamicResolver.ResolveAsync(group, cancellationToken);
            if (composition.Count == 0)
                continue;

            var info = await elementResolver.ResolveAsync(
                composition.Select(e => (e.Kind, e.Id)),
                cancellationToken
            );
            var genreId = await PrimaryGenreAsync(composition, cancellationToken);

            if (Describe(group, composition, info, genreId) is { } candidate)
                candidates.Add(candidate);
        }

        return candidates;
    }

    /// <summary>
    /// Основной жанр группы — самый частый среди её шоу. Нужен, чтобы не ставить подряд две полосы
    /// одного жанра и не выйти за потолок доли жанра из правил канала.
    /// </summary>
    private async Task<Guid?> PrimaryGenreAsync(
        IReadOnlyList<ResolvedElement> composition,
        CancellationToken cancellationToken
    )
    {
        var showIds = composition
            .Where(e => e.Kind == GroupElementKind.Show)
            .Select(e => e.Id)
            .ToList();
        var collectionIds = composition
            .Where(e => e.Kind == GroupElementKind.Collection)
            .Select(e => e.Id)
            .ToList();

        if (collectionIds.Count > 0)
        {
            var parts = await dbContext
                .CollectionItems.AsNoTracking()
                .Where(i => collectionIds.Contains(i.CollectionId))
                .Select(i => i.ShowId)
                .ToListAsync(cancellationToken);
            showIds.AddRange(parts);
        }

        if (showIds.Count == 0)
            return null;

        var genres = await dbContext
            .ShowGenres.AsNoTracking()
            .Where(g => showIds.Contains(g.ShowId) && g.IsPrimary)
            .Select(g => g.GenreId)
            .ToListAsync(cancellationToken);

        return genres.Count == 0
            ? null
            : genres.GroupBy(id => id).OrderByDescending(g => g.Count()).First().Key;
    }

    /// <summary>
    /// Сводит вычисленный состав к тому, что нужно раскладке. Группа без готовых единиц не годится
    /// вовсе — слот по ней встал бы, а место закрыл фон.
    /// </summary>
    private static GroupCandidate? Describe(
        Group group,
        IReadOnlyList<ResolvedElement> composition,
        IReadOnlyDictionary<(GroupElementKind Kind, Guid Id), GroupElementInfo> info,
        Guid? primaryGenreId
    )
    {
        var resolved = composition
            .Select(e => info.GetValueOrDefault((e.Kind, e.Id)))
            .OfType<GroupElementInfo>()
            .ToList();

        var units = resolved.Sum(r => r.UnitCount);
        if (units == 0)
            return null;

        var duration = resolved.Aggregate(TimeSpan.Zero, (sum, r) => sum + r.TotalDuration);

        return new GroupCandidate(
            group.Id,
            group.Name,
            composition.Count,
            units,
            resolved.Max(r => r.Audience),
            DominantKindOf(resolved, units),
            duration.TotalMinutes / units,
            primaryGenreId
        );
    }

    /// <summary>
    /// Чем группа является по составу — по единицам, а не по позициям: полоса собирается под длину
    /// того, что в ней играет.
    ///
    /// Ролики проверяются первыми: группа рекламы даёт сотни коротких единиц, то есть самый большой
    /// <c>UnitCount</c> на канале, и записанная «полным метром» она побеждала бы по баллам в любой
    /// полосе без предпочтения по типу — в эфир уходила бы полоса рекламы. У коллекции своего типа
    /// нет: франшиза — это почти всегда полнометражки.
    /// </summary>
    private static ShowKind DominantKindOf(IReadOnlyList<GroupElementInfo> resolved, int units)
    {
        var interstitialUnits = resolved
            .Where(r => r.ShowKind == ShowKind.Interstitial)
            .Sum(r => r.UnitCount);
        if (interstitialUnits * 2 >= units)
            return ShowKind.Interstitial;

        var seriesUnits = resolved.Where(r => r.ShowKind == ShowKind.Series).Sum(r => r.UnitCount);
        return seriesUnits * 2 >= units ? ShowKind.Series : ShowKind.Single;
    }
}
