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

/// <summary>
/// Создаёт слоты по плану. Слои заводятся по именам профиля и переиспользуются: повторная генерация
/// не должна плодить «Основная сетка (2)».
///
/// Эфир при этом не двигается — как и любая правка сетки, генерация только помечает шаблон
/// изменённым, а хвост пересобирает отдельная кнопка применения.
/// </summary>
public sealed class GenerateGridCommandHandler(
    IAppDbContext dbContext,
    GridPlanner planner,
    SlotWriter writer
) : ICommandHandler<GenerateGridCommand, Result<GenerateGridResultDto>>
{
    /// <summary>Приоритеты слоёв генератора: выходные обязаны перекрывать будни.</summary>
    private const int MainPriority = 10;
    private const int WeekendPriority = 20;

    /// <summary>Праздники перекрывают и будни, и выходные: 1 января — это не «обычное воскресенье».</summary>
    private const int SeasonPriority = 30;

    public async Task<Result<GenerateGridResultDto>> Handle(
        GenerateGridCommand command,
        CancellationToken cancellationToken
    )
    {
        var planned = await planner.BuildAsync(
            command.ChannelId,
            command.Profile,
            command.Mode,
            cancellationToken
        );
        if (!planned.IsSuccess)
            return Result.Failure<GenerateGridResultDto>(planned.Error);

        var plan = planned.Value;

        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<GenerateGridResultDto>(ChannelErrors.TemplateNotFound);

        var removed = command.Mode == GridGenerationMode.Rebuild ? RemoveAllSlots(template) : 0;

        var created = 0;
        foreach (var slot in plan.Slots)
        {
            var layer = EnsureLayer(template, slot.Layer, plan.Profile.Season);
            var applied = await writer.ApplyAsync(layer, null, ToInput(slot), cancellationToken);
            if (!applied.IsSuccess)
                return Result.Failure<GenerateGridResultDto>(applied.Error);
            created++;
        }

        // Аварийная группа — то, чем закрываются паузы между слотами. Без неё в ленте остались бы
        // дыры, поэтому генератор проставляет её, если админ ещё не выбрал свою.
        if (plan.FallbackGroupId is { } fallbackGroupId && template.FallbackGroupId is null)
            template.SetFallbackGroup(fallbackGroupId);

        template.MarkChanged();
        return Result.Success(new GenerateGridResultDto(created, removed));
    }

    /// <summary>Снимает все слоты шаблона и возвращает их число — режим «собрать с нуля».</summary>
    private static int RemoveAllSlots(ScheduleTemplate template)
    {
        var removed = 0;

        foreach (var layer in template.Layers)
        {
            // Идентификаторы снимаем заранее: удаление правит ту самую коллекцию, по которой идём.
            var slotIds = layer.Slots.Select(s => s.Id).ToList();
            foreach (var slotId in slotIds)
                layer.RemoveSlot(slotId);
            removed += slotIds.Count;
        }

        return removed;
    }

    /// <summary>
    /// Слой под слоты плана. Существующий переиспользуется по имени, новый — заводится; применимость
    /// проставляется только новому: даты праздничного слоя админ мог подправить под свой канал,
    /// и повторная генерация не должна их затирать.
    /// </summary>
    private static GridLayer EnsureLayer(
        ScheduleTemplate template,
        GridPlanLayer kind,
        GridSeason? season
    )
    {
        var (name, priority) = kind switch
        {
            GridPlanLayer.Weekend => (GridPlanner.WeekendLayerName, WeekendPriority),
            GridPlanLayer.Season => (season?.Name ?? "Праздники", SeasonPriority),
            _ => (GridPlanner.MainLayerName, MainPriority),
        };

        if (template.Layers.FirstOrDefault(l => !l.IsBackground && l.Name == name) is { } existing)
            return existing;

        var layer = template.AddLayer(name, priority);
        if (kind == GridPlanLayer.Season && season is not null)
            layer.Update(name, priority, Applicability(season).ToJson(), isEnabled: true);

        return layer;
    }

    /// <summary>Когда действует праздничный слой — ежегодный период профиля.</summary>
    private static LayerApplicability Applicability(GridSeason season) =>
        new(AnnualRanges: [season.Range]);

    private static SlotInput ToInput(PlannedSlot slot) =>
        new(
            slot.Title,
            slot.Weekday,
            slot.Start,
            slot.DurationMinutes,
            slot.Daypart,
            slot.SlotKind,
            slot.GroupId,
            slot.Strategy,
            slot.RepeatSource,
            slot.BlockMode,
            slot.BlockValue,
            OverflowPolicy.ContinueNext,
            slot.IsAnchor,
            Slot.DefaultMaxDriftMinutes,
            // Округление старта до четверти часа: программа передач не начинается в 19:47, а разницу
            // добирает рекламный шов. Мягкое — если добирать пришлось бы дольше допуска, пропустится.
            SnapMinutes
        );

    /// <summary>Шаг округления стартов у собранных слотов — та же решётка, что и у длин.</summary>
    private const int SnapMinutes = 15;
}
