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

namespace TeleWave.Application.Media.MovieImport;

/// <summary>
/// Заводит фильмы пачкой: шоу на файл, метаданные из источника, файл — в очередь обработки.
///
/// Фильмы разбираются независимо: один неподдерживаемый контейнер или пропавший из <c>manual/</c>
/// файл не должен отменять остальные сорок — в разборе чужой библиотеки это частый случай, и
/// заставлять человека повторять всю пачку из-за одной строки нельзя.
/// </summary>
public sealed class ImportMoviesCommandHandler(
    IAppDbContext dbContext,
    ManualInboxTaker taker,
    IMediaProcessingQueue queue,
    ShowMetadataApplier applier
) : ICommandHandler<ImportMoviesCommand, Result<ImportMoviesResultDto>>
{
    public async Task<Result<ImportMoviesResultDto>> Handle(
        ImportMoviesCommand command,
        CancellationToken cancellationToken
    )
    {
        var available = taker.List();

        var failed = new List<MovieImportFailureDto>();
        var created = 0;
        var enriched = 0;

        foreach (var item in command.Items)
        {
            var asset = await ResolveAssetAsync(item, available, failed, cancellationToken);
            if (asset is null)
                continue;

            var show = Show.Create(item.Title.Trim(), ShowKind.Single);
            dbContext.Shows.Add(show);
            show.AddEpisode(asset.Value.AssetId);
            created++;

            if (asset.Value.Enqueue)
                queue.Enqueue(asset.Value.AssetId);

            if (item.RelativePath is { Length: > 0 } path)
                await taker.CleanupAsync(path, cancellationToken);

            if (item.ExternalId is not { Length: > 0 } externalId)
                continue;

            // Метаданные — отдельная неудача: шоу с файлом уже полезно, а без постера оно
            // всего лишь неполно, и дотянуть его можно массовым обогащением.
            var result = await applier.ApplyAsync(
                show,
                command.Provider,
                externalId,
                cancellationToken
            );
            if (!result.IsSuccess)
            {
                failed.Add(new MovieImportFailureDto(item.Title, result.Error.Message));
                continue;
            }

            enriched++;
            Rename(show, item, result.Value);
        }

        return Result.Success(new ImportMoviesResultDto(created, enriched, failed));
    }

    /// <summary>
    /// Называет фильм так, как он называется в источнике, а не так, как назывался файл: в таблице
    /// разбора поле «Название» — это строка поиска, и «Die Hard 2» там стоит ровно для того, чтобы
    /// найти «Крепкий орешек 2». Название из файла при этом не пропадает — оно становится
    /// оригинальным, если источник своего не дал: по нему потом ищутся релизы и сходятся имена.
    /// </summary>
    private static void Rename(Show show, MovieImportItem item, ShowMetadata meta)
    {
        if (meta.Title is { Length: > 0 } title)
            show.SetName(title);

        if (string.IsNullOrWhiteSpace(show.OriginalName) && !Same(item.Title, show.Name))
            show.SetOriginalName(item.Title.Trim());
    }

    private static bool Same(string left, string right) =>
        string.Equals(left.Trim(), right.Trim(), StringComparison.CurrentCultureIgnoreCase);

    /// <summary>Ассет фильма: уже загруженный браузером либо забранный из <c>manual/</c>.</summary>
    private async Task<(Guid AssetId, bool Enqueue)?> ResolveAssetAsync(
        MovieImportItem item,
        IReadOnlyDictionary<string, IMediaStorage.ManualInboxFile> available,
        List<MovieImportFailureDto> failed,
        CancellationToken cancellationToken
    )
    {
        if (item.AssetId is { } assetId)
            return await UploadedAsync(item, assetId, failed, cancellationToken);

        var taken = await taker.TakeAsync(
            available,
            item.RelativePath ?? string.Empty,
            cancellationToken
        );
        if (taken.IsSuccess)
            return (taken.Value, true);

        failed.Add(new MovieImportFailureDto(item.Title, taken.Error.Message));
        return null;
    }

    /// <summary>
    /// Уже загруженный браузером файл. В очередь он попал ещё при загрузке, поэтому второй раз
    /// не ставится — иначе один и тот же исходник нарезался бы дважды.
    /// </summary>
    private async Task<(Guid AssetId, bool Enqueue)?> UploadedAsync(
        MovieImportItem item,
        Guid assetId,
        List<MovieImportFailureDto> failed,
        CancellationToken cancellationToken
    )
    {
        var exists = await dbContext.MediaAssets.AnyAsync(a => a.Id == assetId, cancellationToken);
        if (!exists)
        {
            failed.Add(new MovieImportFailureDto(item.Title, MediaErrors.NotFound.Message));
            return null;
        }

        var taken = await dbContext.Shows.AnyAsync(
            s => s.Episodes.Any(e => e.MediaAssetId == assetId),
            cancellationToken
        );
        if (taken)
        {
            failed.Add(new MovieImportFailureDto(item.Title, MediaErrors.AlreadyAttached.Message));
            return null;
        }

        return (assetId, false);
    }
}
