namespace TeleWave.Domain.Broadcast.Scheduling;
/// Шоу канала, подготовленное для планировщика: только готовые серии, с курсором.
public sealed record PlannerShow(
Guid ChannelShowId,
Guid ShowId,
int Weight,
BlockMode BlockMode,
int BlockValue,
IReadOnlyList EpisodeAssetIds,
int NextEpisodeIndex,
IReadOnlyList? PreferredHours = null,
int PreferredWeightMultiplier = 1
);
/// Окно предпочтительных часов [StartHour, EndHour) суток (UTC) для планировщика.
public sealed record PlannerHourWindow(int StartHour, int EndHour)
{
public bool Contains(int hour) => hour >= StartHour && hour < EndHour;
}
///
/// Override в терминах планировщика: режим + шоу с весами + правило действия (разовое окно либо
/// еженедельно по дню недели на окне минут суток UTC).
///
public sealed record PlannerOverride(
OverrideMode Mode,
IReadOnlyList Shows,
OverrideRecurrence Recurrence = OverrideRecurrence.OneTime,
DateTimeOffset? StartsAtUtc = null,
DateTimeOffset? EndsAtUtc = null,
int? DayOfWeek = null,
int? StartMinute = null,
int? EndMinute = null
)
{
/// Действует ли override в этот момент.
public bool Covers(DateTimeOffset moment)
{
if (Recurrence == OverrideRecurrence.Weekly)
{
if (
DayOfWeek is not { } day
|| StartMinute is not { } start
|| EndMinute is not { } end
)
return false;
var utc = moment.UtcDateTime;
var minuteOfDay = utc.Hour * 60 + utc.Minute;
var today = (int)utc.DayOfWeek;
if (end > start)
// Обычное окно в пределах одних суток [start, end).
return today == day && minuteOfDay >= start && minuteOfDay < end;
if (end < start)
{
// Окно пересекает полночь: [start, 24:00) в день `day` и [00:00, end) на следующий день.
var nextDay = (day + 1) % 7;
return (today == day && minuteOfDay >= start)
|| (today == nextDay && minuteOfDay < end);
}
return false; // end == start — пустое окно
}
return moment >= StartsAtUtc && moment < EndsAtUtc;
}
}
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
///
/// Политика ТВ-заставок на переходах. Планировщик из подходящих подблоков (,
/// фильтр по и контексту перехода) выбирает один по стратегии
/// и резервирует слот длины его блока. Ассет подставляет оркестратор.
/// / — вероятность самого появления
/// заставки на смене шоу / между блоками одного шоу (0..1).
///
public sealed record PlannerBumperConfig(
bool Enabled,
TimeSpan MinInterval,
BumperSelection Selection,
IReadOnlyList Variants,
double ShowChangeChance = 1.0,
double EpisodeChangeChance = 1.0
);
///
/// Подблок заставки в терминах планировщика: id варианта + id родительского блока (стиль/звук) +
/// длительность слота (кратна сегменту) + правило показа + вес (для ).
///
public sealed record PlannerBumperVariant(
Guid VariantId,
Guid TemplateId,
TimeSpan Duration,
BumperTrigger Trigger,
int Weight = 1
);
/// Полный вход планировщика для одного прогона по каналу.
public sealed record PlannerInput(
Guid ChannelId,
AdInsertion AdInsertion,
int AdsPerBreak,
int NextAdIndex,
IReadOnlyList Shows,
IReadOnlyList AdPool,
IReadOnlyDictionary Durations,
IReadOnlyList Overrides,
DateTimeOffset StartTime,
DateTimeOffset HorizonEnd,
PlannerBumperConfig? Bumpers = null,
int NextBumperIndex = 0
);
///
/// Одна запланированная запись (ещё не доменная сущность). Для заставок ( ==
/// ) пуст — его подставит
/// оркестратор после рендера по паре ( → ) и выбранному
/// блоку ().
///
public sealed record PlannedEntry(
Guid MediaAssetId,
ScheduleEntryKind Kind,
DateTimeOffset StartsAtUtc,
DateTimeOffset EndsAtUtc,
Guid? ShowId,
int? EpisodeIndex,
Guid? FromShowId = null,
Guid? ToShowId = null,
Guid? BumperTemplateId = null,
Guid? BumperVariantId = null
);
/// Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow, рекламы, заставок).
public sealed record PlannerResult(
IReadOnlyList Entries,
IReadOnlyDictionary NextEpisodeIndexByChannelShow,
int NextAdIndex,
int NextBumperIndex
);