using TeleWave.Domain.Broadcast.Scheduling;

namespace TeleWave.Domain.Programming.Planning;

/// <summary>Что фоновый слой отдал в паузу: единица, слот-источник и выбранный элемент (для трейса).</summary>
public sealed record BackgroundPick(
    PlanningSlot Slot,
    PlanningElement Element,
    PlanningUnit Unit,
    SlotStrategyKind Strategy
);

/// <summary>
/// Фоновый слой в паузах: остаток слота короче серии, добор до якоря, пустой слот-повтор. Раньше
/// всё это закрывал аварийный запас — зацикленный филлер канала, — и в эфире получались часы
/// прочерков там, где по замыслу должен идти фон со своей группой и своим временем суток.
///
/// Позиция слота живёт не здесь, а в общем на прогон <see cref="SlotCursors"/>: тот же слот может
/// в одни сутки идти обычным (его ведёт основной цикл), а в другие быть перекрытым и играть только
/// в паузах. Со своим счётчиком фон переигрывал бы серии, уже поставленные основным циклом.
/// </summary>
public sealed class BackgroundFiller(
    IReadOnlyList<PlanningSlot> slots,
    IRandomSource random,
    SlotCursors cursors,
    RunPlayHistory plays
)
{
    /// <summary>
    /// Единица фона под остаток <paramref name="available"/>; null — фон на это время не задан,
    /// пуст или его серия длиннее остатка. Короткий хвост добирает аварийный запас.
    /// </summary>
    public BackgroundPick? Next(DateTimeOffset moment, TimeSpan available)
    {
        if (SlotAt(moment) is not { } scheduled)
            return null;

        var slot = cursors.Apply(scheduled);

        // Начатый элемент доигрывается; исчерпан — берём следующий по стратегии слота.
        if ((Position(slot) ?? Reselect(slot, moment)) is not { } position)
            return null;

        var (element, nextUnitIndex) = position;
        var unit = element.Units[nextUnitIndex];
        if (unit.Duration <= TimeSpan.Zero || unit.Duration > available)
            return null;

        cursors.Advance(slot.SlotId, element, nextUnitIndex + 1);
        return new BackgroundPick(slot, element, unit, slot.Strategy.Kind);
    }

    /// <summary>Активный фоновый слот на момент; сутки фон покрывает целиком, но дыры возможны.</summary>
    private PlanningSlot? SlotAt(DateTimeOffset moment) =>
        slots.FirstOrDefault(s => s.TargetStartUtc <= moment && moment < s.TargetEndUtc);

    /// <summary>Где слот стоит сейчас, если начатый элемент ещё не доигран.</summary>
    private static (PlanningElement Element, int NextUnitIndex)? Position(PlanningSlot slot)
    {
        if (slot.Cursor?.ElementId is not { } elementId)
            return null;

        var element = slot.Elements.FirstOrDefault(e =>
            e.ElementId == elementId && e.Kind == slot.Cursor.ElementKind
        );
        if (element is null)
            return null;

        var index = Math.Clamp(slot.Cursor.NextUnitIndex, 0, element.Units.Count);
        return index < element.Units.Count ? (element, index) : null;
    }

    /// <summary>Берёт следующий элемент по стратегии слота; null — играть в фоне нечем.</summary>
    private (PlanningElement Element, int NextUnitIndex)? Reselect(
        PlanningSlot slot,
        DateTimeOffset moment
    )
    {
        if (ElementSelector.Select(slot, moment, random, plays) is not { } pick)
            return null;

        var index = Math.Clamp(pick.StartUnitIndex, 0, pick.Element.Units.Count);
        return index < pick.Element.Units.Count ? (pick.Element, index) : null;
    }
}
