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

namespace TeleWave.Application.Library.DeleteShow;

public sealed class DeleteShowCommandHandler(
    IAppDbContext dbContext,
    GroupMembershipCleaner groupCleaner,
    ShowMediaEraser eraser
) : ICommandHandler<DeleteShowCommand, Result>
{
    public async Task<Result> Handle(DeleteShowCommand command, CancellationToken cancellationToken)
    {
        var show = await dbContext
            .Shows.Include(s => s.Episodes)
            .FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
        if (show is null)
            return Result.Failure(ShowErrors.NotFound);

        // Уже сгенерированный эфир ссылается на шоу без внешнего ключа: удали его — и зритель
        // получит запись без названия, а раздача упрётся в удалённые каскадом серии. Прошедшие
        // записи не держим: они лишь теряют название в EPG, а иначе шоу было бы не удалить никогда.
        if (
            !command.WithSchedule
            && await OnAirAsync(command.ShowId, cancellationToken) is { Length: > 0 } channels
        )
            return Result.Failure(ShowErrors.InFutureSchedule(channels));

        // Позиции коллекций уходят каскадом БД, позиции групп — вручную: ссылка группы полиморфна.
        await groupCleaner.RemoveElementAsync(
            GroupElementKind.Show,
            command.ShowId,
            cancellationToken
        );

        if (!command.WithMedia && !command.WithSchedule)
        {
            dbContext.Shows.Remove(show);
            return Result.Success();
        }

        // Удаление записей и ассетов идёт мимо SaveChanges (ExecuteDelete), поэтому одной
        // транзакцией с удалением шоу их связывает только явная транзакция. Файлы — после коммита.
        await using var transaction = await dbContext.BeginTransactionAsync(cancellationToken);

        if (command.WithSchedule)
            await CutFromAirAsync(command.ShowId, cancellationToken);

        var erased = command.WithMedia ? await eraser.EraseAsync(show, cancellationToken) : [];

        dbContext.Shows.Remove(show);
        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);

        await eraser.DeleteFilesAsync(erased, cancellationToken);
        return Result.Success();
    }

    /// <summary>
    /// Вырезает шоу из будущего эфира всех каналов. Прошлое не трогаем: там запись уже отыграна,
    /// и её удаление только стёрло бы название в архиве EPG. Идущую сейчас — трогаем: пересборка
    /// её не берёт, а без этого шоу нельзя было бы удалить до конца текущей серии.
    /// </summary>
    private Task CutFromAirAsync(Guid showId, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;

        return dbContext
            .ScheduleEntries.Where(e => e.ShowId == showId && e.EndsAtUtc > now)
            .ExecuteDeleteAsync(cancellationToken);
    }

    /// <summary>
    /// Каналы, у которых шоу стоит в ещё не отыгранном эфире, — строкой «Первый (до 29.07 14:20)».
    /// Пусто — шоу свободно. Идущая сейчас запись тоже держит: пересборка её не трогает, и убрать
    /// шоу из-под зрителя нельзя.
    /// </summary>
    private async Task<string> OnAirAsync(Guid showId, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;

        var busy = await dbContext
            .ScheduleEntries.AsNoTracking()
            .Where(e => e.ShowId == showId && e.EndsAtUtc > now)
            .GroupBy(e => e.ChannelId)
            .Select(g => new { ChannelId = g.Key, Until = g.Max(e => e.EndsAtUtc) })
            .ToListAsync(cancellationToken);

        if (busy.Count == 0)
            return string.Empty;

        var channelIds = busy.Select(b => b.ChannelId).ToList();
        var names = await dbContext
            .Channels.AsNoTracking()
            .Where(c => channelIds.Contains(c.Id))
            .ToDictionaryAsync(c => c.Id, c => c.Name, cancellationToken);

        return string.Join(
            ", ",
            busy.Select(b =>
                $"{names.GetValueOrDefault(b.ChannelId, "канал")} — до {b.Until.ToLocalTime():dd.MM HH:mm}"
            )
        );
    }
}
