using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using TeleWave.Application.Broadcast.Scheduling;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Templates;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;

namespace TeleWave.Application.Programming.Planning.DebugExport;

/// <summary>
/// Собирает отладочный снимок канала: вход планировщика, его состояние и то, что получилось.
///
/// Смысл дампа — отвечать на вопрос «почему сетка построилась так», не подключаясь к рабочей базе.
/// Поэтому здесь не выжимка, а полный вход: развёрнутые группы вплоть до отдельных единиц,
/// эффективная сетка по суткам, курсоры слотов, стыки с условиями, записанная лента с трейсом
/// и сухой прогон от текущего состояния. Всё, что хранится JSON-строкой (стратегия, правила,
/// условия врезки, трейс), разбирается в объекты: читать дамп глазами придётся чаще, чем кодом.
/// </summary>
public sealed class ChannelDebugCollector(
    IAppDbContext dbContext,
    GroupExpander expander,
    GridScheduleGenerator generator,
    IOptions<SchedulerOptions> options
)
{
    private readonly SchedulerOptions _options = options.Value;

    /// <summary>Сколько прошлого класть в дамп: сдвиги и повторы объясняются вчерашним эфиром.</summary>
    private const int PastDays = 2;

    public async Task<IReadOnlyList<DebugFile>> CollectAsync(
        Channel channel,
        ScheduleTemplate template,
        DateTimeOffset now,
        CancellationToken cancellationToken
    )
    {
        var horizonDays = Math.Max(1, _options.HorizonDays);
        var from = now.AddDays(-PastDays);
        var to = now.AddDays(horizonDays);

        var grid = EffectiveGridBuilder.Build(
            template,
            channel.UtcOffsetMinutes,
            channel.DayStartTime,
            now,
            to
        );

        var junctions = await dbContext
            .JunctionTemplates.AsNoTracking()
            .Include(j => j.Elements)
            .ToListAsync(cancellationToken);

        var groups = await dbContext.Groups.AsNoTracking().ToListAsync(cancellationToken);
        var groupNames = groups.ToDictionary(g => g.Id, g => g.Name);
        var junctionNames = junctions.ToDictionary(j => j.Id, j => j.Name);
        var bumperNames = await dbContext
            .BumperTemplates.AsNoTracking()
            .ToDictionaryAsync(b => b.Id, b => b.Name, cancellationToken);

        // Разворачиваем ровно те группы, что участвуют в эфире канала: чужие только раздули бы дамп.
        var usedGroupIds = grid
            .Slots.Concat(grid.Background)
            .Select(s => s.Slot.GroupId)
            .Concat(junctions.SelectMany(j => j.Elements).Select(e => e.GroupId))
            .Append(template.FallbackGroupId)
            .Where(id => id is not null)
            .Select(id => id!.Value)
            .Distinct()
            .ToList();

        var expanded = await expander.ExpandAsync(usedGroupIds, channel.Id, cancellationToken);
        var showNames = await ShowNamesAsync(expanded, cancellationToken);

        var slotIds = grid.Slots.Concat(grid.Background).Select(s => s.Slot.Id).Distinct().ToList();
        var slotTitles = template
            .Layers.SelectMany(l => l.Slots)
            .ToDictionary(s => s.Id, s => s.Title);
        var states = await dbContext
            .SlotStates.AsNoTracking()
            .Where(s => slotIds.Contains(s.SlotId))
            .ToListAsync(cancellationToken);

        var entries = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e => e.ChannelId == channel.Id && e.StartsAtUtc < to && e.EndsAtUtc > from)
            .OrderBy(e => e.StartsAtUtc)
            .ToListAsync(cancellationToken);

        var entryShowNames = await dbContext
            .Shows.AsNoTracking()
            .Where(s => entries.Select(e => e.ShowId).Contains(s.Id))
            .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);

        // Сухой прогон от текущего состояния: то, что планировщик построил бы прямо сейчас. Рядом
        // с записанной лентой по нему видно, расходится ли расчёт с тем, что уже в эфире.
        var preview = await generator.PreviewAsync(channel.Id, now, horizonDays, cancellationToken);

        return
        [
            new DebugFile("README.md", Readme(channel, now, horizonDays)),
            new DebugFile(
                "summary.json",
                Summary(channel, template, now, horizonDays, grid, entries, preview)
            ),
            new DebugFile("channel.json", ChannelFacts(channel)),
            new DebugFile("template.json", TemplateFacts(template, groupNames, junctionNames)),
            new DebugFile("slot-states.json", StateFacts(states, slotTitles)),
            new DebugFile("effective-grid.json", GridFacts(grid, groupNames)),
            new DebugFile("groups.json", GroupFacts(expanded, groupNames, showNames)),
            new DebugFile("junctions.json", JunctionFacts(junctions, groupNames, bumperNames)),
            new DebugFile("schedule.json", ScheduleFacts(entries, slotTitles, entryShowNames)),
            new DebugFile("plan-preview.json", PreviewFacts(preview, slotTitles, showNames)),
        ];
    }

    private static string Readme(Channel channel, DateTimeOffset now, int horizonDays) =>
        $"""
            # Отладочный дамп канала «{channel.Name}»

            Собран: {now:yyyy-MM-dd HH:mm:ss} UTC. Горизонт планирования: {horizonDays} сут.,
            прошлое в дампе: {PastDays} сут.

            - `summary.json` — что за канал, сколько чего собралось, свод предупреждений.
            - `channel.json` — настройки канала: время, начало вещательных суток, эпоха, филлер.
            - `template.json` — сетка: слои, слоты, стратегии, правила канала. JSON-поля разобраны.
            - `slot-states.json` — курсоры слотов: на чём каждый слот остановился.
            - `effective-grid.json` — эффективная сетка на горизонт: экземпляры слотов по суткам
              с временами в UTC. `background` — перекрытые слоты фонового слоя.
            - `groups.json` — развёрнутые группы вплоть до единиц воспроизведения: что именно
              планировщик мог поставить и в каком порядке.
            - `junctions.json` — стыки: врезки, условия показа, развилки.
            - `schedule.json` — записанная лента с трейсом «почему это здесь».
            - `plan-preview.json` — сухой прогон от текущего состояния: что построилось бы сейчас.

            Времена везде UTC. Время канала = UTC{(
                channel.UtcOffsetMinutes < 0 ? "-" : "+"
            )}{Math.Abs(channel.UtcOffsetMinutes) / 60:00}:{Math.Abs(channel.UtcOffsetMinutes)
                % 60:00}.
            """;

    private object Summary(
        Channel channel,
        ScheduleTemplate template,
        DateTimeOffset now,
        int horizonDays,
        EffectiveGrid grid,
        IReadOnlyList<ScheduleEntry> entries,
        PlanningResult? preview
    ) =>
        new
        {
            CollectedAtUtc = now,
            Channel = new
            {
                channel.Id,
                channel.Name,
                channel.Slug,
                channel.IsEnabled,
            },
            Scheduler = new
            {
                HorizonDays = horizonDays,
                _options.RetentionDays,
                _options.TickMinutes,
            },
            Template = new
            {
                template.Id,
                template.Revision,
                template.HasPendingChanges,
                Layers = template.Layers.Count,
                Slots = template.Layers.Sum(l => l.Slots.Count),
            },
            Grid = new { Instances = grid.Slots.Count, Background = grid.Background.Count },
            Tape = new
            {
                Entries = entries.Count,
                ByKind = entries
                    .GroupBy(e => e.Kind)
                    .ToDictionary(g => g.Key.ToString(), g => g.Count()),
                FirstUtc = entries.Count > 0 ? entries[0].StartsAtUtc : (DateTimeOffset?)null,
                LastUtc = entries.Count > 0 ? entries[^1].EndsAtUtc : (DateTimeOffset?)null,
            },
            Preview = new
            {
                Items = preview?.Items.Count ?? 0,
                Warnings = preview
                    ?.Warnings.GroupBy(w => w.Kind)
                    .ToDictionary(g => g.Key.ToString(), g => g.Count()),
            },
        };

    private static object ChannelFacts(Channel channel) =>
        new
        {
            channel.Id,
            channel.Name,
            channel.Slug,
            channel.Number,
            channel.IsEnabled,
            channel.UtcOffsetMinutes,
            channel.DayStartTime,
            channel.EpochUtc,
            channel.TemplateId,
            channel.FillerAssetId,
            channel.LogoImageId,
            channel.ShowClock,
            channel.AnalogFilterStrength,
        };

    private static object TemplateFacts(
        ScheduleTemplate template,
        IReadOnlyDictionary<Guid, string> groupNames,
        IReadOnlyDictionary<Guid, string> junctionNames
    ) =>
        new
        {
            template.Id,
            template.Name,
            template.Revision,
            template.HasPendingChanges,
            FallbackGroup = Named(template.FallbackGroupId, groupNames),
            DefaultJunction = Named(template.DefaultJunctionId, junctionNames),
            Rules = PlanningRules.FromJson(template.RulesJson),
            Layers = template
                .Layers.OrderByDescending(l => l.Priority)
                .Select(layer => new
                {
                    layer.Id,
                    layer.Name,
                    layer.Priority,
                    layer.IsEnabled,
                    layer.IsBackground,
                    Applicability = LayerApplicability.FromJson(layer.ApplicabilityJson),
                    Slots = layer
                        .Slots.OrderBy(s => s.Weekday)
                        .ThenBy(s => s.TargetStart)
                        .Select(slot => new
                        {
                            slot.Id,
                            slot.Title,
                            slot.Weekday,
                            slot.TargetStart,
                            slot.TargetDurationMinutes,
                            slot.Daypart,
                            slot.SlotKind,
                            Group = Named(slot.GroupId, groupNames),
                            Strategy = SlotStrategy.FromJson(slot.StrategyJson),
                            Repeat = RepeatSource.FromJson(slot.RepeatSourceJson),
                            slot.BlockMode,
                            slot.BlockValue,
                            slot.OverflowPolicy,
                            slot.IsAnchor,
                            slot.MaxDriftMinutes,
                            slot.SnapToMinutes,
                            JunctionBetween = Named(slot.JunctionBetweenId, junctionNames),
                            JunctionAfter = Named(slot.JunctionAfterId, junctionNames),
                        }),
                }),
        };

    private static object StateFacts(
        IReadOnlyList<SlotState> states,
        IReadOnlyDictionary<Guid, string> slotTitles
    ) =>
        states.Select(state => new
        {
            state.SlotId,
            Slot = slotTitles.GetValueOrDefault(state.SlotId),
            state.CurrentElementKind,
            state.CurrentElementId,
            state.NextUnitIndex,
        });

    private static object GridFacts(
        EffectiveGrid grid,
        IReadOnlyDictionary<Guid, string> groupNames
    ) =>
        new
        {
            Slots = grid.Slots.Select(s => Instance(s, groupNames)),
            Background = grid.Background.Select(s => Instance(s, groupNames)),
        };

    private static object Instance(
        ScheduledSlot scheduled,
        IReadOnlyDictionary<Guid, string> groupNames
    ) =>
        new
        {
            scheduled.Slot.Id,
            scheduled.Slot.Title,
            scheduled.BroadcastDate,
            StartUtc = scheduled.StartUtc,
            EndUtc = scheduled.StartUtc.AddMinutes(scheduled.Slot.TargetDurationMinutes),
            scheduled.Slot.TargetDurationMinutes,
            scheduled.Slot.SlotKind,
            scheduled.Slot.IsAnchor,
            Group = Named(scheduled.Slot.GroupId, groupNames),
        };

    private static object GroupFacts(
        IReadOnlyDictionary<Guid, IReadOnlyList<PlanningElement>> expanded,
        IReadOnlyDictionary<Guid, string> groupNames,
        IReadOnlyDictionary<Guid, string> showNames
    ) =>
        expanded.Select(pair => new
        {
            GroupId = pair.Key,
            Name = groupNames.GetValueOrDefault(pair.Key),
            Elements = pair.Value.Select(element => new
            {
                element.Kind,
                element.ElementId,
                element.Weight,
                element.Position,
                element.Audience,
                element.LastPlayedUtc,
                UnitCount = element.Units.Count,
                TotalMinutes = Math.Round(element.Units.Sum(u => u.Duration.TotalMinutes), 1),
                Units = element.Units.Select(unit => new
                {
                    unit.MediaAssetId,
                    unit.ShowId,
                    Show = showNames.GetValueOrDefault(unit.ShowId),
                    unit.UnitIndex,
                    Minutes = Math.Round(unit.Duration.TotalMinutes, 2),
                }),
            }),
        });

    private static object JunctionFacts(
        IReadOnlyList<JunctionTemplate> junctions,
        IReadOnlyDictionary<Guid, string> groupNames,
        IReadOnlyDictionary<Guid, string> bumperNames
    ) =>
        junctions.Select(junction => new
        {
            junction.Id,
            junction.Name,
            junction.MaxTotalSeconds,
            Elements = junction
                .Elements.OrderBy(e => e.Position)
                .Select(element => new
                {
                    element.Id,
                    element.Position,
                    element.Kind,
                    Group = Named(element.GroupId, groupNames),
                    BumperTemplate = Named(element.BumperTemplateId, bumperNames),
                    element.BumperVariantId,
                    element.AmountMode,
                    element.AmountValue,
                    element.IsRequired,
                    element.ChoiceKey,
                    element.ChoiceWeight,
                    Conditions = JunctionConditions.FromJson(element.ConditionsJson),
                }),
        });

    private static object ScheduleFacts(
        IReadOnlyList<ScheduleEntry> entries,
        IReadOnlyDictionary<Guid, string> slotTitles,
        IReadOnlyDictionary<Guid, string> showNames
    ) =>
        entries.Select(entry => new
        {
            entry.Id,
            entry.StartsAtUtc,
            entry.EndsAtUtc,
            Minutes = Math.Round((entry.EndsAtUtc - entry.StartsAtUtc).TotalMinutes, 2),
            entry.Kind,
            entry.ShowId,
            Show = entry.ShowId is { } showId ? showNames.GetValueOrDefault(showId) : null,
            entry.EpisodeIndex,
            entry.CollectionId,
            entry.SlotId,
            Slot = entry.SlotId is { } slotId ? slotTitles.GetValueOrDefault(slotId) : null,
            entry.MediaAssetId,
            Trace = Trace(entry.TraceJson),
        });

    /// <summary>
    /// Трейс записи как объект, а не строкой в строке. Разбирается в узел, а не в <c>PlanTrace</c>:
    /// в старых записях полей могло быть меньше, и терять их при типизации — ровно тот случай,
    /// когда дамп перестаёт объяснять прошлое.
    /// </summary>
    private static JsonNode? Trace(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return null;

        try
        {
            return JsonNode.Parse(json);
        }
        catch (JsonException)
        {
            return null;
        }
    }

    private static object? PreviewFacts(
        PlanningResult? preview,
        IReadOnlyDictionary<Guid, string> slotTitles,
        IReadOnlyDictionary<Guid, string> showNames
    ) =>
        preview is null
            ? null
            : new
            {
                Warnings = preview.Warnings.Select(w => new
                {
                    w.Kind,
                    w.SlotId,
                    Slot = w.SlotId is { } id ? slotTitles.GetValueOrDefault(id) : null,
                    w.Details,
                }),
                Cursors = preview.Cursors,
                Items = preview.Items.Select(item => new
                {
                    item.StartsAtUtc,
                    item.EndsAtUtc,
                    Minutes = Math.Round((item.EndsAtUtc - item.StartsAtUtc).TotalMinutes, 2),
                    item.Kind,
                    item.ShowId,
                    Show = item.ShowId is { } showId ? showNames.GetValueOrDefault(showId) : null,
                    item.UnitIndex,
                    item.CollectionId,
                    item.SlotId,
                    Slot = item.SlotId is { } slotId ? slotTitles.GetValueOrDefault(slotId) : null,
                    item.MediaAssetId,
                    item.Trace,
                }),
            };

    /// <summary>Ссылка «идентификатор + имя»: по одному GUID в дампе ничего не понять.</summary>
    private static object? Named(Guid? id, IReadOnlyDictionary<Guid, string> names) =>
        id is not { } value ? null : new { Id = value, Name = names.GetValueOrDefault(value) };

    private async Task<Dictionary<Guid, string>> ShowNamesAsync(
        IReadOnlyDictionary<Guid, IReadOnlyList<PlanningElement>> expanded,
        CancellationToken cancellationToken
    )
    {
        var showIds = expanded
            .Values.SelectMany(elements => elements)
            .SelectMany(element => element.Units)
            .Select(unit => unit.ShowId)
            .Distinct()
            .ToList();

        return await dbContext
            .Shows.AsNoTracking()
            .Where(s => showIds.Contains(s.Id))
            .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
    }
}
