namespace TeleWave.Domain.Broadcast.Live;

/// <summary>
/// Чистая математика скользящего окна HLS-live. По текущему времени, эпохе канала и материализованному
/// расписанию собирает последние N сегментов до «живого края», расставляя DISCONTINUITY на стыках
/// разных ассетов. Всё выровнено на длину сегмента (длительности ассетов кратны ей), поэтому
/// арифметика целочисленная, а `MEDIA-SEQUENCE = floor((now - epoch)/seg)` монотонен по построению.
///
/// Без БД и ФС — юнит-тестируемо (см. LiveWindowCalculatorTests).
/// </summary>
public static class LiveWindowCalculator
{
    public static LivePlaylist Build(LiveInput input)
    {
        var seg = input.SegmentSeconds;
        if (seg <= 0)
            return new LivePlaylist(0, Math.Max(seg, 1), []);

        var elapsed = (input.Now - input.Epoch).TotalSeconds;
        if (elapsed < 0)
            return new LivePlaylist(0, seg, []); // эфир ещё не начался

        var currentIndex = (long)Math.Floor(elapsed / seg);
        var windowStart = Math.Max(0, currentIndex - input.WindowSegments + 1);

        // Резолвим каждый глобальный сегмент; неразрешённые (дыра без филлера) — null.
        var resolved = new List<(Guid Asset, int Local)?>();
        for (var g = windowStart; g <= currentIndex; g++)
        {
            var segTime = input.Epoch + TimeSpan.FromSeconds(g * seg);
            resolved.Add(Resolve(segTime, g, seg, input));
        }

        // Оставляем самый длинный суффикс подряд разрешённых сегментов у живого края
        // (ведущие дыры отбрасываем, сдвигая MEDIA-SEQUENCE).
        var startIdx = 0;
        for (var i = 0; i < resolved.Count; i++)
            if (resolved[i] is null)
                startIdx = i + 1;

        if (startIdx >= resolved.Count)
            return new LivePlaylist(currentIndex + 1, seg, []);

        var mediaSequence = windowStart + startIdx;
        var segments = new List<LiveSegment>();
        Guid? previousAsset = null;
        for (var i = startIdx; i < resolved.Count; i++)
        {
            var (asset, local) = resolved[i]!.Value;
            var discontinuity = previousAsset.HasValue && previousAsset.Value != asset;
            segments.Add(new LiveSegment(asset, local, discontinuity));
            previousAsset = asset;
        }

        return new LivePlaylist(mediaSequence, seg, segments);
    }

    private static (Guid Asset, int Local)? Resolve(
        DateTimeOffset segTime,
        long globalIndex,
        int seg,
        LiveInput input
    )
    {
        var entry = input.Entries.FirstOrDefault(e =>
            segTime >= e.StartsAtUtc && segTime < e.EndsAtUtc
        );
        if (entry is not null)
        {
            var local = (int)Math.Floor((segTime - entry.StartsAtUtc).TotalSeconds / seg);
            if (local >= 0 && local < entry.SegmentCount)
                return (entry.MediaAssetId, local);
        }

        if (input.Filler is { SegmentCount: > 0 } filler)
        {
            var local = (int)(
                ((globalIndex % filler.SegmentCount) + filler.SegmentCount) % filler.SegmentCount
            );
            return (filler.AssetId, local);
        }

        return null;
    }
}
