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

namespace TeleWave.Application.Programming.Groups;

/// <summary>
/// Применяет правило набора к библиотеке. Одно место на всех: по нему ищет кандидатов подбор в
/// карточке группы и по нему же считается состав динамической группы — расхождение между «что
/// показал подбор» и «что ушло в эфир» было бы неотлаживаемым.
///
/// Длительность единицы правило проверяет уже по развёрнутым данным
/// (<see cref="GroupElementResolver"/>), поэтому этот шаг вынесен в <see cref="MatchesUnitDuration"/>
/// и применяется вызывающей стороной после разворота.
/// </summary>
public sealed class GroupFilterMatcher(IAppDbContext dbContext)
{
    /// <summary>Элементы (шоу и коллекции), подходящие под правило, — без проверки длительности.</summary>
    public async Task<IReadOnlyList<(GroupElementKind Kind, Guid Id)>> MatchAsync(
        GroupFilter filter,
        CancellationToken cancellationToken
    )
    {
        var wantsShows =
            filter.ElementKinds is null or { Count: 0 }
            || filter.ElementKinds.Contains(GroupElementKind.Show);
        var wantsCollections =
            filter.ElementKinds is null or { Count: 0 }
            || filter.ElementKinds.Contains(GroupElementKind.Collection);

        var showIds = await MatchShowIdsAsync(filter, cancellationToken);

        var elements = new List<(GroupElementKind, Guid)>();
        if (wantsShows)
            elements.AddRange(showIds.Select(id => (GroupElementKind.Show, id)));

        if (wantsCollections)
        {
            // Коллекция подходит, только если подходят все её части: иначе во «взрослую ночь» через
            // франшизу просочилось бы детское, а в «боевики» — комедия из той же серии фильмов.
            var collections = await dbContext
                .Collections.AsNoTracking()
                .Select(c => new { c.Id, ShowIds = c.Items.Select(i => i.ShowId).ToList() })
                .ToListAsync(cancellationToken);

            var matching = showIds.ToHashSet();
            elements.AddRange(
                collections
                    .Where(c => c.ShowIds.Count > 0 && c.ShowIds.All(matching.Contains))
                    .Select(c => (GroupElementKind.Collection, c.Id))
            );
        }

        return elements;
    }

    private async Task<List<Guid>> MatchShowIdsAsync(
        GroupFilter filter,
        CancellationToken cancellationToken
    )
    {
        var shows = dbContext.Shows.AsNoTracking();

        if (filter.ShowKinds is { Count: > 0 } kinds)
            shows = shows.Where(s => kinds.Contains(s.Kind));
        else
            // Ролики в набор сами не идут: они живут отдельной библиотекой (см. 6.7), и правило без
            // указанного типа означает «кино и сериалы», а не «всё, что есть в базе». Группу роликов
            // собирают, отметив тип «Ролик» явно.
            shows = shows.Where(s => s.Kind != ShowKind.Interstitial);

        // Рейтинги упорядочены по возрастанию строгости, поэтому «не строже» — обычное сравнение.
        // Шоу без рейтинга остаются в выборке — ровно как в планировщике (ElementSelector): источники
        // проставляют рейтинг далеко не всему, и молчаливое выбрасывание непроставленного оставило бы
        // от фильтра «мультики не строже PG» горстку голливудского.
        if (filter.MaxAudience is { } maxAudience)
            shows = shows.Where(s => s.Audience == null || s.Audience <= maxAudience);

        // Нижняя граница ведёт себя иначе, чем верхняя: непроставленный рейтинг из выборки уходит.
        // «Не строже PG» с неизвестным рейтингом — допустимый риск, а «R и старше» с ним означало бы
        // мультики в ночном слоте, то есть ровно ту ошибку, ради которой границу и заводят.
        if (filter.MinAudience is { } minAudience)
            shows = shows.Where(s => s.Audience != null && s.Audience >= minAudience);

        if (filter.YearMin is { } yearMin)
            shows = shows.Where(s => s.Year != null && s.Year >= yearMin);
        if (filter.YearMax is { } yearMax)
            shows = shows.Where(s => s.Year != null && s.Year <= yearMax);

        // Жанры — «любой из»: шоу редко подходит под все перечисленные сразу.
        if (filter.GenreIds is { Count: > 0 } genreIds)
            shows = shows.Where(s => s.Genres.Any(g => genreIds.Contains(g.GenreId)));

        return await shows.Select(s => s.Id).ToListAsync(cancellationToken);
    }

    /// <summary>
    /// Длительность проверяется по средней единице, а не по сумме: у сериала в фильтре «серии
    /// по 20–25 минут» осмысленна именно длина серии. Элементы без готовых ассетов длительности
    /// не имеют — их этот фильтр не отбрасывает, иначе из группы выпадало бы всё, что ещё
    /// обрабатывается.
    /// </summary>
    public static bool MatchesUnitDuration(GroupFilter filter, GroupElementInfo element)
    {
        if (filter.UnitMinutesMin is null && filter.UnitMinutesMax is null)
            return true;
        if (element.UnitCount == 0 || element.TotalDuration <= TimeSpan.Zero)
            return true;

        var averageMinutes = element.TotalDuration.TotalMinutes / element.UnitCount;
        if (filter.UnitMinutesMin is { } min && averageMinutes < min)
            return false;
        if (filter.UnitMinutesMax is { } max && averageMinutes > max)
            return false;
        return true;
    }
}
