using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.MediaSources;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Libraries.Mapper;

namespace ErsatzTV.Application.Libraries;

public class CreateLocalLibraryHandler : LocalLibraryHandlerBase,
    IRequestHandler<CreateLocalLibrary, Either<BaseError, LocalLibraryViewModel>>
{
    private readonly IDbContextFactory<TvContext> _dbContextFactory;
    private readonly IEntityLocker _entityLocker;
    private readonly IFileSystem _fileSystem;
    private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;

    public CreateLocalLibraryHandler(
        ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,
        IEntityLocker entityLocker,
        IFileSystem fileSystem,
        IDbContextFactory<TvContext> dbContextFactory)
    {
        _scannerWorkerChannel = scannerWorkerChannel;
        _entityLocker = entityLocker;
        _fileSystem = fileSystem;
        _dbContextFactory = dbContextFactory;
    }

    public async Task<Either<BaseError, LocalLibraryViewModel>> Handle(
        CreateLocalLibrary request,
        CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        Validation<BaseError, LocalLibrary> validation = await Validate(_fileSystem, dbContext, request);
        return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
    }

    private async Task<LocalLibraryViewModel> PersistLocalLibrary(
        TvContext dbContext,
        LocalLibrary localLibrary)
    {
        await dbContext.LocalLibraries.AddAsync(localLibrary);
        await dbContext.SaveChangesAsync();

        if (_entityLocker.LockLibrary(localLibrary.Id))
        {
            try
            {
                await _scannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(localLibrary.Id));
            }
            catch
            {
                // the scanner only unlocks when it receives the message; if the enqueue fails
                // after we acquired the lock, release it here or it is held forever.
                _entityLocker.UnlockLibrary(localLibrary.Id);
                throw;
            }
        }

        return ProjectToViewModel(localLibrary);
    }

    private static Task<Validation<BaseError, LocalLibrary>> Validate(
        IFileSystem fileSystem,
        TvContext dbContext,
        CreateLocalLibrary request) =>
        MediaSourceMustExist(dbContext, request)
            .BindT(localLibrary => NameMustBeValid(request, localLibrary))
            .BindT(MediaKindMustBeSupportedLocally)
            .BindT(localLibrary => PathsMustBeValid(dbContext, localLibrary))
            .BindT(localLibrary => NewPathsMustExist(fileSystem, localLibrary));

    /// <summary>
    /// Mixed is only ever produced for remote (Jellyfin) libraries, where the media server classifies
    /// each item for us. No local folder scanner handles it, so a local Mixed library would fail every
    /// scan forever. The API takes a raw LibraryMediaKind, so this must be enforced here rather than
    /// left to the SPA's media-kind options.
    /// </summary>
    private static Validation<BaseError, LocalLibrary> MediaKindMustBeSupportedLocally(
        LocalLibrary localLibrary) =>
        localLibrary.MediaKind is LibraryMediaKind.Mixed
            ? BaseError.New(
                "Local libraries cannot use the Mixed media kind; it is only valid for Jellyfin libraries.")
            : localLibrary;

    private static Task<Validation<BaseError, LocalLibrary>> MediaSourceMustExist(
        TvContext dbContext,
        CreateLocalLibrary request) =>
        dbContext.LocalMediaSources
            .OrderBy(lms => lms.Id)
            .FirstOrDefaultAsync()
            .Map(Optional)
            .MapT(lms => new LocalLibrary
            {
                Name = request.Name,
                Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(),
                MediaKind = request.MediaKind,
                MediaSourceId = lms.Id
            })
            .Map(o => o.ToValidation<BaseError>("LocalMediaSource does not exist."));
}
