namespace TeleWave.Domain.Programming;
///
/// Группа контента — «что может попасть в эфир». Слот планировщика ссылается на группу, стратегия
/// выбирает из неё элемент. Группы общие для всех каналов и переиспользуются.
///
/// Состав — всегда явный список (), а — лишь правило
/// быстрого набора: «применить фильтр» добавляет найденное в список, дальше состав правится руками.
/// Живой запрос при генерации был бы проще в коде, но тогда добавление одного шоу в библиотеку
/// перетасовывало бы всё будущее расписание всех каналов.
///
public class Group
{
private readonly List _items = new();
public Guid Id { get; private set; }
public string Name { get; private set; } = string.Empty;
public string? Description { get; private set; }
/// Правило набора (JSON) или null. Домен его не интерпретирует — схема живёт в Application.
public string? FilterJson { get; private set; }
// ── Кэш статистики: считается при правке состава и фоново, нужен UI («342 позиции · 118 ч») ──
/// Число позиций в группе.
public int ItemCount { get; private set; }
/// Число единиц воспроизведения: у сериала и коллекции их больше одной.
public int UnitCount { get; private set; }
/// Суммарная длительность готовых единиц.
public TimeSpan TotalDuration { get; private set; }
public DateTimeOffset? StatsComputedAt { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
/// Позиции группы (backing-field для EF); порядок — по .
public IReadOnlyList Items => _items;
private Group() { }
public static Group Create(string name, string? description = null) =>
new()
{
Id = Guid.NewGuid(),
Name = name.Trim(),
Description = description,
CreatedAt = DateTimeOffset.UtcNow,
};
public void Rename(string name, string? description)
{
Name = name.Trim();
Description = description;
}
/// Сохранить правило набора (null — снять). Состав при этом не меняется.
public void SetFilter(string? filterJson) =>
FilterJson = string.IsNullOrWhiteSpace(filterJson) ? null : filterJson;
/// Записать пересчитанную статистику.
public void UpdateStats(int itemCount, int unitCount, TimeSpan totalDuration, DateTimeOffset at)
{
ItemCount = itemCount;
UnitCount = unitCount;
TotalDuration = totalDuration;
StatsComputedAt = at;
}
public bool Contains(GroupElementKind kind, Guid elementId) =>
_items.Any(i => i.ElementKind == kind && i.ElementId == elementId);
/// Добавляет элемент в конец. Повторное добавление игнорируется — возвращает null.
public GroupItem? AddElement(GroupElementKind kind, Guid elementId)
{
if (Contains(kind, elementId))
return null;
var nextPosition = _items.Count == 0 ? 0 : _items.Max(i => i.Position) + 1;
var item = GroupItem.Create(Id, kind, elementId, nextPosition);
_items.Add(item);
return item;
}
public bool RemoveItem(Guid itemId)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item is null)
return false;
_items.Remove(item);
return true;
}
/// Убирает все позиции, ссылающиеся на элемент. Вызывается при удалении шоу/коллекции
/// из библиотеки — внешнего ключа на полиморфную ссылку нет. Возвращает число снятых позиций.
public int RemoveElement(GroupElementKind kind, Guid elementId) =>
_items.RemoveAll(i => i.ElementKind == kind && i.ElementId == elementId);
public bool SetWeight(Guid itemId, int weight)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item is null)
return false;
item.SetWeight(weight);
return true;
}
///
/// Переставляет позиции в порядке переданных идентификаторов. Не упомянутые остаются после них,
/// сохраняя относительный порядок, а неизвестные игнорируются — так перетаскивание одного
/// элемента не теряет остальные.
///
public void Reorder(IEnumerable itemIdsInOrder)
{
var known = _items.Select(i => i.Id).ToHashSet();
var requested = itemIdsInOrder.Distinct().Where(known.Contains).ToList();
var rest = _items
.Where(i => !requested.Contains(i.Id))
.OrderBy(i => i.Position)
.Select(i => i.Id);
var position = 0;
foreach (var itemId in requested.Concat(rest))
_items.First(i => i.Id == itemId).SetPosition(position++);
}
}