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.CopyTemplate;

public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
    : ICommandHandler<CopyTemplateCommand, Result<CopyTemplateResultDto>>
{
    public async Task<Result<CopyTemplateResultDto>> Handle(
        CopyTemplateCommand command,
        CancellationToken cancellationToken
    )
    {
        var target = await dbContext
            .Channels.Include(c => c.BumperTemplates)
            .FirstOrDefaultAsync(c => c.Id == command.TargetChannelId, cancellationToken);
        if (target is null)
            return Result.Failure<CopyTemplateResultDto>(ChannelErrors.NotFound);

        var source = await dbContext
            .ScheduleTemplates.AsNoTracking()
            .Include(t => t.Layers)
                .ThenInclude(l => l.Slots)
            .AsSplitQuery()
            .FirstOrDefaultAsync(t => t.ChannelId == command.SourceChannelId, cancellationToken);
        if (source is null)
            return Result.Failure<CopyTemplateResultDto>(ChannelErrors.TemplateNotFound);

        var sourceJunctions = await dbContext
            .JunctionTemplates.AsNoTracking()
            .Include(j => j.Elements)
            .Where(j => j.ChannelId == command.SourceChannelId)
            .ToListAsync(cancellationToken);

        // Заставки живут на канале и на диске, поэтому не копируются: врезка ищет блок с таким же
        // именем у приёмника, а не найдя — остаётся без ссылки, и это возвращается в отчёте.
        var bumperByName = target
            .BumperTemplates.GroupBy(t => t.Name)
            .ToDictionary(g => g.Key, g => g.First().Id);
        var sourceBumperNames = await dbContext
            .Channels.AsNoTracking()
            .Where(c => c.Id == command.SourceChannelId)
            .SelectMany(c => c.BumperTemplates)
            .Select(t => new { t.Id, t.Name })
            .ToDictionaryAsync(t => t.Id, t => t.Name, cancellationToken);

        var (junctionMap, droppedBumperRefs) = CopyJunctions(
            sourceJunctions,
            target.Id,
            sourceBumperNames,
            bumperByName
        );

        // Прежняя сетка приёмника заменяется целиком: слить две сетки автоматически нельзя,
        // а «добавить поверх» дало бы кашу из пересекающихся слотов.
        var existing = await dbContext
            .ScheduleTemplates.Where(t => t.ChannelId == target.Id)
            .ToListAsync(cancellationToken);
        dbContext.ScheduleTemplates.RemoveRange(existing);
        await dbContext
            .JunctionTemplates.Where(j =>
                j.ChannelId == target.Id && !junctionMap.Values.Contains(j.Id)
            )
            .ExecuteDeleteAsync(cancellationToken);

        var copyTemplate = ScheduleTemplate.Create(target.Id, source.Name);
        copyTemplate.SetFallbackGroup(source.FallbackGroupId);
        copyTemplate.SetRules(source.RulesJson);
        if (
            source.DefaultJunctionId is { } defaultJunction
            && junctionMap.TryGetValue(defaultJunction, out var mappedDefault)
        )
            copyTemplate.SetDefaultJunction(mappedDefault);

        var (layers, slots) = CopyGrid(source, copyTemplate, junctionMap);

        dbContext.ScheduleTemplates.Add(copyTemplate);
        target.SetTemplate(copyTemplate.Id);

        return Result.Success(
            new CopyTemplateResultDto(layers, slots, junctionMap.Count, droppedBumperRefs)
        );
    }

    /// <summary>
    /// Копирует стыки на канал-приёмник. Возвращает соответствие «стык источника → копия» (по нему
    /// потом перевешиваются ссылки слотов) и число врезок, потерявших ссылку на блок заставки.
    /// </summary>
    private (Dictionary<Guid, Guid> Map, int DroppedBumperRefs) CopyJunctions(
        IReadOnlyList<JunctionTemplate> sourceJunctions,
        Guid targetChannelId,
        IReadOnlyDictionary<Guid, string> sourceBumperNames,
        IReadOnlyDictionary<string, Guid> targetBumperByName
    )
    {
        var map = new Dictionary<Guid, Guid>();
        var dropped = 0;

        foreach (var junction in sourceJunctions)
        {
            var copy = JunctionTemplate.Create(targetChannelId, junction.Name);
            map[junction.Id] = copy.Id;

            foreach (var element in junction.Elements.OrderBy(e => e.Position))
            {
                var bumperTemplateId = MapBumper(
                    element,
                    sourceBumperNames,
                    targetBumperByName,
                    ref dropped
                );
                copy.AddElement(element.Kind)
                    .Update(
                        element.Kind,
                        element.GroupId,
                        bumperTemplateId,
                        element.AmountMode,
                        element.AmountValue,
                        element.IsRequired,
                        element.ConditionsJson
                    );
            }

            dbContext.JunctionTemplates.Add(copy);
        }

        return (map, dropped);
    }

    /// <summary>
    /// Блок заставки у приёмника, соответствующий блоку источника по имени. Заставки живут на канале
    /// и на диске, поэтому не копируются: не нашлось одноимённого — врезка остаётся без ссылки,
    /// и это попадает в отчёт.
    /// </summary>
    private static Guid? MapBumper(
        JunctionElement element,
        IReadOnlyDictionary<Guid, string> sourceBumperNames,
        IReadOnlyDictionary<string, Guid> targetBumperByName,
        ref int dropped
    )
    {
        if (element.Kind != JunctionElementKind.Bumper)
            return null;

        if (
            element.BumperTemplateId is { } sourceId
            && sourceBumperNames.TryGetValue(sourceId, out var name)
            && targetBumperByName.TryGetValue(name, out var mapped)
        )
            return mapped;

        dropped++;
        return null;
    }

    /// <summary>Переносит слои со слотами. Возвращает, сколько слоёв (кроме фонового) и слотов скопировано.</summary>
    private static (int Layers, int Slots) CopyGrid(
        ScheduleTemplate source,
        ScheduleTemplate copyTemplate,
        IReadOnlyDictionary<Guid, Guid> junctionMap
    )
    {
        var layers = 0;
        var slots = 0;

        foreach (var layer in source.Layers.OrderByDescending(l => l.Priority))
        {
            // Фоновый слой у нового шаблона уже есть — в него переносим слоты, а не заводим второй.
            var copyLayer = layer.IsBackground
                ? copyTemplate.Background!
                : copyTemplate.AddLayer(layer.Name, layer.Priority);
            copyLayer.Update(layer.Name, layer.Priority, layer.ApplicabilityJson, layer.IsEnabled);
            if (!layer.IsBackground)
                layers++;

            foreach (var slot in layer.Slots)
            {
                CopySlot(slot, copyLayer, junctionMap);
                slots++;
            }
        }

        return (layers, slots);
    }

    private static void CopySlot(
        Slot slot,
        GridLayer copyLayer,
        IReadOnlyDictionary<Guid, Guid> junctionMap
    )
    {
        var copySlot = copyLayer.AddSlot(
            slot.Title,
            slot.TargetStart,
            slot.TargetDurationMinutes,
            slot.Daypart,
            slot.SlotKind,
            slot.Weekday
        );
        copySlot.UpdateTiming(
            slot.Weekday,
            slot.TargetStart,
            slot.TargetDurationMinutes,
            slot.Daypart,
            slot.IsAnchor,
            slot.MaxDriftMinutes,
            slot.SnapToMinutes
        );
        copySlot.UpdateContent(
            new SlotContent(
                slot.Title,
                slot.SlotKind,
                slot.GroupId,
                slot.StrategyJson,
                slot.RepeatSourceJson,
                slot.BlockMode,
                slot.BlockValue,
                slot.OverflowPolicy,
                Map(slot.JunctionBetweenId, junctionMap),
                Map(slot.JunctionAfterId, junctionMap)
            )
        );
    }

    private static Guid? Map(Guid? id, IReadOnlyDictionary<Guid, Guid> map) =>
        id is { } value && map.TryGetValue(value, out var mapped) ? mapped : null;
}
