namespace TeleWave.Domain.Programming.Planning;

/// <summary>Состояние стыков в рамках прогона: когда какая врезка ставилась последний раз.</summary>
public sealed class JunctionHistory
{
    private readonly Dictionary<JunctionElementKind, DateTimeOffset> _lastPlaced = [];

    public bool Allows(PlanningJunctionElement element, DateTimeOffset moment)
    {
        if (element.MinMinutesBetween <= 0)
            return true;
        if (!_lastPlaced.TryGetValue(element.Kind, out var last))
            return true;
        return moment - last >= TimeSpan.FromMinutes(element.MinMinutesBetween);
    }

    public void Record(PlanningJunctionElement element, DateTimeOffset moment) =>
        _lastPlaced[element.Kind] = moment;
}

/// <summary>
/// Где ставится стык и между чем. Собрано в один параметр: по отдельности эти четыре значения ехали
/// сквозь всю раскладку и вместе с курсором, пределом и накопителями раздували сигнатуры.
/// </summary>
/// <param name="FromShowId">Шоу перед стыком, <paramref name="ToShowId"/> — после; заставке нужна
/// именно пара соседей, её ассет рендерится под неё после сборки ленты.</param>
public sealed record JunctionPlacement(
    Guid SlotId,
    Guid? FromShowId,
    Guid? ToShowId,
    bool ElementChanged
);

/// <summary>
/// Раскладка врезок стыка: реклама, промо, заставка, заполнитель. Ставит только то, что влезает
/// целиком до предела (якорь или горизонт) — обрезать врезку нельзя, а перехлёст сдвинул бы якорь.
///
/// Обязательные врезки (<see cref="PlanningJunctionElement.IsRequired"/>) идут первыми: при нехватке
/// времени выбрасываются необязательные, а не то, ради чего стык и заведён.
/// </summary>
public static class JunctionFiller
{
    /// <summary>Раскладывает стык от <paramref name="cursor"/>. Возвращает курсор после стыка.</summary>
    public static DateTimeOffset Fill(
        PlanningJunction? junction,
        DateTimeOffset cursor,
        DateTimeOffset limit,
        JunctionPlacement placement,
        JunctionHistory history,
        List<PlannedItem> items,
        PlanTrace? trace
    )
    {
        if (junction is null || junction.Elements.Count == 0)
            return cursor;

        var ordered = junction
            .Elements.OrderByDescending(e => e.IsRequired)
            .ThenBy(e => junction.Elements.ToList().IndexOf(e))
            .ToList();

        foreach (var element in ordered)
        {
            if (element.OnlyOnElementChange && !placement.ElementChanged)
                continue;
            if (!history.Allows(element, cursor))
                continue;

            var placedAt = cursor;
            cursor =
                element.Kind == JunctionElementKind.Bumper
                    ? PlaceBumper(element, cursor, limit, placement, items, trace)
                    : PlaceUnits(element, cursor, limit, placement.SlotId, items, trace);

            if (cursor > placedAt)
                history.Record(element, placedAt);
        }

        return cursor;
    }

    /// <summary>
    /// Резервирует место под заставку. Ассет пуст: он рендерится под конкретную пару «из/в» уже после
    /// того, как лента собрана, — до наполнения слотов пара попросту неизвестна.
    /// </summary>
    private static DateTimeOffset PlaceBumper(
        PlanningJunctionElement element,
        DateTimeOffset cursor,
        DateTimeOffset limit,
        JunctionPlacement placement,
        List<PlannedItem> items,
        PlanTrace? trace
    )
    {
        if (element.BumperDuration <= TimeSpan.Zero || cursor + element.BumperDuration > limit)
            return cursor;

        var end = cursor + element.BumperDuration;
        items.Add(
            new PlannedItem(
                Guid.Empty,
                cursor,
                end,
                placement.ToShowId,
                null,
                placement.SlotId,
                PlannedItemKind.Bumper,
                trace,
                element.BumperTemplateId,
                placement.FromShowId,
                placement.ToShowId
            )
        );
        return end;
    }

    /// <summary>Ставит единицы врезки по её бюджету: N штук либо пока не наберётся M минут.</summary>
    private static DateTimeOffset PlaceUnits(
        PlanningJunctionElement element,
        DateTimeOffset cursor,
        DateTimeOffset limit,
        Guid slotId,
        List<PlannedItem> items,
        PlanTrace? trace
    )
    {
        if (element.Units.Count == 0)
            return cursor;

        var kind = element.Kind switch
        {
            JunctionElementKind.Ad => PlannedItemKind.Ad,
            JunctionElementKind.Promo => PlannedItemKind.Promo,
            _ => PlannedItemKind.Fallback,
        };

        var placed = 0;
        var accumulated = TimeSpan.Zero;
        var index = 0;

        while (index < element.Units.Count)
        {
            var unit = element.Units[index];
            index++;

            if (unit.Duration <= TimeSpan.Zero || cursor + unit.Duration > limit)
                break;

            var enough =
                element.AmountMode == JunctionAmountMode.Count
                    ? placed >= Math.Max(1, element.AmountValue)
                    // Последняя единица входит целиком: рекламный блок не разрезается.
                    : accumulated >= TimeSpan.FromMinutes(Math.Max(1, element.AmountValue));
            if (enough)
                break;

            items.Add(
                new PlannedItem(
                    unit.MediaAssetId,
                    cursor,
                    cursor + unit.Duration,
                    null,
                    null,
                    slotId,
                    kind,
                    trace
                )
            );
            cursor += unit.Duration;
            accumulated += unit.Duration;
            placed++;
        }

        return cursor;
    }
}
