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<ExportGridQuery, Result<GridConfig>>
{
    public async Task<Result<GridConfig>> 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<GridConfig>(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<GridConfig>(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<string>()
            .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
            )
        );
    }

    /// <summary>
    /// Состав использованных групп и коллекций именами. Динамическая группа выгружается снимком
    /// вычисленного состава, а не правилом: правило ссылается на жанры и рейтинги этой установки,
    /// и на чужой переехало бы бессмыслицей.
    /// </summary>
    private async Task<(
        List<GridConfigGroup> Groups,
        List<GridConfigCollection> Collections
    )> ExportGroupsAsync(IReadOnlySet<string> 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<GridConfigGroup>();
        var collectionIds = new HashSet<Guid>();

        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<string>()
                .ToList();

            var inGroup = composition
                .Where(e =>
                    e.Kind == GroupElementKind.Collection && collectionNames.ContainsKey(e.Id)
                )
                .Select(e => e.Id)
                .ToList();

            // Идентификаторы копим отдельно: по ним ниже выгружается состав самих коллекций.
            collectionIds.UnionWith(inGroup);
            var collections = inGroup.Select(id => collectionNames[id]).ToList();

            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<string>(),
                    ],
                    collection.Description
                )),
            ]
        );
    }

    private static GridConfigSlot ToConfig(
        Slot slot,
        IReadOnlyDictionary<Guid, string> groups,
        IReadOnlyDictionary<Guid, string> 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)
        );

    /// <summary>Имя по ссылке. Пропавшая ссылка выгружается пустой — файл важнее её сохранности.</summary>
    private static string? Name(Guid? id, IReadOnlyDictionary<Guid, string> names) =>
        id is { } value ? names.GetValueOrDefault(value) : null;
}
