namespace TeleWave.Domain.Programming; /// /// Шаблон сетки канала: слои со слотами плюс аварийные настройки. Один активный шаблон на канал — /// сезонность выражается слоями внутри него, а не вторым шаблоном, иначе получились бы два механизма /// для одного и того же. На другой канал переносится глубокой копией. /// /// растёт при любой правке правил. Эфир при этом не меняется: правка помечает /// шаблон изменённым, а хвост пересобирается отдельной командой применения. /// public class ScheduleTemplate { private readonly List _layers = []; public Guid Id { get; private set; } public Guid ChannelId { get; private set; } public string Name { get; private set; } = string.Empty; /// Аварийная группа, если пуст даже фоновый слой. public Guid? FallbackGroupId { get; private set; } /// Стык по умолчанию — для слотов, у которых свой не задан. public Guid? DefaultJunctionId { get; private set; } /// /// Правила отбора кандидатов (детское время, потолок повторов) в JSON. Домен их не разбирает — /// схема живёт в Application, как и у стратегий слотов и условий стыка. /// public string? RulesJson { get; private set; } /// Номер правки правил; входит в кэш-ключи и историю. public int Revision { get; private set; } /// Ревизия, по которой собрано текущее будущее расписание. public int AppliedRevision { get; private set; } public DateTimeOffset CreatedAt { get; private set; } public IReadOnlyList Layers => _layers; /// Есть ли правки, не применённые к эфиру. 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; /// Отметить, что правила изменились — эфир пойдёт по старым до применения. public void MarkChanged() => Revision++; /// Отметить, что хвост пересобран по текущей ревизии. public void MarkApplied() => AppliedRevision = Revision; 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; } /// Удаляет слой. Фоновый удалить нельзя — вернёт false. 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; } /// Слой, содержащий слот, или null. public GridLayer? FindLayerOfSlot(Guid slotId) => _layers.FirstOrDefault(l => l.FindSlot(slotId) is not null); }