using LiteCqrs; using Microsoft.EntityFrameworkCore; using TeleWave.Application.Broadcast; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Common.Models; namespace TeleWave.Application.Programming.Templates.Reset; /// /// Сброс сетки и собранного по ней эфира — «начать канал заново», не пересоздавая его. /// /// Прошлое и идущая сейчас запись не трогаются: инвариант общий с генератором — у зрителя нельзя /// вырезать программу из-под носа, а история показов нужна остыванию и потолку повторов. /// Курсоры слотов уходят каскадом вместе со слотами, поэтому сериалы после сброса начинаются /// с начала — это и просят словом «полностью». /// /// Снимок последнего применения остаётся: он единственная точка отката, и после сброса «Откатить» /// вернёт сетку. Ленту это не вернёт — её пересоберёт применение. /// public sealed class ResetTemplateCommandHandler(IAppDbContext dbContext) : ICommandHandler> { public async Task> Handle( ResetTemplateCommand command, CancellationToken cancellationToken ) { var template = await dbContext .ScheduleTemplates.Include(t => t.Layers) .ThenInclude(l => l.Slots) .AsSplitQuery() .FirstOrDefaultAsync(t => t.ChannelId == command.ChannelId, cancellationToken); if (template is null) return Result.Failure(ChannelErrors.TemplateNotFound); var now = DateTimeOffset.UtcNow; // Удаление ленты идёт мимо change-tracker (ExecuteDelete), снятие слотов — через него: // связывает их в одну операцию только явная транзакция. Лок канала — тот же, что берёт // генератор: фоновый тик не должен дописывать хвост в момент сброса. await using var transaction = await dbContext.BeginTransactionAsync(cancellationToken); await dbContext.AcquireChannelLockAsync(command.ChannelId, cancellationToken); var entries = await dbContext .ScheduleEntries.Where(e => e.ChannelId == command.ChannelId && e.StartsAtUtc >= now) .ExecuteDeleteAsync(cancellationToken); var slots = 0; var layers = 0; foreach (var layer in template.Layers.ToList()) { slots += layer.Slots.Count; // Нефоновый слой уходит целиком — слоты за ним каскадом. Фоновый удалить нельзя, // поэтому его чистим послотно. if (template.RemoveLayer(layer.Id)) { layers++; continue; } foreach (var slot in layer.Slots.ToList()) layer.RemoveSlot(slot.Id); } template.MarkChanged(); await dbContext.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return Result.Success(new ResetTemplateResultDto(slots, layers, entries)); } }