using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;

namespace ErsatzTV.Scanner.Core.Jellyfin;

public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<JellyfinConnectionParameters,
    JellyfinLibrary,
    JellyfinShow, JellyfinSeason, JellyfinEpisode,
    JellyfinItemEtag>, IJellyfinTelevisionLibraryScanner
{
    private readonly IJellyfinApiClient _jellyfinApiClient;
    private readonly ILogger<JellyfinTelevisionLibraryScanner> _logger;
    private readonly IMediaSourceRepository _mediaSourceRepository;
    private readonly IJellyfinPathReplacementService _pathReplacementService;
    private readonly IMetadataRepository _metadataRepository;
    private readonly IJellyfinTelevisionRepository _jellyfinTelevisionRepository;
    private readonly ITelevisionRepository _televisionRepository;

    public JellyfinTelevisionLibraryScanner(
        IScannerProxy scannerProxy,
        IJellyfinApiClient jellyfinApiClient,
        IMediaSourceRepository mediaSourceRepository,
        IJellyfinTelevisionRepository jellyfinTelevisionRepository,
        ITelevisionRepository televisionRepository,
        IJellyfinPathReplacementService pathReplacementService,
        IFileSystem fileSystem,
        ILocalChaptersProvider localChaptersProvider,
        IMetadataRepository metadataRepository,
        ILogger<JellyfinTelevisionLibraryScanner> logger)
        : base(
            scannerProxy,
            fileSystem,
            localChaptersProvider,
            metadataRepository,
            logger)
    {
        _jellyfinApiClient = jellyfinApiClient;
        _mediaSourceRepository = mediaSourceRepository;
        _jellyfinTelevisionRepository = jellyfinTelevisionRepository;
        _televisionRepository = televisionRepository;
        _pathReplacementService = pathReplacementService;
        _metadataRepository = metadataRepository;
        _logger = logger;
    }

    protected override bool ServerSupportsRemoteStreaming => true;

    public async Task<Either<BaseError, Unit>> ScanLibrary(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        bool deepScan,
        CancellationToken cancellationToken)
    {
        List<JellyfinPathReplacement> pathReplacements =
            await _mediaSourceRepository.GetJellyfinPathReplacements(library.MediaSourceId);

        string GetLocalPath(JellyfinEpisode episode)
        {
            return _pathReplacementService.GetReplacementJellyfinPath(
                pathReplacements,
                episode.GetHeadVersion().MediaFiles.Head().Path,
                false);
        }

        return await ScanLibrary(
            _jellyfinTelevisionRepository,
            connectionParameters,
            library,
            GetLocalPath,
            deepScan,
            cancellationToken);
    }

    public async Task<Either<BaseError, Unit>> ScanSingleShow(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        string showId,
        string showTitle,
        bool deepScan,
        CancellationToken cancellationToken)
    {
        List<JellyfinPathReplacement> pathReplacements =
            await _mediaSourceRepository.GetJellyfinPathReplacements(library.MediaSourceId);

        string GetLocalPath(JellyfinEpisode episode)
        {
            return _pathReplacementService.GetReplacementJellyfinPath(
                pathReplacements,
                episode.GetHeadVersion().MediaFiles.Head().Path,
                false);
        }

        // Search for the specific show
        Either<BaseError, Option<JellyfinShow>> searchResult = await _jellyfinApiClient.GetSingleShow(
            connectionParameters.Address,
            connectionParameters.AuthorizationHeader,
            library,
            showId);

        return await searchResult.Match(
            async maybeShow =>
            {
                foreach (JellyfinShow show in maybeShow)
                {
                    _logger.LogInformation(
                        "Found show '{ShowTitle}' with id {ShowId}, starting targeted scan",
                        showTitle,
                        show.ItemId);

                    return await ScanSingleShowInternal(
                        _jellyfinTelevisionRepository,
                        connectionParameters,
                        library,
                        show,
                        GetLocalPath,
                        deepScan,
                        cancellationToken);
                }

                _logger.LogWarning("No show found with id {ShowId} in library {LibraryName}", showId, library.Name);

                return Right<BaseError, Unit>(Unit.Default);
            },
            error => Task.FromResult<Either<BaseError, Unit>>(error));
    }

    protected override IAsyncEnumerable<Tuple<JellyfinShow, int>> GetShowLibraryItems(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library) =>
        _jellyfinApiClient.GetShowLibraryItemsWithoutPeople(
            connectionParameters.Address,
            connectionParameters.AuthorizationHeader,
            library);

    protected override string MediaServerItemId(JellyfinShow show) => show.ItemId;
    protected override string MediaServerItemId(JellyfinSeason season) => season.ItemId;
    protected override string MediaServerItemId(JellyfinEpisode episode) => episode.ItemId;

    protected override string MediaServerEtag(JellyfinShow show) => show.Etag;
    protected override string MediaServerEtag(JellyfinSeason season) => season.Etag;
    protected override string MediaServerEtag(JellyfinEpisode episode) => episode.Etag;

    protected override IAsyncEnumerable<Tuple<JellyfinSeason, int>> GetSeasonLibraryItems(
        JellyfinLibrary library,
        JellyfinConnectionParameters connectionParameters,
        JellyfinShow show) =>
        _jellyfinApiClient.GetSeasonLibraryItems(
            connectionParameters.Address,
            connectionParameters.AuthorizationHeader,
            library,
            show.ItemId);

    protected override IAsyncEnumerable<Tuple<JellyfinEpisode, int>> GetEpisodeLibraryItems(
        JellyfinLibrary library,
        JellyfinConnectionParameters connectionParameters,
        JellyfinShow show,
        JellyfinSeason season,
        bool isNewSeason)
    {
        if (isNewSeason)
        {
            return _jellyfinApiClient.GetEpisodeLibraryItems(
                connectionParameters.Address,
                connectionParameters.AuthorizationHeader,
                library,
                season.ItemId);
        }

        return _jellyfinApiClient.GetEpisodeLibraryItemsWithoutPeople(
            connectionParameters.Address,
            connectionParameters.AuthorizationHeader,
            library,
            season.ItemId);
    }

    protected override async Task<Option<ShowMetadata>> GetFullMetadata(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        MediaItemScanResult<JellyfinShow> result,
        JellyfinShow incoming,
        bool deepScan)
    {
        if (result.IsAdded || result.Item.Etag != incoming.Etag || deepScan)
        {
            Either<BaseError, Option<JellyfinShow>> maybeShowResult = await _jellyfinApiClient.GetSingleShow(
                connectionParameters.Address,
                connectionParameters.AuthorizationHeader,
                library,
                incoming.ItemId);

            foreach (BaseError error in maybeShowResult.LeftToSeq())
            {
                _logger.LogWarning("Failed to get show metadata from Jellyfin: {Error}", error.ToString());
            }

            foreach (Option<JellyfinShow> maybeShow in maybeShowResult.RightToSeq())
            {
                foreach (JellyfinShow show in maybeShow)
                {
                    return show.ShowMetadata.HeadOrNone();
                }
            }
        }

        return None;
    }

    protected override Task<Option<SeasonMetadata>> GetFullMetadata(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        MediaItemScanResult<JellyfinSeason> result,
        JellyfinSeason incoming,
        bool deepScan) =>
        Task.FromResult(Option<SeasonMetadata>.None);

    protected override async Task<Option<EpisodeMetadata>> GetFullMetadata(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        MediaItemScanResult<JellyfinEpisode> result,
        JellyfinEpisode incoming,
        bool deepScan)
    {
        if (result.Item.Season is JellyfinSeason jellyfinSeason &&
            (result.IsAdded || result.Item.Etag != incoming.Etag || deepScan))
        {
            Either<BaseError, Option<JellyfinEpisode>> maybeEpisodeResult = await _jellyfinApiClient.GetSingleEpisode(
                connectionParameters.Address,
                connectionParameters.AuthorizationHeader,
                library,
                jellyfinSeason.ItemId,
                incoming.ItemId);

            foreach (BaseError error in maybeEpisodeResult.LeftToSeq())
            {
                _logger.LogWarning("Failed to get episode metadata from Jellyfin: {Error}", error.ToString());
            }

            foreach (Option<JellyfinEpisode> maybeEpisode in maybeEpisodeResult.RightToSeq())
            {
                foreach (JellyfinEpisode episode in maybeEpisode)
                {
                    return episode.EpisodeMetadata.HeadOrNone();
                }
            }
        }

        return None;
    }

    protected override Task<Option<Tuple<EpisodeMetadata, MediaVersion>>> GetFullMetadataAndStatistics(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        MediaItemScanResult<JellyfinEpisode> result,
        JellyfinEpisode incoming) => Task.FromResult(Option<Tuple<EpisodeMetadata, MediaVersion>>.None);

    protected override async Task<Option<MediaVersion>> GetMediaServerStatistics(
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        MediaItemScanResult<JellyfinEpisode> result,
        JellyfinEpisode incoming)
    {
        _logger.LogDebug("Refreshing {Attribute} for {Path}", "Jellyfin Statistics", result.LocalPath);

        Either<BaseError, MediaVersion> maybeVersion =
            await _jellyfinApiClient.GetPlaybackInfo(
                connectionParameters.Address,
                connectionParameters.AuthorizationHeader,
                library,
                incoming.ItemId);

        foreach (BaseError error in maybeVersion.LeftToSeq())
        {
            _logger.LogWarning("Failed to get episode statistics from Jellyfin: {Error}", error.ToString());
        }

        // chapters are pulled with metadata, not with statistics, but we need to save them here
        foreach (MediaVersion version in maybeVersion.RightToSeq())
        {
            version.Chapters = result.Item.GetHeadVersion().Chapters;
        }

        return maybeVersion.ToOption();
    }

    protected override async Task<Either<BaseError, MediaItemScanResult<JellyfinShow>>> UpdateMetadata(
        MediaItemScanResult<JellyfinShow> result,
        ShowMetadata fullMetadata)
    {
        JellyfinShow existing = result.Item;
        ShowMetadata existingMetadata = existing.ShowMetadata.Head();

        foreach (Actor actor in existingMetadata.Actors
                     .Filter(a =>
                         fullMetadata.Actors.All(a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null))
                     .ToList())
        {
            existingMetadata.Actors.Remove(actor);
            if (await _metadataRepository.RemoveActor(actor))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Actor actor in fullMetadata.Actors
                     .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name))
                     .ToList())
        {
            existingMetadata.Actors.Add(actor);
            if (await _televisionRepository.AddActor(existingMetadata, actor))
            {
                result.IsUpdated = true;
            }
        }

        if (result.IsUpdated)
        {
            await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
        }

        return result;
    }

    protected override Task<Either<BaseError, MediaItemScanResult<JellyfinSeason>>> UpdateMetadata(
        MediaItemScanResult<JellyfinSeason> result,
        SeasonMetadata fullMetadata) =>
        Task.FromResult<Either<BaseError, MediaItemScanResult<JellyfinSeason>>>(result);

    protected override async Task<Either<BaseError, MediaItemScanResult<JellyfinEpisode>>> UpdateMetadata(
        MediaItemScanResult<JellyfinEpisode> result,
        EpisodeMetadata fullMetadata,
        CancellationToken cancellationToken)
    {
        JellyfinEpisode existing = result.Item;
        EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head();

        foreach (Actor actor in existingMetadata.Actors
                     .Filter(a =>
                         fullMetadata.Actors.All(a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null))
                     .ToList())
        {
            existingMetadata.Actors.Remove(actor);
            if (await _metadataRepository.RemoveActor(actor))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Actor actor in fullMetadata.Actors
                     .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name))
                     .ToList())
        {
            existingMetadata.Actors.Add(actor);
            if (await _televisionRepository.AddActor(existingMetadata, actor))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Director director in existingMetadata.Directors
                     .Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name))
                     .ToList())
        {
            existingMetadata.Directors.Remove(director);
            if (await _metadataRepository.RemoveDirector(director))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Director director in fullMetadata.Directors
                     .Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name))
                     .ToList())
        {
            existingMetadata.Directors.Add(director);
            if (await _televisionRepository.AddDirector(existingMetadata, director))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Writer writer in existingMetadata.Writers
                     .Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name))
                     .ToList())
        {
            existingMetadata.Writers.Remove(writer);
            if (await _metadataRepository.RemoveWriter(writer))
            {
                result.IsUpdated = true;
            }
        }

        foreach (Writer writer in fullMetadata.Writers
                     .Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name))
                     .ToList())
        {
            existingMetadata.Writers.Add(writer);
            if (await _televisionRepository.AddWriter(existingMetadata, writer))
            {
                result.IsUpdated = true;
            }
        }

        if (result.IsUpdated)
        {
            await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
        }

        return result;
    }

    private async Task<Either<BaseError, Unit>> ScanSingleShowInternal(
        IJellyfinTelevisionRepository televisionRepository,
        JellyfinConnectionParameters connectionParameters,
        JellyfinLibrary library,
        JellyfinShow targetShow,
        Func<JellyfinEpisode, string> getLocalPath,
        bool deepScan,
        CancellationToken cancellationToken)
    {
        try
        {
            async IAsyncEnumerable<Tuple<JellyfinShow, int>> GetSingleShow()
            {
                yield return new Tuple<JellyfinShow, int>(targetShow, 1);
                await Task.CompletedTask;
            }

            return await ScanLibraryWithoutCleanup(
                televisionRepository,
                connectionParameters,
                library,
                getLocalPath,
                GetSingleShow(),
                deepScan,
                cancellationToken);
        }
        catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
        {
            return new ScanCanceled();
        }
    }
}
