using System.Diagnostics;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Scanner.Core.Interfaces;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using Humanizer;
using Microsoft.Extensions.Logging;

namespace ErsatzTV.Scanner.Application.MediaSources;

public class ScanLocalLibraryHandler : IRequestHandler<ScanLocalLibrary, Either<BaseError, string>>
{
    private readonly IConfigElementRepository _configElementRepository;
    private readonly IImageFolderScanner _imageFolderScanner;
    private readonly IScannerProxy _scannerProxy;
    private readonly ILibraryRepository _libraryRepository;
    private readonly ILogger<ScanLocalLibraryHandler> _logger;
    private readonly IMovieFolderScanner _movieFolderScanner;
    private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
    private readonly IOtherVideoFolderScanner _otherVideoFolderScanner;
    private readonly IRemoteStreamFolderScanner _remoteStreamFolderScanner;
    private readonly ISongFolderScanner _songFolderScanner;
    private readonly ITelevisionFolderScanner _televisionFolderScanner;

    public ScanLocalLibraryHandler(
        IScannerProxy scannerProxy,
        ILibraryRepository libraryRepository,
        IConfigElementRepository configElementRepository,
        IMovieFolderScanner movieFolderScanner,
        ITelevisionFolderScanner televisionFolderScanner,
        IMusicVideoFolderScanner musicVideoFolderScanner,
        IOtherVideoFolderScanner otherVideoFolderScanner,
        ISongFolderScanner songFolderScanner,
        IImageFolderScanner imageFolderScanner,
        IRemoteStreamFolderScanner remoteStreamFolderScanner,
        ILogger<ScanLocalLibraryHandler> logger)
    {
        _scannerProxy = scannerProxy;
        _libraryRepository = libraryRepository;
        _configElementRepository = configElementRepository;
        _movieFolderScanner = movieFolderScanner;
        _televisionFolderScanner = televisionFolderScanner;
        _musicVideoFolderScanner = musicVideoFolderScanner;
        _otherVideoFolderScanner = otherVideoFolderScanner;
        _songFolderScanner = songFolderScanner;
        _imageFolderScanner = imageFolderScanner;
        _remoteStreamFolderScanner = remoteStreamFolderScanner;
        _logger = logger;
    }

    public Task<Either<BaseError, string>> Handle(ScanLocalLibrary request, CancellationToken cancellationToken) =>
        Validate(request, cancellationToken)
            .MapT(parameters => PerformScan(parameters, cancellationToken).Map(_ => parameters.LocalLibrary.Name))
            .Bind(v => v.ToEitherAsync());

    private async Task<Unit> PerformScan(RequestParameters parameters, CancellationToken cancellationToken)
    {
        (LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan,
            int libraryRefreshInterval, string baseUrl) = parameters;

        var sw = new Stopwatch();
        sw.Start();

        _scannerProxy.SetBaseUrl(baseUrl);

        var scanned = false;
        var anyFailed = false;

        for (var i = 0; i < localLibrary.Paths.Count; i++)
        {
            LibraryPath libraryPath = localLibrary.Paths[i];

            decimal progressMin = (decimal)i / localLibrary.Paths.Count;
            decimal progressMax = (decimal)(i + 1) / localLibrary.Paths.Count;

            var lastScan = new DateTimeOffset(libraryPath.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
            DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
            if (forceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now)
            {
                scanned = true;

                Either<BaseError, Unit> result = localLibrary.MediaKind switch
                {
                    LibraryMediaKind.Movies =>
                        await _movieFolderScanner.ScanFolder(
                            libraryPath,
                            ffmpegPath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.Shows =>
                        await _televisionFolderScanner.ScanFolder(
                            libraryPath,
                            ffmpegPath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.MusicVideos =>
                        await _musicVideoFolderScanner.ScanFolder(
                            libraryPath,
                            ffmpegPath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.OtherVideos =>
                        await _otherVideoFolderScanner.ScanFolder(
                            libraryPath,
                            ffmpegPath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.Songs =>
                        await _songFolderScanner.ScanFolder(
                            libraryPath,
                            ffprobePath,
                            ffmpegPath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.Images =>
                        await _imageFolderScanner.ScanFolder(
                            libraryPath,
                            ffprobePath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),
                    LibraryMediaKind.RemoteStreams =>
                        await _remoteStreamFolderScanner.ScanFolder(
                            libraryPath,
                            ffprobePath,
                            ffprobePath,
                            progressMin,
                            progressMax,
                            cancellationToken),

                    // returning success here would stamp LastScan as though the library had been
                    // scanned; a local library has no scanner for Mixed and never should
                    _ => BaseError.New(
                        $"Local library {localLibrary.Name} has unsupported media kind {localLibrary.MediaKind}")
                };

                if (result.IsRight)
                {
                    libraryPath.LastScan = DateTime.UtcNow;
                    await _libraryRepository.UpdateLastScan(libraryPath);
                }
                else
                {
                    anyFailed = true;

                    // a failed path now suppresses the library-level scan time below, so without this the
                    // user sees "Never scanned" with nothing explaining why. The remote scanners log the
                    // same way. A cancellation is user-initiated, not a failure, so it's logged separately
                    // at a lower level; genuine errors still log at ERROR.
                    foreach (BaseError error in result.LeftToSeq())
                    {
                        if (error is ScanCanceled)
                        {
                            _logger.LogInformation(
                                "Scan of local library path {Path} was canceled",
                                libraryPath.Path);
                        }
                        else
                        {
                            _logger.LogError(
                                "Error scanning local library path {Path}: {Error}",
                                libraryPath.Path,
                                error);
                        }
                    }
                }
            }

            await _scannerProxy.UpdateProgress(progressMax, cancellationToken);
        }

        sw.Stop();

        // the library-level LastScan drives the "last scanned" / "Never scanned" badge in the API and SPA,
        // and is separate from the per-path LastScan that gates the refresh interval above. Only record it
        // when every path that ran succeeded, mirroring the remote scanners (which set it solely on IsRight).
        if (scanned && !anyFailed)
        {
            localLibrary.LastScan = DateTime.UtcNow;
            await _libraryRepository.UpdateLastScan(localLibrary);
        }

        if (scanned)
        {
            _logger.LogDebug(
                "Scan of library {Name} completed in {Duration}",
                localLibrary.Name,
                sw.Elapsed.Humanize());
        }
        else
        {
            _logger.LogDebug(
                "Skipping unforced scan of local media library {Name}",
                localLibrary.Name);
        }

        return Unit.Default;
    }

    private async Task<Validation<BaseError, RequestParameters>> Validate(
        ScanLocalLibrary request,
        CancellationToken cancellationToken)
    {
        Validation<BaseError, LocalLibrary> libraryResult = await LocalLibraryMustExist(request);
        Validation<BaseError, string> ffprobePathResult = await ValidateFFprobePath(cancellationToken);
        Validation<BaseError, string> ffmpegPathResult = await ValidateFFmpegPath(cancellationToken);
        Validation<BaseError, int> refreshIntervalResult = await ValidateLibraryRefreshInterval(cancellationToken);

        return (libraryResult, ffprobePathResult, ffmpegPathResult, refreshIntervalResult)
            .Apply((library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters(
                library,
                ffprobePath,
                ffmpegPath,
                request.ForceScan,
                libraryRefreshInterval,
                request.BaseUrl));
    }

    private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(ScanLocalLibrary request) =>
        _libraryRepository.GetLibrary(request.LibraryId)
            .Map(maybeLibrary => maybeLibrary.OfType<LocalLibrary>().HeadOrNone())
            .Map(v => v.ToValidation<BaseError>($"Local library {request.LibraryId} does not exist."));

    private Task<Validation<BaseError, string>> ValidateFFprobePath(CancellationToken cancellationToken) =>
        _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath, cancellationToken)
            .FilterT(File.Exists)
            .Map(ffprobePath =>
                ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));

    private Task<Validation<BaseError, string>> ValidateFFmpegPath(CancellationToken cancellationToken) =>
        _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath, cancellationToken)
            .FilterT(File.Exists)
            .Map(ffmpegPath =>
                ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));

    private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval(CancellationToken cancellationToken) =>
        _configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval, cancellationToken)
            .FilterT(lri => lri is >= 0 and < 1_000_000)
            .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));

    private record RequestParameters(
        LocalLibrary LocalLibrary,
        string FFprobePath,
        string FFmpegPath,
        bool ForceScan,
        int LibraryRefreshInterval,
        string BaseUrl);
}
