using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Scanner.Core.Interfaces;
using Microsoft.Extensions.Logging;

namespace ErsatzTV.Scanner.Application.Jellyfin;

public class
    SynchronizeJellyfinShowByIdHandler : IRequestHandler<SynchronizeJellyfinShowById, Either<BaseError, string>>
{
    private readonly IJellyfinSecretStore _jellyfinSecretStore;
    private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner;
    private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository;
    private readonly ILogger<SynchronizeJellyfinShowByIdHandler> _logger;
    private readonly IScannerProxy _scannerProxy;
    private readonly IMediaSourceRepository _mediaSourceRepository;

    public SynchronizeJellyfinShowByIdHandler(
        IScannerProxy scannerProxy,
        IMediaSourceRepository mediaSourceRepository,
        IJellyfinTelevisionRepository jellyfinTelevisionRepository,
        IJellyfinSecretStore jellyfinSecretStore,
        IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner,
        ILogger<SynchronizeJellyfinShowByIdHandler> logger)
    {
        _scannerProxy = scannerProxy;
        _mediaSourceRepository = mediaSourceRepository;
        _jellyfinTelevisionRepository = jellyfinTelevisionRepository;
        _jellyfinSecretStore = jellyfinSecretStore;
        _jellyfinTelevisionLibraryScanner = jellyfinTelevisionLibraryScanner;
        _logger = logger;
    }

    public async Task<Either<BaseError, string>> Handle(
        SynchronizeJellyfinShowById request,
        CancellationToken cancellationToken)
    {
        Validation<BaseError, RequestParameters> validation = await Validate(request, cancellationToken);
        return await validation.Match(
            parameters => Synchronize(parameters, cancellationToken),
            error => Task.FromResult<Either<BaseError, string>>(error.Join()));
    }

    private async Task<Either<BaseError, string>> Synchronize(
        RequestParameters parameters,
        CancellationToken cancellationToken)
    {
        // a mixed library legitimately contains shows alongside movies and music videos
        if (parameters.Library.MediaKind is not (LibraryMediaKind.Shows or LibraryMediaKind.Mixed))
        {
            return BaseError.New($"Library {parameters.Library.Name} is not a TV show library");
        }

        _scannerProxy.SetBaseUrl(parameters.BaseUrl);

        _logger.LogInformation(
            "Starting targeted scan for show '{ShowTitle}' in Jellyfin library {LibraryName}",
            parameters.ShowTitle,
            parameters.Library.Name);

        Either<BaseError, Unit> result = await _jellyfinTelevisionLibraryScanner.ScanSingleShow(
            parameters.ConnectionParameters,
            parameters.Library,
            parameters.ItemId,
            parameters.ShowTitle,
            parameters.DeepScan,
            cancellationToken);

        foreach (BaseError error in result.LeftToSeq())
        {
            _logger.LogError("Error synchronizing Jellyfin show '{ShowTitle}': {Error}", parameters.ShowTitle, error);
        }

        return result.Map(_ => $"Show '{parameters.ShowTitle}' in {parameters.Library.Name}");
    }

    private async Task<Validation<BaseError, RequestParameters>> Validate(
        SynchronizeJellyfinShowById request,
        CancellationToken cancellationToken) =>
        (await ValidateConnection(request), await JellyfinLibraryMustExist(request),
            await JellyfinShowMustExist(request, cancellationToken))
        .Apply((connectionParameters, jellyfinLibrary, showTitleItemId) =>
            new RequestParameters(
                connectionParameters,
                jellyfinLibrary,
                showTitleItemId.ItemId,
                showTitleItemId.Title,
                request.DeepScan,
                request.BaseUrl
            ));

    private Task<Validation<BaseError, JellyfinConnectionParameters>> ValidateConnection(
        SynchronizeJellyfinShowById request) =>
        JellyfinMediaSourceMustExist(request)
            .BindT(MediaSourceMustHaveActiveConnection)
            .BindT(MediaSourceMustHaveApiKey);

    private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist(
        SynchronizeJellyfinShowById request) =>
        _mediaSourceRepository.GetJellyfinByLibraryId(request.JellyfinLibraryId)
            .Map(v => v.ToValidation<BaseError>(
                $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist."));

    private Validation<BaseError, JellyfinConnectionParameters> MediaSourceMustHaveActiveConnection(
        JellyfinMediaSource jellyfinMediaSource)
    {
        Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
        return maybeConnection.Map(connection => new JellyfinConnectionParameters(
                connection.Address,
                string.Empty,
                connection.JellyfinMediaSourceId))
            .ToValidation<BaseError>("Jellyfin media source requires an active connection");
    }

    private async Task<Validation<BaseError, JellyfinConnectionParameters>> MediaSourceMustHaveApiKey(
        JellyfinConnectionParameters connectionParameters)
    {
        JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
        return Optional(secrets.Address == connectionParameters.Address)
            .Where(match => match)
            .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
            .ToValidation<BaseError>("Jellyfin media source requires an api key");
    }

    private Task<Validation<BaseError, JellyfinLibrary>> JellyfinLibraryMustExist(
        SynchronizeJellyfinShowById request) =>
        _mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId)
            .Map(v => v.ToValidation<BaseError>($"Jellyfin library {request.JellyfinLibraryId} does not exist."));

    private Task<Validation<BaseError, JellyfinShowTitleItemIdResult>> JellyfinShowMustExist(
        SynchronizeJellyfinShowById request,
        CancellationToken cancellationToken) =>
        _jellyfinTelevisionRepository.GetShowTitleItemId(request.JellyfinLibraryId, request.ShowId, cancellationToken)
            .Map(v => v.ToValidation<BaseError>(
                $"Jellyfin show {request.ShowId} does not exist in library {request.JellyfinLibraryId}."));

    private record RequestParameters(
        JellyfinConnectionParameters ConnectionParameters,
        JellyfinLibrary Library,
        string ItemId,
        string ShowTitle,
        bool DeepScan,
        string BaseUrl);
}
