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

namespace TeleWave.Application.Metadata.EnrichShows;

public sealed class EnrichShowsMetadataCommandHandler(
    IAppDbContext dbContext,
    IMetadataProviderResolver resolver,
    ShowMetadataApplier applier
) : ICommandHandler<EnrichShowsMetadataCommand, Result<EnrichShowsResultDto>>
{
    public async Task<Result<EnrichShowsResultDto>> Handle(
        EnrichShowsMetadataCommand command,
        CancellationToken cancellationToken
    )
    {
        var provider = resolver.Resolve(command.Provider);
        if (provider is null)
            return Result.Failure<EnrichShowsResultDto>(MetadataErrors.ProviderNotAvailable);

        var showIds = command.ShowIds.Distinct().ToList();
        var shows = await dbContext
            .Shows.Include(s => s.Genres)
            .Where(s => showIds.Contains(s.Id) && s.Kind != ShowKind.Interstitial)
            .ToListAsync(cancellationToken);

        var applied = 0;
        var skipped = new List<EnrichSkipDto>();

        foreach (var show in shows)
        {
            // Уже привязанное не трогаем: массовая операция не должна переписывать выбор, сделанный
            // руками. Обновить такое шоу можно точечно на его карточке.
            if (!string.IsNullOrEmpty(show.MetadataExternalId))
            {
                skipped.Add(Skip(show, "источник уже привязан"));
                continue;
            }

            // Метаданные ищутся по оригинальному названию — как и в карточке шоу.
            var term = string.IsNullOrWhiteSpace(show.OriginalName) ? show.Name : show.OriginalName;
            var candidates = await provider.SearchShowsAsync(term, show.Kind, cancellationToken);

            if (Pick(candidates, show) is not { } match)
            {
                skipped.Add(
                    Skip(
                        show,
                        candidates.Count == 0 ? "ничего не найдено" : "нет точного совпадения"
                    )
                );
                continue;
            }

            var result = await applier.ApplyAsync(
                show,
                command.Provider,
                match.ExternalId,
                cancellationToken
            );
            if (result.IsSuccess)
                applied++;
            else
                skipped.Add(Skip(show, result.Error.Message));
        }

        return Result.Success(new EnrichShowsResultDto(applied, skipped));
    }

    /// <summary>
    /// Кандидат берётся, только если название совпало точно (без учёта регистра и пробелов), а при
    /// известном годе сходится и он. Брать «первый из выдачи» нельзя: у поиска по «Пилот» или
    /// «Сезон 1» верхним окажется случайное чужое шоу, и в эфир уедет чужой постер с чужим рейтингом.
    /// Промахи остаются админу — точечно, с глазами.
    /// </summary>
    private static MetadataCandidate? Pick(IReadOnlyList<MetadataCandidate> candidates, Show show)
    {
        var names = new[] { show.OriginalName, show.Name }
            .Where(n => !string.IsNullOrWhiteSpace(n))
            .Select(n => n!.Trim())
            .ToList();

        var matches = candidates
            .Where(c =>
                names.Any(n => string.Equals(c.Title.Trim(), n, StringComparison.OrdinalIgnoreCase))
            )
            .Where(c => show.Year is not { } year || c.Year is null || c.Year == year)
            .ToList();

        return matches.Count == 1 ? matches[0] : null;
    }

    private static EnrichSkipDto Skip(Show show, string reason) => new(show.Id, show.Name, reason);
}
