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);
}