using System.Globalization;
using System.Text;
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Programming.Templates.Generate;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;

namespace TeleWave.Application.Programming.Templates.Transfer;

public sealed class BuildGridPromptQueryHandler(IAppDbContext dbContext, GroupCatalog catalog)
    : IQueryHandler<BuildGridPromptQuery, Result<GridPromptDto>>
{
    /// <summary>
    /// Потолок на список шоу. Библиотека канала может быть в тысячах серий, а запрос читает модель
    /// с ограниченным контекстом: группы для сетки важнее поимённого перечисления, поэтому список
    /// обрезается, и об обрезке прямо написано в запросе.
    /// </summary>
    private const int ShowLimit = 300;

    private static readonly CultureInfo Culture = CultureInfo.GetCultureInfo("ru-RU");

    public async Task<Result<GridPromptDto>> Handle(
        BuildGridPromptQuery query,
        CancellationToken cancellationToken
    )
    {
        var channel = await dbContext
            .Channels.AsNoTracking()
            .FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
        if (channel is null)
            return Result.Failure<GridPromptDto>(ChannelErrors.NotFound);

        var groups = await catalog.LoadAsync(cancellationToken);
        var genres = await dbContext
            .Genres.AsNoTracking()
            .ToDictionaryAsync(g => g.Id, g => g.Name, cancellationToken);
        var shows = await LoadShowsAsync(cancellationToken);
        var junctions = await dbContext
            .JunctionTemplates.AsNoTracking()
            .OrderBy(j => j.Name)
            .Select(j => j.Name)
            .ToListAsync(cancellationToken);
        var bumpers = await dbContext
            .BumperTemplates.AsNoTracking()
            .OrderBy(b => b.Name)
            .Select(b => b.Name)
            .ToListAsync(cancellationToken);

        var text = new StringBuilder();
        text.AppendLine(GridPromptText.Task).AppendLine();

        text.AppendLine("## Канал");
        text.AppendLine(
            Culture,
            $"- Название: {channel.Name}\n"
                + $"- Вещательные сутки начинаются в {channel.DayStartTime:HH\\:mm} (dayStart)\n"
                + $"- Время канала: UTC{(channel.UtcOffsetMinutes < 0 ? "-" : "+")}{Math.Abs(channel.UtcOffsetMinutes) / 60}:{Math.Abs(channel.UtcOffsetMinutes) % 60:00}"
        );

        AppendReferences(text, query);
        AppendGroups(text, groups, genres);
        AppendShows(text, shows, genres);
        AppendNames(
            text,
            "Стыки",
            junctions,
            "стыков ещё нет — поля junctionAfter/junctionBetween оставь пустыми"
        );
        AppendNames(text, "Заставки", bumpers, "заставок ещё нет");

        text.AppendLine().AppendLine(GridPromptText.Rules);
        text.AppendLine().AppendLine(GridPromptText.Schema);
        text.AppendLine().AppendLine(GridPromptText.Answer);

        var prompt = text.ToString();
        return Result.Success(new GridPromptDto(prompt, groups.Count, shows.Count, prompt.Length));
    }

    private static void AppendReferences(StringBuilder text, BuildGridPromptQuery query)
    {
        var references = query.References.Where(r => !string.IsNullOrWhiteSpace(r)).ToList();
        if (references.Count > 0)
        {
            text.AppendLine().AppendLine("## Референсы");
            text.AppendLine(
                "Равняйся на ритм этих каналов — длину блоков, что стоит в прайме, чем закрыта ночь:"
            );
            foreach (var reference in references)
                text.AppendLine(Culture, $"- {reference.Trim()}");
        }

        if (string.IsNullOrWhiteSpace(query.Notes))
            return;

        text.AppendLine().AppendLine("## Пожелания");
        text.AppendLine(query.Notes.Trim());
    }

    private static void AppendGroups(
        StringBuilder text,
        IReadOnlyList<GroupCandidate> groups,
        IReadOnlyDictionary<Guid, string> genres
    )
    {
        text.AppendLine().AppendLine("## Доступные группы");
        if (groups.Count == 0)
        {
            text.AppendLine(
                "Готовых групп нет — сетку по ним собрать нельзя. Сначала собери группы в библиотеке."
            );
            return;
        }

        text.AppendLine(
            "Эти группы уже собраны — их имена можно ставить в слоты как есть. «Единиц» — сколько "
                + "выходов группа даёт без повторов, «средняя» — длительность одной единицы. "
                + "Если нужного разреза библиотеки здесь нет, собери свою группу в разделе groups."
        );
        text.AppendLine();
        text.AppendLine("| Группа | Тип | Единиц | Средняя | Рейтинг | Жанр |");
        text.AppendLine("|---|---|---|---|---|---|");

        foreach (var group in groups.OrderByDescending(g => g.UnitCount))
        {
            var genre = group.PrimaryGenreId is { } id ? genres.GetValueOrDefault(id) : null;
            text.AppendLine(
                Culture,
                $"| {group.Name} | {KindName(group.DominantKind)} | {group.UnitCount} | "
                    + $"{(int)Math.Round(group.AverageUnitMinutes)} мин | "
                    + $"{AudienceName(group.Strictest)} | {genre ?? "—"} |"
            );
        }
    }

    private static void AppendShows(
        StringBuilder text,
        IReadOnlyList<ShowFact> shows,
        IReadOnlyDictionary<Guid, string> genres
    )
    {
        text.AppendLine().AppendLine("## Библиотека");
        if (shows.Count == 0)
        {
            text.AppendLine("Библиотека пуста.");
            return;
        }

        text.AppendLine(
            $"Всего готового к эфиру: {shows.Count}. Названия — ключ: собирая свои группы, пиши их "
                + "дословно отсюда, иначе шоу не найдётся при загрузке."
        );
        text.AppendLine();
        text.AppendLine("| Шоу | Тип | Год | Единиц | Средняя | Рейтинг | Жанр |");
        text.AppendLine("|---|---|---|---|---|---|---|");

        foreach (var show in shows.Take(ShowLimit))
        {
            var genre = show.PrimaryGenreId is { } id ? genres.GetValueOrDefault(id) : null;
            text.AppendLine(
                Culture,
                $"| {show.Name} | {KindName(show.Kind)} | {show.Year?.ToString(Culture) ?? "—"} | "
                    + $"{show.Units} | {show.AverageMinutes} мин | "
                    + $"{AudienceName(show.Audience)} | {genre ?? "—"} |"
            );
        }

        if (shows.Count > ShowLimit)
            text.AppendLine(
                Culture,
                $"\n…и ещё {shows.Count - ShowLimit} — список обрезан, полный состав виден по группам выше."
            );
    }

    private static void AppendNames(
        StringBuilder text,
        string title,
        IReadOnlyList<string> names,
        string empty
    )
    {
        text.AppendLine().AppendLine(Culture, $"## {title}");
        text.AppendLine(names.Count == 0 ? empty : string.Join(", ", names));
    }

    /// <summary>
    /// Рейтинг ровно так, как он пишется в файле обмена («PG-13», не «Pg13»). Модель переписывает
    /// в ответ то, что увидела в таблице, а формат читает написание с провода: разойдись эти два
    /// написания — и правило детского времени из ответа не разберётся вовсе.
    /// </summary>
    private static string AudienceName(ShowAudience? audience) =>
        audience switch
        {
            ShowAudience.G => "G",
            ShowAudience.Pg => "PG",
            ShowAudience.Pg13 => "PG-13",
            ShowAudience.R => "R",
            ShowAudience.Nc17 => "NC-17",
            _ => "—",
        };

    /// <summary>Тип контента словами: «сериал», «полный метр» — так его читает модель.</summary>
    private static string KindName(ShowKind kind) =>
        kind switch
        {
            ShowKind.Series => "сериал",
            ShowKind.Interstitial => "ролик",
            _ => "полный метр",
        };

    /// <summary>
    /// Шоу, у которых есть что показывать. Считается по готовым медиа: шоу без обработанного файла
    /// в эфир не пойдёт, и предлагать его модели значит получить сетку под несуществующий контент.
    ///
    /// Ролики в список не идут: их бывают сотни, они съедают потолок перечисления, а главное —
    /// поимённый список рекламы читается моделью как материал для слотов, и она честно собирает
    /// из него группу. В эфире это полоса рекламы, подписанная как программа.
    /// </summary>
    private async Task<List<ShowFact>> LoadShowsAsync(CancellationToken cancellationToken)
    {
        var shows = await dbContext
            .Shows.AsNoTracking()
            .Where(s => s.Kind != ShowKind.Interstitial)
            .Include(s => s.Episodes)
            .ToListAsync(cancellationToken);

        var assetIds = shows
            .SelectMany(s => s.Episodes.Select(e => e.MediaAssetId))
            .Distinct()
            .ToList();
        var ready = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a => assetIds.Contains(a.Id) && a.Status == MediaAssetStatus.Ready)
            .ToDictionaryAsync(a => a.Id, a => a.Duration ?? TimeSpan.Zero, cancellationToken);

        var primaryGenres = await dbContext
            .ShowGenres.AsNoTracking()
            .Where(g => g.IsPrimary)
            .ToDictionaryAsync(g => g.ShowId, g => g.GenreId, cancellationToken);

        var facts = new List<ShowFact>();

        foreach (var show in shows)
        {
            var durations = show
                .Episodes.Select(e =>
                    ready.TryGetValue(e.MediaAssetId, out var d) ? d : (TimeSpan?)null
                )
                .OfType<TimeSpan>()
                .ToList();
            if (durations.Count == 0)
                continue;

            facts.Add(
                new ShowFact(
                    show.Name,
                    show.Kind,
                    show.Year,
                    show.Audience,
                    durations.Count,
                    (int)Math.Round(durations.Average(d => d.TotalMinutes)),
                    primaryGenres.TryGetValue(show.Id, out var genreId) ? genreId : null
                )
            );
        }

        return
        [
            .. facts
                .OrderByDescending(s => s.Units)
                .ThenBy(s => s.Name, StringComparer.CurrentCultureIgnoreCase),
        ];
    }

    private sealed record ShowFact(
        string Name,
        ShowKind Kind,
        int? Year,
        ShowAudience? Audience,
        int Units,
        int AverageMinutes,
        Guid? PrimaryGenreId
    );
}
