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

namespace TeleWave.Application.Media.ManualInbox;

public sealed class ImportManualInboxCommandHandler(
    IAppDbContext dbContext,
    IMediaStorage storage,
    IMediaProcessingQueue queue
) : ICommandHandler<ImportManualInboxCommand, Result<ImportManualInboxResultDto>>
{
    public async Task<Result<ImportManualInboxResultDto>> Handle(
        ImportManualInboxCommand 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<ImportManualInboxResultDto>(ShowErrors.NotFound);

        var available = storage
            .ListManualInbox(int.MaxValue)
            .ToDictionary(f => f.RelativePath, StringComparer.Ordinal);

        var failed = new List<ImportFailureDto>();
        var imported = 0;

        // Файлы разбираются по одному и независимо: один неподдерживаемый контейнер не должен
        // отменять весь пакет — в ручном разборе это как раз частый случай.
        foreach (var item in command.Items.DistinctBy(i => i.RelativePath, StringComparer.Ordinal))
        {
            if (!available.TryGetValue(item.RelativePath, out var file))
            {
                failed.Add(
                    new ImportFailureDto(item.RelativePath, MediaErrors.SourceNotFound.Message)
                );
                continue;
            }

            if (!MediaFormats.IsAllowed(file.Name))
            {
                failed.Add(
                    new ImportFailureDto(item.RelativePath, MediaErrors.UnsupportedFormat.Message)
                );
                continue;
            }

            if (!show.CanAddEpisode)
            {
                failed.Add(
                    new ImportFailureDto(
                        item.RelativePath,
                        ShowErrors.SingleAlreadyHasEpisode.Message
                    )
                );
                continue;
            }

            var duplicate = await dbContext.MediaAssets.AnyAsync(
                a => a.OriginalFileName == file.Name && a.Status != MediaAssetStatus.Failed,
                cancellationToken
            );
            if (duplicate)
            {
                failed.Add(
                    new ImportFailureDto(item.RelativePath, MediaErrors.DuplicateFileName.Message)
                );
                continue;
            }

            var assetId = await RegisterAsync(file, item.RelativePath, cancellationToken);
            if (assetId is null)
            {
                failed.Add(
                    new ImportFailureDto(item.RelativePath, MediaErrors.SourceNotFound.Message)
                );
                continue;
            }

            var episode = show.AddEpisode(assetId.Value);
            SetNumbers(episode, item, file.Name);

            queue.Enqueue(assetId.Value);
            imported++;

            // Спутники (субтитры, nfo, обложка) без основного файла — мусор, за которым потом
            // никто не вернётся. Удаляем сразу после успешного переноса, а не «когда-нибудь».
            await storage.CleanupManualLeftoversAsync(item.RelativePath, cancellationToken);
        }

        return Result.Success(new ImportManualInboxResultDto(imported, failed));
    }

    /// <summary>
    /// Номера серии: сначала то, что пришло с клиента (там пользователь видел разбор и мог поправить
    /// его регуляркой или ручным сезоном), иначе — разбор имени на сервере.
    /// </summary>
    private static void SetNumbers(
        Domain.Library.ShowEpisode episode,
        ManualImportItem item,
        string fileName
    )
    {
        if (item.Episode is { } episodeNumber)
        {
            episode.SetNumbers(item.Season ?? 1, episodeNumber);
            return;
        }

        if (EpisodeName.Parse(fileName) is { } parsed)
            episode.SetNumbers(item.Season ?? parsed.Season, parsed.Episode);
    }

    /// <summary>
    /// Регистрирует ассет и переносит файл в originals/. Порядок тот же, что при обычной загрузке:
    /// сначала строка в БД, затем файл — при сбое переноса регистрация откатывается, чтобы
    /// не осталось записи без исходника.
    /// </summary>
    private async Task<Guid?> RegisterAsync(
        IMediaStorage.ManualInboxFile file,
        string relativePath,
        CancellationToken cancellationToken
    )
    {
        var extension = Path.GetExtension(file.Name).ToLowerInvariant();
        var asset = MediaAsset.Register(file.Name, extension, MediaSource.ManualInbox);

        dbContext.MediaAssets.Add(asset);
        await dbContext.SaveChangesAsync(cancellationToken);
        try
        {
            await storage.PromoteToOriginalAsync(
                MediaSource.ManualInbox,
                relativePath,
                asset.Id,
                extension,
                cancellationToken
            );
        }
        catch (IOException)
        {
            dbContext.MediaAssets.Remove(asset);
            await dbContext.SaveChangesAsync(cancellationToken);
            return null;
        }

        return asset.Id;
    }
}
