using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;
using TeleWave.Domain.Images;
using TeleWave.Domain.Library;

namespace TeleWave.Application.Metadata.RefreshEpisodes;

public sealed class RefreshShowEpisodesMetadataCommandHandler(
    IAppDbContext dbContext,
    IMetadataProviderResolver resolver,
    IImageDownloader downloader,
    IImageStore imageStore,
    ILogger<RefreshShowEpisodesMetadataCommandHandler> logger
) : ICommandHandler<RefreshShowEpisodesMetadataCommand, Result<int>>
{
    public async Task<Result<int>> Handle(
        RefreshShowEpisodesMetadataCommand 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<int>(ShowErrors.NotFound);

        // Серии есть только у сериала — см. FindMissingEpisodesQueryHandler.
        if (show.Kind != ShowKind.Series)
            return Result.Failure<int>(MetadataErrors.EpisodesForSeriesOnly);

        if (string.IsNullOrEmpty(show.MetadataExternalId))
            return Result.Failure<int>(MetadataErrors.NoLinkedSource);

        var provider = show.MetadataProvider is { } key ? resolver.Resolve(key) : null;
        if (provider is null)
            return Result.Failure<int>(MetadataErrors.ProviderNotAvailable);

        // Имена файлов — чтобы распознать номера у серий, где они ещё не проставлены.
        var assetIds = show.Episodes.Select(e => e.MediaAssetId).Distinct().ToList();
        var names = await dbContext
            .MediaAssets.AsNoTracking()
            .Where(a => assetIds.Contains(a.Id))
            .Select(a => new { a.Id, a.OriginalFileName })
            .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);

        var updated = 0;
        foreach (var episode in show.Episodes)
        {
            if (ResolveNumbers(episode, names) is not { } numbers)
                continue;

            var meta = await provider.GetEpisodeAsync(
                show.MetadataExternalId,
                numbers.Season,
                numbers.Episode,
                cancellationToken
            );
            if (meta is null)
                continue;

            var stillImageId = await SaveStillAsync(meta, cancellationToken);
            episode.ApplyMetadata(meta.Title, meta.Overview, stillImageId, meta.AirDate);
            updated++;
        }

        return Result.Success(updated);
    }

    /// <summary>
    /// Сезон и номер серии: сперва проставленные, иначе распознанные из имени файла (и тогда сразу
    /// сохраняются на серии). null — распознать не удалось, серию пропускаем.
    /// </summary>
    private static (int Season, int Episode)? ResolveNumbers(
        ShowEpisode episode,
        IReadOnlyDictionary<Guid, string> assetNames
    )
    {
        if (episode.Season is { } season && episode.Episode is { } number)
            return (season, number);

        if (
            !assetNames.TryGetValue(episode.MediaAssetId, out var name)
            || EpisodeName.Parse(name) is not { } parsed
        )
            return null;

        episode.SetNumbers(parsed.Season, parsed.Episode);
        return parsed;
    }

    /// <summary>
    /// Скачивает кадр серии и регистрирует его в общем реестре изображений. null — кадра нет либо
    /// скачать не удалось; это не повод отменять применение остальных метаданных.
    /// </summary>
    private async Task<Guid?> SaveStillAsync(
        EpisodeMetadata meta,
        CancellationToken cancellationToken
    )
    {
        // Как и с постером шоу: снаружи «кадра нет» неотличимо от «кадр не доехал», а чинится это
        // по-разному. Подробности отказа скачивания пишет сам загрузчик.
        if (string.IsNullOrEmpty(meta.StillUrl))
        {
            logger.LogWarning(
                "Кадр серии «{Episode}» не проставлен: источник не вернул ссылку",
                meta.Title
            );
            return null;
        }

        var downloaded = await downloader.DownloadAsync(meta.StillUrl, cancellationToken);
        if (downloaded is null)
        {
            logger.LogWarning(
                "Кадр серии «{Episode}» не проставлен: {Url} не скачался",
                meta.Title,
                meta.StillUrl
            );
            return null;
        }

        var image = Image.Create(ImageCategory.EpisodeStill, downloaded.Extension, meta.Title);
        dbContext.Images.Add(image);
        await imageStore.SaveAsync(
            image.Id,
            downloaded.Extension,
            downloaded.Content,
            cancellationToken
        );
        return image.Id;
    }
}
