using LiteCqrs; using Microsoft.EntityFrameworkCore; using TeleWave.Application.Broadcast; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Common.Models; using TeleWave.Application.Programming.Groups; using TeleWave.Domain.Programming; namespace TeleWave.Application.Programming.Templates.Transfer; public sealed class ExportGridQueryHandler( IAppDbContext dbContext, DynamicGroupResolver dynamicResolver ) : IQueryHandler> { public async Task> Handle( ExportGridQuery query, CancellationToken cancellationToken ) { var channel = await dbContext .Channels.AsNoTracking() .FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken); if (channel is null) return Result.Failure(ChannelErrors.NotFound); var template = await dbContext .ScheduleTemplates.AsNoTracking() .Include(t => t.Layers) .ThenInclude(l => l.Slots) .AsSplitQuery() .FirstOrDefaultAsync(t => t.ChannelId == query.ChannelId, cancellationToken); if (template is null) return Result.Failure(ChannelErrors.TemplateNotFound); var groups = await dbContext .Groups.AsNoTracking() .ToDictionaryAsync(g => g.Id, g => g.Name, cancellationToken); var junctions = await dbContext .JunctionTemplates.AsNoTracking() .ToDictionaryAsync(j => j.Id, j => j.Name, cancellationToken); var layers = template .Layers.OrderBy(l => l.Priority) .Select(layer => new GridConfigLayer( layer.Name, layer.Priority, [ .. layer .Slots.OrderBy(s => s.Weekday ?? -1) .ThenBy(s => s.TargetStart) .Select(slot => ToConfig(slot, groups, junctions)), ], layer.IsEnabled, layer.IsBackground, LayerApplicability.FromJson(layer.ApplicabilityJson) )) .ToList(); // Состав групп едет вместе с сеткой: на другой установке тех же групп нет, а слот без // группы поставить некуда. Собираем только использованные — выгружать всю библиотеку // ради одного канала незачем. var used = layers .SelectMany(l => l.Slots) .Select(s => s.Group) .Append(Name(template.FallbackGroupId, groups)) .OfType() .ToHashSet(StringComparer.OrdinalIgnoreCase); var (exportedGroups, exportedCollections) = await ExportGroupsAsync( used, cancellationToken ); return Result.Success( new GridConfig( layers, Channel: new GridConfigChannel( channel.Name, channel.DayStartTime, channel.UtcOffsetMinutes ), Rules: PlanningRules.FromJson(template.RulesJson), FallbackGroup: Name(template.FallbackGroupId, groups), DefaultJunction: Name(template.DefaultJunctionId, junctions), Collections: exportedCollections, Groups: exportedGroups ) ); } /// /// Состав использованных групп и коллекций именами. Динамическая группа выгружается снимком /// вычисленного состава, а не правилом: правило ссылается на жанры и рейтинги этой установки, /// и на чужой переехало бы бессмыслицей. /// private async Task<( List Groups, List Collections )> ExportGroupsAsync(IReadOnlySet used, CancellationToken cancellationToken) { var groups = await dbContext .Groups.AsNoTracking() .Include(g => g.Items) .ToListAsync(cancellationToken); var showNames = await dbContext .Shows.AsNoTracking() .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken); var collectionNames = await dbContext .Collections.AsNoTracking() .ToDictionaryAsync(c => c.Id, c => c.Name, cancellationToken); var exported = new List(); var collectionIds = new HashSet(); foreach (var group in groups.Where(g => used.Contains(g.Name.Trim()))) { var composition = await dynamicResolver.ResolveAsync(group, cancellationToken); var shows = composition .Where(e => e.Kind == GroupElementKind.Show) .Select(e => showNames.GetValueOrDefault(e.Id)) .OfType() .ToList(); var collections = new List(); foreach (var element in composition.Where(e => e.Kind == GroupElementKind.Collection)) { if (collectionNames.GetValueOrDefault(element.Id) is not { } name) continue; collections.Add(name); collectionIds.Add(element.Id); } exported.Add( new GridConfigGroup( group.Name, shows.Count > 0 ? shows : null, collections.Count > 0 ? collections : null, group.Description ) ); } var items = await dbContext .CollectionItems.AsNoTracking() .Where(i => collectionIds.Contains(i.CollectionId)) .OrderBy(i => i.Position) .ToListAsync(cancellationToken); var exportedCollections = await dbContext .Collections.AsNoTracking() .Where(c => collectionIds.Contains(c.Id)) .ToListAsync(cancellationToken); return ( exported, [ .. exportedCollections.Select(collection => new GridConfigCollection( collection.Name, [ .. items .Where(i => i.CollectionId == collection.Id) .Select(i => showNames.GetValueOrDefault(i.ShowId)) .OfType(), ], collection.Description )), ] ); } private static GridConfigSlot ToConfig( Slot slot, IReadOnlyDictionary groups, IReadOnlyDictionary junctions ) => new( slot.Title, slot.TargetStart, slot.TargetDurationMinutes, slot.Daypart, slot.SlotKind, slot.Weekday, Name(slot.GroupId, groups), SlotStrategy.FromJson(slot.StrategyJson), RepeatSource.FromJson(slot.RepeatSourceJson), slot.BlockMode, slot.BlockValue, slot.OverflowPolicy, slot.IsAnchor, slot.MaxDriftMinutes, slot.SnapToMinutes, Name(slot.JunctionBetweenId, junctions), Name(slot.JunctionAfterId, junctions) ); /// Имя по ссылке. Пропавшая ссылка выгружается пустой — файл важнее её сохранности. private static string? Name(Guid? id, IReadOnlyDictionary names) => id is { } value ? names.GetValueOrDefault(value) : null; }