namespace TeleWave.Domain.Programming;

/// <summary>
/// Слот сетки: когда и чем заполнять эфир. Времена — цели, а не жёсткие границы: контент идёт встык,
/// слот считается исчерпанным по бюджету, расхождение переносится на следующий. Опорные точки держат
/// якоря (<see cref="IsAnchor"/>) и мягкое округление (<see cref="SnapToMinutes"/>).
/// </summary>
public class Slot
{
    public Guid Id { get; private set; }
    public Guid LayerId { get; private set; }

    /// <summary>День недели вещательных суток (0=Вс..6=Сб) или null — каждый день.</summary>
    public int? Weekday { get; private set; }

    /// <summary>Целевое время старта в сутках канала.</summary>
    public TimeOnly TargetStart { get; private set; }

    /// <summary>Бюджет слота в минутах.</summary>
    public int TargetDurationMinutes { get; private set; }

    public string Title { get; private set; } = string.Empty;
    public Daypart Daypart { get; private set; }
    public SlotKind SlotKind { get; private set; }

    /// <summary>Группа контента — для <see cref="SlotKind.Content"/>.</summary>
    public Guid? GroupId { get; private set; }

    /// <summary>Стратегия выбора элемента (JSON). Домен её не интерпретирует — схема в Application.</summary>
    public string? StrategyJson { get; private set; }

    /// <summary>Откуда брать повтор (JSON) — для <see cref="SlotKind.Repeat"/>.</summary>
    public string? RepeatSourceJson { get; private set; }

    public SlotBlockMode BlockMode { get; private set; }

    /// <summary>Единиц (<see cref="SlotBlockMode.Count"/>) или минут (<see cref="SlotBlockMode.Duration"/>).</summary>
    public int BlockValue { get; private set; }

    public OverflowPolicy OverflowPolicy { get; private set; }

    /// <summary>Старт жёсткий: генератор не начнёт единицу, которая через него перелезет.</summary>
    public bool IsAnchor { get; private set; }

    /// <summary>Допуск отклонения фактического старта от целевого, минуты.</summary>
    public int MaxDriftMinutes { get; private set; }

    /// <summary>
    /// Округлять старт до кратного N минут (5/10/15/30) или null. Мягкое, в отличие от якоря: если
    /// добирать пришлось бы дольше <see cref="MaxDriftMinutes"/>, округление пропускается.
    /// </summary>
    public int? SnapToMinutes { get; private set; }

    /// <summary>Стык между единицами внутри блока (null — врезок внутри блока нет).</summary>
    public Guid? JunctionBetweenId { get; private set; }

    /// <summary>Стык в конце блока (null — берётся стык шаблона по умолчанию).</summary>
    public Guid? JunctionAfterId { get; private set; }

    public const int DefaultMaxDriftMinutes = 5;

    private Slot() { }

    public static Slot Create(
        Guid layerId,
        string title,
        TimeOnly targetStart,
        int targetDurationMinutes,
        Daypart daypart = Daypart.Day,
        SlotKind slotKind = SlotKind.Content,
        int? weekday = null
    ) =>
        new()
        {
            Id = Guid.NewGuid(),
            LayerId = layerId,
            Title = title.Trim(),
            TargetStart = targetStart,
            TargetDurationMinutes = Math.Max(1, targetDurationMinutes),
            Daypart = daypart,
            SlotKind = slotKind,
            Weekday = weekday,
            BlockMode = SlotBlockMode.FillSlot,
            BlockValue = 1,
            OverflowPolicy = OverflowPolicy.ContinueNext,
            IsAnchor = false,
            MaxDriftMinutes = DefaultMaxDriftMinutes,
        };

    /// <summary>Правит расписание слота: когда, сколько и как выравнивать.</summary>
    public void UpdateTiming(
        int? weekday,
        TimeOnly targetStart,
        int targetDurationMinutes,
        Daypart daypart,
        bool isAnchor,
        int maxDriftMinutes,
        int? snapToMinutes
    )
    {
        Weekday = weekday is >= 0 and <= 6 ? weekday : null;
        TargetStart = targetStart;
        TargetDurationMinutes = Math.Max(1, targetDurationMinutes);
        Daypart = daypart;
        IsAnchor = isAnchor;
        MaxDriftMinutes = Math.Max(0, maxDriftMinutes);
        SnapToMinutes = snapToMinutes is > 0 ? snapToMinutes : null;
    }

    /// <summary>Правит наполнение слота: чем, в каком объёме и с какими врезками.</summary>
    public void UpdateContent(SlotContent content)
    {
        JunctionBetweenId = content.JunctionBetweenId;
        JunctionAfterId = content.JunctionAfterId;
        Title = content.Title.Trim();
        SlotKind = content.SlotKind;
        BlockMode = content.BlockMode;
        BlockValue = Math.Max(1, content.BlockValue);
        OverflowPolicy = content.OverflowPolicy;

        // Поля, не относящиеся к типу слота, гасим: повтор и конец вещания стратегии не имеют,
        // и оставленный от прежнего типа мусор потом читался бы генератором как настройка.
        GroupId = content.SlotKind == SlotKind.Content ? content.GroupId : null;
        StrategyJson = content.SlotKind == SlotKind.Content ? content.StrategyJson : null;
        RepeatSourceJson = content.SlotKind == SlotKind.Repeat ? content.RepeatSourceJson : null;
    }

    /// <summary>Конец слота в сутках канала. Может выйти за полночь — вещательные сутки длиннее суток.</summary>
    public TimeSpan TargetEndOffset =>
        TargetStart.ToTimeSpan() + TimeSpan.FromMinutes(TargetDurationMinutes);
}
