using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using ElementStats = System.Collections.Generic.IReadOnlyDictionary<
    (TeleWave.Domain.Programming.GroupElementKind Kind, System.Guid Id),
    TeleWave.Application.Programming.Groups.GroupElementInfo
>;

namespace TeleWave.Application.Programming.Groups.Suggest;

/// <summary>
/// Предложение: готовая к созданию группа — имя, правило набора и состав. Ключ самоописателен
/// (<c>genre:{id}</c>, <c>kind:Series</c>, …), поэтому создание не тащит состав с клиента: команда
/// пересобирает предложение по ключу на свежих данных.
/// </summary>
public sealed record GroupSuggestion(
    string Key,
    GroupSuggestionKind Kind,
    string Name,
    GroupFilter? Filter,
    IReadOnlyList<Guid> ShowIds,
    int UnitCount,
    TimeSpan TotalDuration
);

/// <summary>
/// Разбирает библиотеку и предлагает группы, которые имеет смысл завести. Правила намеренно простые
/// и объяснимые: админ должен понимать, почему ему это показали, — «12 шоу с жанром Мультфильм»
/// проверяемо, а рекомендательная магия нет.
///
/// Состав предложения — только шоу. Коллекции сюда не берутся: франшиза почти всегда состоит из тех
/// же шоу, и добавление обеих форм задваивало бы эфир. Правило набора у предложения сохраняется,
/// поэтому коллекции при желании добираются кнопкой «Подобрать» уже в самой группе.
/// </summary>
public sealed class GroupSuggestionBuilder(IAppDbContext dbContext, GroupElementResolver resolver)
{
    /// <summary>Сколько шоу должно набраться, чтобы предлагать сборную группу (жанр/тип/рейтинг).</summary>
    private const int MinShows = 2;

    /// <summary>С какого числа серий сериал заслуживает собственной группы.</summary>
    private const int BigSeriesEpisodes = 20;

    /// <summary>Пороги «мягких» рейтингов: до них группа читается как «детское»/«семейное».</summary>
    private static readonly (ShowAudience Max, string Name)[] AudienceTiers =
    [
        (ShowAudience.Pg, "Детское (не строже PG)"),
        (ShowAudience.Pg13, "Семейное (не строже PG-13)"),
    ];

    /// <summary>Нижний порог — под ночные слоты. Непроставленный рейтинг сюда не попадает.</summary>
    private static readonly (ShowAudience Min, string Name) AdultTier = (
        ShowAudience.R,
        "Взрослое (R и старше)"
    );

    private static readonly (ShowKind Kind, string Name)[] ShowKindNames =
    [
        (ShowKind.Series, "Сериалы"),
        (ShowKind.Single, "Полнометражки"),
    ];

    public async Task<IReadOnlyList<GroupSuggestion>> BuildAsync(
        CancellationToken cancellationToken
    )
    {
        // Ролики-врезки в группы не идут: у рекламы и заставок своя подсистема.
        var shows = await dbContext
            .Shows.AsNoTracking()
            .Where(s => s.Kind != ShowKind.Interstitial)
            .Select(s => new ShowRow(
                s.Id,
                s.Name,
                s.Kind,
                s.Audience,
                s.Genres.Select(g => g.GenreId).ToList(),
                s.Episodes.Count
            ))
            .ToListAsync(cancellationToken);
        if (shows.Count == 0)
            return [];

        var stats = await resolver.ResolveAsync(
            shows.Select(s => (GroupElementKind.Show, s.Id)),
            cancellationToken
        );

        var genres = await dbContext
            .Genres.AsNoTracking()
            .ToDictionaryAsync(g => g.Id, g => g.Name, cancellationToken);

        var suggestions = new List<GroupSuggestion>();
        suggestions.AddRange(ByGenre(shows, genres, stats));
        suggestions.AddRange(ByShowKind(shows, stats));
        suggestions.AddRange(ByAudience(shows, stats));
        suggestions.AddRange(ByBigSeries(shows, stats));

        // Внутри вида — сначала самые ёмкие: с них и начинают собирать сетку.
        return
        [
            .. suggestions
                .OrderBy(s => s.Kind)
                .ThenByDescending(s => s.UnitCount)
                .ThenBy(s => s.Name, StringComparer.CurrentCultureIgnoreCase),
        ];
    }

    private static IEnumerable<GroupSuggestion> ByGenre(
        List<ShowRow> shows,
        IReadOnlyDictionary<Guid, string> genres,
        ElementStats stats
    )
    {
        foreach (var (genreId, name) in genres)
        {
            var matched = shows.Where(s => s.GenreIds.Contains(genreId)).ToList();
            if (matched.Count < MinShows)
                continue;

            yield return Build(
                $"genre:{genreId:N}",
                GroupSuggestionKind.Genre,
                name,
                new GroupFilter(GenreIds: [genreId]),
                matched,
                stats
            );
        }
    }

    private static IEnumerable<GroupSuggestion> ByShowKind(List<ShowRow> shows, ElementStats stats)
    {
        foreach (var (kind, name) in ShowKindNames)
        {
            var matched = shows.Where(s => s.Kind == kind).ToList();
            if (matched.Count < MinShows)
                continue;

            yield return Build(
                $"kind:{kind}",
                GroupSuggestionKind.ShowKind,
                name,
                new GroupFilter(ShowKinds: [kind]),
                matched,
                stats
            );
        }
    }

    /// <summary>
    /// «Не строже X» — ровно та форма, которую понимает правило набора. Шоу без рейтинга в выборке
    /// остаются (как в фильтре и в планировщике): источники проставляют рейтинг далеко не всему.
    /// </summary>
    private static IEnumerable<GroupSuggestion> ByAudience(List<ShowRow> shows, ElementStats stats)
    {
        foreach (var (max, name) in AudienceTiers)
        {
            var matched = shows.Where(s => s.Audience == null || s.Audience <= max).ToList();
            if (matched.Count < MinShows)
                continue;

            // Если под порог попадает вообще всё, группа повторяет библиотеку — предлагать нечего.
            if (matched.Count == shows.Count)
                continue;

            yield return Build(
                $"audience:{max}",
                GroupSuggestionKind.Audience,
                name,
                new GroupFilter(MaxAudience: max),
                matched,
                stats
            );
        }

        var adult = shows.Where(s => s.Audience >= AdultTier.Min).ToList();
        if (adult.Count >= MinShows)
            yield return Build(
                $"audience-min:{AdultTier.Min}",
                GroupSuggestionKind.Audience,
                AdultTier.Name,
                new GroupFilter(MinAudience: AdultTier.Min),
                adult,
                stats
            );
    }

    /// <summary>
    /// Крупный сериал ставят в слот целиком, поэтому ему нужна собственная группа. Правила набора
    /// у неё нет: «именно это шоу» фильтром не выражается, да и не нужно — состав из одной позиции.
    /// </summary>
    private static IEnumerable<GroupSuggestion> ByBigSeries(List<ShowRow> shows, ElementStats stats)
    {
        foreach (
            var show in shows.Where(s =>
                s.Kind == ShowKind.Series && s.EpisodeCount >= BigSeriesEpisodes
            )
        )
            yield return Build(
                $"show:{show.Id:N}",
                GroupSuggestionKind.BigSeries,
                show.Name,
                null,
                [show],
                stats
            );
    }

    private static GroupSuggestion Build(
        string key,
        GroupSuggestionKind kind,
        string name,
        GroupFilter? filter,
        IReadOnlyList<ShowRow> matched,
        ElementStats stats
    )
    {
        var units = 0;
        var duration = TimeSpan.Zero;
        foreach (var show in matched)
        {
            if (!stats.TryGetValue((GroupElementKind.Show, show.Id), out var info))
                continue;
            units += info.UnitCount;
            duration += info.TotalDuration;
        }

        return new GroupSuggestion(
            key,
            kind,
            name,
            filter,
            [.. matched.Select(s => s.Id)],
            units,
            duration
        );
    }

    private sealed record ShowRow(
        Guid Id,
        string Name,
        ShowKind Kind,
        ShowAudience? Audience,
        List<Guid> GenreIds,
        int EpisodeCount
    );
}
