using TeleWave.Application.Programming.Templates;
using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Planning;

/// <summary>Слот, привязанный к конкретным вещательным суткам и переведённый в UTC.</summary>
public sealed record ScheduledSlot(Slot Slot, DateOnly BroadcastDate, DateTimeOffset StartUtc);

/// <summary>
/// Эффективная сетка: что реально действует на диапазон. <paramref name="Background"/> — слоты
/// фонового слоя, перекрытые слоями выше: в эфир они сами не идут, но ими закрываются паузы
/// (остаток слота, добор до якоря, пустой повтор), и без них там играл бы аварийный филлер.
/// </summary>
public sealed record EffectiveGrid(
    IReadOnlyList<ScheduledSlot> Slots,
    IReadOnlyList<ScheduledSlot> Background
);

/// <summary>
/// Разворачивает шаблон в эффективную сетку на диапазон дат: какие слоты и в какие моменты реально
/// действуют. Применимость слоёв разрешается здесь, потому что её схема (JSON) принадлежит
/// прикладному слою; чистый планировщик получает уже готовые окна в UTC.
/// </summary>
public static class EffectiveGridBuilder
{
    public static EffectiveGrid Build(
        ScheduleTemplate template,
        int utcOffsetMinutes,
        TimeOnly dayStartTime,
        DateTimeOffset fromUtc,
        DateTimeOffset toUtc
    )
    {
        var offset = TimeSpan.FromMinutes(utcOffsetMinutes);
        var result = new List<ScheduledSlot>();
        var background = new List<ScheduledSlot>();

        // Захватываем сутки с запасом с обеих сторон: слот вещательных суток может начаться
        // до fromUtc (ночной блок предыдущего дня) либо после полуночи следующих.
        var firstDate = DateOnly.FromDateTime(fromUtc.ToOffset(offset).DateTime).AddDays(-1);
        var lastDate = DateOnly.FromDateTime(toUtc.ToOffset(offset).DateTime).AddDays(1);

        var layers = template
            .Layers.Where(l => l.IsEnabled)
            .OrderByDescending(l => l.Priority)
            .ToList();

        for (var date = firstDate; date <= lastDate; date = date.AddDays(1))
        {
            var applicable = layers
                .Where(l =>
                    LayerApplicability.FromJson(l.ApplicabilityJson) is not { } applicability
                    || applicability.Covers(date)
                )
                .ToList();

            var (day, skipped) = ResolveDay(applicable, date, offset, dayStartTime);
            foreach (var scheduled in day)
            {
                if (scheduled.StartUtc >= fromUtc.AddDays(-1) && scheduled.StartUtc < toUtc)
                    result.Add(scheduled);
            }

            foreach (var scheduled in skipped)
            {
                if (scheduled.StartUtc >= fromUtc.AddDays(-1) && scheduled.StartUtc < toUtc)
                    background.Add(scheduled);
            }
        }

        return new EffectiveGrid(
            result.OrderBy(s => s.StartUtc).ToList(),
            background.OrderBy(s => s.StartUtc).ToList()
        );
    }

    /// <summary>
    /// Слоты одних вещательных суток с разрешённым перекрытием слоёв. Слои идут от старшего
    /// приоритета к младшему; слот младшего слоя пропускается, если его окно уже занято старшим.
    ///
    /// Именно пропускается, а не обрезается: слот — атомарная единица со своей группой и стратегией,
    /// и половина слота означала бы половину настройки.
    ///
    /// Пропущенные слоты фонового слоя возвращаются отдельно: в эфир они не идут, но именно ими
    /// закрываются паузы внутри чужих слотов.
    /// </summary>
    private static (
        IReadOnlyList<ScheduledSlot> Slots,
        IReadOnlyList<ScheduledSlot> Background
    ) ResolveDay(
        IReadOnlyList<GridLayer> layers,
        DateOnly date,
        TimeSpan offset,
        TimeOnly dayStartTime
    )
    {
        var taken = new List<(TimeSpan From, TimeSpan To)>();
        var result = new List<ScheduledSlot>();
        var background = new List<ScheduledSlot>();

        foreach (var layer in layers)
        {
            var slots = layer
                .Slots.Where(s => s.Weekday is null || s.Weekday == (int)date.DayOfWeek)
                .OrderBy(s => s.TargetStart)
                .ToList();

            // Слоты одного слоя добавляются все разом: внутри слоя перекрытий не бывает по построению,
            // а занятые интервалы должны учитываться только от более приоритетных слоёв.
            var added = new List<(TimeSpan From, TimeSpan To)>();

            foreach (var slot in slots)
            {
                var from = OffsetInBroadcastDay(slot.TargetStart, dayStartTime);
                var to = from + TimeSpan.FromMinutes(slot.TargetDurationMinutes);

                var scheduled = new ScheduledSlot(
                    slot,
                    date,
                    ToUtc(date, slot.TargetStart, offset, dayStartTime)
                );

                if (taken.Any(t => from < t.To && t.From < to))
                {
                    if (layer.IsBackground)
                        background.Add(scheduled);
                    continue;
                }

                result.Add(scheduled);
                added.Add((from, to));
            }

            taken.AddRange(added);
        }

        return (result, background);
    }

    /// <summary>
    /// Смещение времени от начала вещательных суток. Ночной блок (00:00–06:00 при старте суток
    /// в 06:00) относится к предыдущему дню, поэтому его смещение больше суточного, а не отрицательное.
    /// </summary>
    private static TimeSpan OffsetInBroadcastDay(TimeOnly time, TimeOnly dayStartTime) =>
        time >= dayStartTime
            ? time.ToTimeSpan() - dayStartTime.ToTimeSpan()
            : time.ToTimeSpan() - dayStartTime.ToTimeSpan() + TimeSpan.FromDays(1);

    /// <summary>Момент слота в UTC. Время до начала вещательных суток попадает на следующий календарный день.</summary>
    public static DateTimeOffset ToUtc(
        DateOnly broadcastDate,
        TimeOnly time,
        TimeSpan offset,
        TimeOnly dayStartTime
    )
    {
        var calendarDate = time >= dayStartTime ? broadcastDate : broadcastDate.AddDays(1);
        return new DateTimeOffset(calendarDate.ToDateTime(time), offset).ToUniversalTime();
    }
}
