using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Programming;

namespace TeleWave.Application.Programming.Templates.Restore;

/// <summary>
/// Возвращает шаблон к снимку последнего применения. Слои и слоты правятся на месте по
/// идентификаторам из снимка: у пережившего откат слота сохраняется состояние (курсор сериала),
/// и после отката сериал не начинается заново. Слот, который успели удалить, вернётся новым —
/// его состояние ушло вместе с ним, и сделать с этим ничего нельзя.
///
/// Эфир не трогается: он и так идёт по этому состоянию, а шаблон после отката снова ему равен.
/// </summary>
public sealed class RestoreTemplateCommandHandler(IAppDbContext dbContext)
    : ICommandHandler<RestoreTemplateCommand, Result<RestoreTemplateResultDto>>
{
    public async Task<Result<RestoreTemplateResultDto>> Handle(
        RestoreTemplateCommand 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<RestoreTemplateResultDto>(ChannelErrors.TemplateNotFound);

        if (TemplateSnapshot.FromJson(template.AppliedSnapshotJson) is not { } snapshot)
            return Result.Failure<RestoreTemplateResultDto>(TemplateErrors.NothingToRestore);

        var refs = await LoadAliveRefsAsync(snapshot, cancellationToken);

        // Слои, которых в снимке нет, — заведены после применения. Фоновый не трогаем: он есть
        // в любом шаблоне и удалению не подлежит.
        var staleLayers = template
            .Layers.Where(layer =>
                !layer.IsBackground && snapshot.Layers.All(l => l.Id != layer.Id)
            )
            .Select(layer => layer.Id)
            .ToList();
        foreach (var layerId in staleLayers)
            template.RemoveLayer(layerId);

        foreach (var layerSnapshot in snapshot.Layers)
            RestoreLayer(template, layerSnapshot, refs);

        template.Rename(snapshot.Name);
        template.SetFallbackGroup(refs.GroupOrNull(snapshot.FallbackGroupId));
        template.SetDefaultJunction(refs.JunctionOrNull(snapshot.DefaultJunctionId));
        template.SetRules(snapshot.RulesJson);

        // Шаблон снова равен эфирному состоянию — отметка «изменено» гаснет, снимок остаётся тем же.
        template.MarkApplied();

        return Result.Success(
            new RestoreTemplateResultDto(template.Layers.Sum(l => l.Slots.Count), refs.Dropped)
        );
    }

    private static void RestoreLayer(
        ScheduleTemplate template,
        LayerSnapshot snapshot,
        AliveRefs refs
    )
    {
        var layer =
            template.FindLayer(snapshot.Id)
            ?? (snapshot.IsBackground ? template.Background : null)
            ?? template.AddLayer(snapshot.Name, snapshot.Priority);

        layer.Update(
            snapshot.Name,
            snapshot.Priority,
            snapshot.ApplicabilityJson,
            snapshot.IsEnabled
        );

        var staleSlots = layer
            .Slots.Where(slot => snapshot.Slots.All(s => s.Id != slot.Id))
            .Select(slot => slot.Id)
            .ToList();
        foreach (var slotId in staleSlots)
            layer.RemoveSlot(slotId);

        foreach (var slotSnapshot in snapshot.Slots)
            RestoreSlot(layer, slotSnapshot, refs);
    }

    private static void RestoreSlot(GridLayer layer, SlotSnapshot snapshot, AliveRefs refs)
    {
        var slot =
            layer.FindSlot(snapshot.Id)
            ?? layer.AddSlot(
                snapshot.Title,
                snapshot.TargetStart,
                snapshot.TargetDurationMinutes,
                snapshot.Daypart,
                snapshot.SlotKind,
                snapshot.Weekday
            );

        slot.UpdateTiming(
            snapshot.Weekday,
            snapshot.TargetStart,
            snapshot.TargetDurationMinutes,
            snapshot.Daypart,
            snapshot.IsAnchor,
            snapshot.MaxDriftMinutes,
            snapshot.SnapToMinutes
        );
        slot.UpdateContent(
            new SlotContent(
                snapshot.Title,
                snapshot.SlotKind,
                refs.GroupOrNull(snapshot.GroupId),
                snapshot.StrategyJson,
                snapshot.RepeatSourceJson,
                snapshot.BlockMode,
                snapshot.BlockValue,
                snapshot.OverflowPolicy,
                refs.JunctionOrNull(snapshot.JunctionBetweenId),
                refs.JunctionOrNull(snapshot.JunctionAfterId)
            )
        );
    }

    /// <summary>
    /// Какие группы и стыки из снимка ещё существуют. Ссылку на удалённое восстанавливать нельзя —
    /// внешний ключ не даст сохранить, и откат упал бы целиком из-за одной прибранной группы.
    /// </summary>
    private async Task<AliveRefs> LoadAliveRefsAsync(
        TemplateSnapshot snapshot,
        CancellationToken cancellationToken
    )
    {
        var slots = snapshot.Layers.SelectMany(l => l.Slots).ToList();

        var groupIds = slots
            .Select(s => s.GroupId)
            .Append(snapshot.FallbackGroupId)
            .OfType<Guid>()
            .Distinct()
            .ToList();
        var junctionIds = slots
            .SelectMany(s => new[] { s.JunctionBetweenId, s.JunctionAfterId })
            .Append(snapshot.DefaultJunctionId)
            .OfType<Guid>()
            .Distinct()
            .ToList();

        var groups = await dbContext
            .Groups.AsNoTracking()
            .Where(g => groupIds.Contains(g.Id))
            .Select(g => g.Id)
            .ToListAsync(cancellationToken);
        var junctions = await dbContext
            .JunctionTemplates.AsNoTracking()
            .Where(j => junctionIds.Contains(j.Id))
            .Select(j => j.Id)
            .ToListAsync(cancellationToken);

        return new AliveRefs([.. groups], [.. junctions]);
    }

    private sealed class AliveRefs(HashSet<Guid> groups, HashSet<Guid> junctions)
    {
        public int Dropped { get; private set; }

        public Guid? GroupOrNull(Guid? id) => Keep(id, groups);

        public Guid? JunctionOrNull(Guid? id) => Keep(id, junctions);

        private Guid? Keep(Guid? id, HashSet<Guid> alive)
        {
            if (id is not { } value)
                return null;
            if (alive.Contains(value))
                return value;

            Dropped++;
            return null;
        }
    }
}
