namespace TeleWave.Domain.Programming;

/// <summary>
/// Шаблон сетки канала: слои со слотами плюс аварийные настройки. Один активный шаблон на канал —
/// сезонность выражается слоями внутри него, а не вторым шаблоном, иначе получились бы два механизма
/// для одного и того же. На другой канал переносится глубокой копией.
///
/// <see cref="Revision"/> растёт при любой правке правил. Эфир при этом не меняется: правка помечает
/// шаблон изменённым, а хвост пересобирается отдельной командой применения.
/// </summary>
public class ScheduleTemplate
{
    private readonly List<GridLayer> _layers = [];

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

    /// <summary>Аварийная группа, если пуст даже фоновый слой.</summary>
    public Guid? FallbackGroupId { get; private set; }

    /// <summary>Стык по умолчанию — для слотов, у которых свой не задан.</summary>
    public Guid? DefaultJunctionId { get; private set; }

    /// <summary>
    /// Правила отбора кандидатов (детское время, потолок повторов) в JSON. Домен их не разбирает —
    /// схема живёт в Application, как и у стратегий слотов и условий стыка.
    /// </summary>
    public string? RulesJson { get; private set; }

    /// <summary>Номер правки правил; входит в кэш-ключи и историю.</summary>
    public int Revision { get; private set; }

    /// <summary>Ревизия, по которой собрано текущее будущее расписание.</summary>
    public int AppliedRevision { get; private set; }

    /// <summary>
    /// Состояние шаблона на момент последнего применения (JSON) — то, по чему идёт эфир прямо
    /// сейчас. Нужно, чтобы правки можно было откатить: слои и слоты правятся на месте, и без
    /// снимка «как было» взять неоткуда. Домен его не разбирает — схема живёт в Application, как
    /// у правил и стратегий.
    /// </summary>
    public string? AppliedSnapshotJson { get; private set; }

    public DateTimeOffset CreatedAt { get; private set; }

    public IReadOnlyList<GridLayer> Layers => _layers;

    /// <summary>Есть ли правки, не применённые к эфиру.</summary>
    public bool HasPendingChanges => Revision != AppliedRevision;

    private const string BackgroundLayerName = "Фон";

    private ScheduleTemplate() { }

    public static ScheduleTemplate Create(Guid channelId, string name)
    {
        var template = new ScheduleTemplate
        {
            Id = Guid.NewGuid(),
            ChannelId = channelId,
            Name = name.Trim(),
            Revision = 0,
            AppliedRevision = 0,
            CreatedAt = DateTimeOffset.UtcNow,
        };
        // Фоновый слой заводится сразу: без него первая же дыра в сетке осталась бы нечем закрыть.
        template._layers.Add(
            GridLayer.Create(
                template.Id,
                BackgroundLayerName,
                GridLayer.BackgroundPriority,
                isBackground: true
            )
        );
        return template;
    }

    public void Rename(string name) => Name = name.Trim();

    public void SetFallbackGroup(Guid? groupId) => FallbackGroupId = groupId;

    public void SetDefaultJunction(Guid? junctionId) => DefaultJunctionId = junctionId;

    public void SetRules(string? rulesJson) =>
        RulesJson = string.IsNullOrWhiteSpace(rulesJson) ? null : rulesJson;

    /// <summary>Отметить, что правила изменились — эфир пойдёт по старым до применения.</summary>
    public void MarkChanged() => Revision++;

    /// <summary>
    /// Отметить, что хвост пересобран по текущей ревизии. Снимок — состояние, по которому собран
    /// эфир; он же станет точкой отката для последующих правок.
    /// </summary>
    public void MarkApplied(string? snapshotJson = null)
    {
        AppliedRevision = Revision;
        if (!string.IsNullOrWhiteSpace(snapshotJson))
            AppliedSnapshotJson = snapshotJson;
    }

    public GridLayer? FindLayer(Guid layerId) => _layers.FirstOrDefault(l => l.Id == layerId);

    public GridLayer? Background => _layers.FirstOrDefault(l => l.IsBackground);

    public GridLayer AddLayer(string name, int priority)
    {
        var layer = GridLayer.Create(Id, name, priority);
        _layers.Add(layer);
        return layer;
    }

    /// <summary>Удаляет слой. Фоновый удалить нельзя — вернёт false.</summary>
    public bool RemoveLayer(Guid layerId)
    {
        var layer = _layers.FirstOrDefault(l => l.Id == layerId);
        if (layer is null || layer.IsBackground)
            return false;
        _layers.Remove(layer);
        return true;
    }

    /// <summary>Слой, содержащий слот, или null.</summary>
    public GridLayer? FindLayerOfSlot(Guid slotId) =>
        _layers.FirstOrDefault(l => l.FindSlot(slotId) is not null);
}
