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>
/// Фоновый слой в паузах: остаток слота короче серии, добор до якоря, пустой слот-повтор. Раньше
/// всё это закрывал аварийный запас — зацикленный филлер канала, — и в эфире получались часы
/// прочерков там, где по замыслу должен идти фон со своей группой и своим временем суток.
///
/// Состояние держится на прогон: фон зовут десятки раз за сутки, и он обязан продолжаться с той
/// серии, на которой остановился, а не начинаться заново в каждой паузе.
/// </summary>
public sealed class BackgroundFiller(IReadOnlyList<PlanningSlot> slots, IRandomSource random)
{
    private readonly Dictionary<Guid, BackgroundState> _states = [];

    /// <summary>
    /// Единица фона под остаток <paramref name="available"/>; null — фон на это время не задан,
    /// пуст или его серия длиннее остатка. Короткий хвост добирает аварийный запас.
    /// </summary>
    public BackgroundPick? Next(DateTimeOffset moment, TimeSpan available)
    {
        if (SlotAt(moment) is not { } slot)
            return null;

        var state = State(slot);
        if (!state.HasUnitsLeft && !Reselect(slot, state, moment))
            return null;

        var unit = state.Element!.Units[state.NextUnitIndex];
        if (unit.Duration <= TimeSpan.Zero || unit.Duration > available)
            return null;

        state.NextUnitIndex++;
        return new BackgroundPick(slot, state.Element, unit, slot.Strategy.Kind);
    }

    /// <summary>
    /// Куда доиграл фон. Слоты, которые в этом прогоне шли ещё и обычными (не были перекрыты),
    /// пропускаются: их курсор ведёт основной цикл, и два независимых счётчика одной позиции
    /// перетирали бы друг друга.
    /// </summary>
    public IEnumerable<PlanningCursorUpdate> Cursors(IReadOnlySet<Guid> ownedByMainLoop) =>
        _states
            .Where(pair => !ownedByMainLoop.Contains(pair.Key) && pair.Value.Element is not null)
            .Select(pair => new PlanningCursorUpdate(
                pair.Key,
                pair.Value.Element!.Kind,
                pair.Value.Element.ElementId,
                pair.Value.NextUnitIndex
            ));

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

    private BackgroundState State(PlanningSlot slot)
    {
        if (_states.TryGetValue(slot.SlotId, out var existing))
            return existing;

        // Первый заход — стартуем с того места, где фон остановился в прошлом прогоне.
        var state = new BackgroundState();
        if (slot.Cursor?.ElementId is { } elementId)
        {
            state.Element = slot.Elements.FirstOrDefault(e =>
                e.ElementId == elementId && e.Kind == slot.Cursor.ElementKind
            );
            state.NextUnitIndex = state.Element is null
                ? 0
                : Math.Clamp(slot.Cursor.NextUnitIndex, 0, state.Element.Units.Count);
        }

        _states[slot.SlotId] = state;
        return state;
    }

    /// <summary>Берёт следующий элемент по стратегии слота; false — играть в фоне нечем.</summary>
    private bool Reselect(PlanningSlot slot, BackgroundState state, DateTimeOffset moment)
    {
        var withCursor = slot with
        {
            Cursor = state.Element is null
                ? null
                : new PlanningCursor(
                    state.Element.Kind,
                    state.Element.ElementId,
                    state.NextUnitIndex
                ),
        };

        if (ElementSelector.Select(withCursor, moment, random) is not { } pick)
            return false;

        state.Element = pick.Element;
        state.NextUnitIndex = pick.StartUnitIndex;
        return state.HasUnitsLeft;
    }

    /// <summary>Где фоновый слот остановился: элемент и следующая единица внутри него.</summary>
    private sealed class BackgroundState
    {
        public PlanningElement? Element { get; set; }
        public int NextUnitIndex { get; set; }

        public bool HasUnitsLeft => Element is not null && NextUnitIndex < Element.Units.Count;
    }
}
