namespace TeleWave.Domain.Programming;

/// <summary>
/// Слой сетки: набор слотов, действующий при выполнении условия применимости. Для каждого момента
/// активен слот из применимого слоя с наибольшим <see cref="Priority"/>.
///
/// Фоновый слой (<see cref="Priority"/> = 0) покрывает сутки целиком и заменяет собой отдельную
/// сущность «заполнитель»: любая дыра в расписании — просто место, где сверху ничего не легло.
/// </summary>
public class GridLayer
{
    private readonly List<Slot> _slots = [];

    public Guid Id { get; private set; }
    public Guid TemplateId { get; private set; }
    public string Name { get; private set; } = string.Empty;

    /// <summary>Больше — специфичнее, побеждает. Фоновый слой имеет 0.</summary>
    public int Priority { get; private set; }

    /// <summary>Когда слой действует (JSON). Домен его не интерпретирует — схема в Application.</summary>
    public string? ApplicabilityJson { get; private set; }

    public bool IsEnabled { get; private set; }

    /// <summary>Фоновый слой удалять нельзя — без него в сетке появляются дыры.</summary>
    public bool IsBackground { get; private set; }

    public IReadOnlyList<Slot> Slots => _slots;

    public const int BackgroundPriority = 0;

    private GridLayer() { }

    internal static GridLayer Create(
        Guid templateId,
        string name,
        int priority,
        bool isBackground = false
    ) =>
        new()
        {
            Id = Guid.NewGuid(),
            TemplateId = templateId,
            Name = name.Trim(),
            Priority = isBackground ? BackgroundPriority : Math.Max(1, priority),
            IsEnabled = true,
            IsBackground = isBackground,
        };

    public void Update(string name, int priority, string? applicabilityJson, bool isEnabled)
    {
        Name = name.Trim();
        // Приоритет фонового слоя не двигается: он обязан оставаться под всеми остальными.
        if (!IsBackground)
            Priority = Math.Max(1, priority);
        ApplicabilityJson = string.IsNullOrWhiteSpace(applicabilityJson) ? null : applicabilityJson;
        IsEnabled = isEnabled;
    }

    public Slot AddSlot(
        string title,
        TimeOnly targetStart,
        int targetDurationMinutes,
        Daypart daypart = Daypart.Day,
        SlotKind slotKind = SlotKind.Content,
        int? weekday = null
    )
    {
        var slot = Slot.Create(
            Id,
            title,
            targetStart,
            targetDurationMinutes,
            daypart,
            slotKind,
            weekday
        );
        _slots.Add(slot);
        return slot;
    }

    public Slot? FindSlot(Guid slotId) => _slots.FirstOrDefault(s => s.Id == slotId);

    public bool RemoveSlot(Guid slotId)
    {
        var slot = _slots.FirstOrDefault(s => s.Id == slotId);
        if (slot is null)
            return false;
        _slots.Remove(slot);
        return true;
    }

    /// <summary>
    /// Пересекается ли интервал с уже существующими слотами того же дня. Внутри одного слоя
    /// перекрытие запрещено: два слота на одну минуту сделали бы выбор недетерминированным.
    /// Слоты на разные дни недели не конфликтуют; слот без дня недели («каждый день») конфликтует
    /// со всеми.
    /// </summary>
    public bool HasOverlap(int? weekday, TimeOnly start, int durationMinutes, Guid? exceptSlotId)
    {
        var from = start.ToTimeSpan();
        var to = from + TimeSpan.FromMinutes(durationMinutes);

        return _slots.Any(other =>
            other.Id != exceptSlotId
            && (weekday is null || other.Weekday is null || other.Weekday == weekday)
            && from < other.TargetEndOffset
            && other.TargetStart.ToTimeSpan() < to
        );
    }
}
