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

namespace TeleWave.Application.Metadata;

/// <summary>
/// Применение найденных метаданных к шоу: описание, год, постер в реестр изображений, жанры и
/// возрастной рейтинг. Общее для точечного применения и массового обогащения — правила «что можно
/// перезаписать» должны быть одни, иначе пакетная обработка тихо разойдётся с ручной.
/// </summary>
public sealed class ShowMetadataApplier(
    IAppDbContext dbContext,
    IMetadataProviderResolver resolver,
    IImageDownloader downloader,
    IImageStore imageStore,
    GenreMatcher genreMatcher,
    ILogger<ShowMetadataApplier> logger
)
{
    /// <summary>
    /// Применяет метаданные и возвращает то, что отдал источник: вызывающему бывает нужно само
    /// содержимое — например, чтобы назвать только что заведённый фильм так, как он называется
    /// в источнике, а не так, как назывался файл.
    /// </summary>
    public async Task<Result<ShowMetadata>> ApplyAsync(
        Show show,
        string providerKey,
        string externalId,
        CancellationToken cancellationToken
    )
    {
        // Ролики метаданными не обогащаются: у них нет ни года, ни постера, а сотня роликов
        // в поиске только мешала бы.
        if (show.Kind == ShowKind.Interstitial)
            return Result.Failure<ShowMetadata>(MetadataErrors.NotForInterstitials);

        var provider = resolver.Resolve(providerKey);
        if (provider is null)
            return Result.Failure<ShowMetadata>(MetadataErrors.ProviderNotAvailable);

        // Тип берём у шоу, а не из запроса: иначе клиент смог бы заставить TMDb отдать метаданные
        // одноимённого фильма для сериала — идентификаторы у них независимые.
        var meta = await provider.GetShowAsync(externalId, show.Kind, cancellationToken);
        if (meta is null)
            return Result.Failure<ShowMetadata>(MetadataErrors.NotFound);

        var posterImageId = await SavePosterAsync(show, meta.PosterUrl, cancellationToken);

        show.ApplyMetadata(
            providerKey,
            meta.ExternalId,
            meta.Overview,
            meta.Year,
            posterImageId,
            meta.Franchise
        );

        // Жанры источника перезаписывают текущие, только если хоть один распознан справочником:
        // иначе применение метаданных к шоу с руками проставленными жанрами обнуляло бы их.
        var genreIds = await genreMatcher.MatchAsync(meta.Genres, cancellationToken);
        if (genreIds.Count > 0)
            show.SetGenres(genreIds);

        // Рейтинг — по тому же правилу: проставляем только распознанный. «Not Rated», «N/A» и местные
        // шкалы означают отсутствие данных, и снимать ими уже проставленный рейтинг нельзя — иначе
        // «обновить метаданные» тихо открыло бы шоу дорогу в детское время.
        if (ContentRating.Parse(meta.ContentRating) is { } audience)
            show.SetAudience(audience);

        // Оригинальное название проставляем только пустому: у релизов оно латиницей, а карточка
        // источника русская, — но если админ вписал своё, переписывать его нельзя.
        if (string.IsNullOrWhiteSpace(show.OriginalName) && meta.OriginalTitle is { Length: > 0 })
            show.SetOriginalName(meta.OriginalTitle);

        return Result.Success(meta);
    }

    /// <summary>Постер скачиваем и регистрируем в общем реестре изображений (галерея).</summary>
    private async Task<Guid?> SavePosterAsync(
        Show show,
        string? posterUrl,
        CancellationToken cancellationToken
    )
    {
        // Три исхода — три разные починки, и снаружи они неразличимы: шоу просто остаётся без
        // постера. Пишем в лог, какой именно случился.
        if (string.IsNullOrEmpty(posterUrl))
        {
            logger.LogWarning(
                "Постер для «{Show}» не проставлен: источник не вернул ссылку на картинку",
                show.Name
            );
            return null;
        }

        var downloaded = await downloader.DownloadAsync(posterUrl, cancellationToken);
        if (downloaded is null)
        {
            // Подробности (статус, исключение) пишет сам загрузчик.
            logger.LogWarning(
                "Постер для «{Show}» не проставлен: {Url} не скачался",
                show.Name,
                posterUrl
            );
            return null;
        }

        var image = Image.Create(ImageCategory.ShowPoster, downloaded.Extension, show.Name);
        dbContext.Images.Add(image);
        await imageStore.SaveAsync(
            image.Id,
            downloaded.Extension,
            downloaded.Content,
            cancellationToken
        );
        // Успех — отладочным уровнем: при массовом обогащении это строка на каждое шоу, а знать
        // оператору интересно обратное — что постер не доехал.
        logger.LogDebug(
            "Постер для «{Show}» сохранён как изображение {ImageId}",
            show.Name,
            image.Id
        );
        return image.Id;
    }
}
