using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;

namespace ErsatzTV.Application.Channels;

public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>>
{
    private readonly IDbContextFactory<TvContext> _dbContextFactory;
    private readonly IFileSystem _fileSystem;
    private readonly ISearchTargets _searchTargets;
    private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;

    public DeleteChannelHandler(
        ChannelWriter<IBackgroundServiceRequest> workerChannel,
        IDbContextFactory<TvContext> dbContextFactory,
        IFileSystem fileSystem,
        ISearchTargets searchTargets)
    {
        _workerChannel = workerChannel;
        _dbContextFactory = dbContextFactory;
        _fileSystem = fileSystem;
        _searchTargets = searchTargets;
    }

    public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        Option<Channel> maybeChannel = await dbContext.Channels
            .SelectOneAsync(c => c.Id, c => c.Id == request.ChannelId, cancellationToken);

        return await maybeChannel.Match(
            Some: async channel =>
            {
                await DoDeletion(dbContext, channel, cancellationToken);
                return Right<BaseError, Unit>(Unit.Default);
            },
            None: () => Task.FromResult(
                Left<BaseError, Unit>(new NotFoundError($"Channel {request.ChannelId} does not exist."))));
    }

    private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
    {
        // Delete the guide cache file through the filesystem abstraction (so it's observable under a
        // MockFileSystem) and BEFORE the commit: deleting after commit orphans {number}.xml if the
        // process crashes in between (nothing reaps it, and GetChannelGuideHandler serves everything
        // in the cache folder). The guide xml is regenerable on demand, so losing it pre-commit is safe (#254).
        string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
        if (_fileSystem.File.Exists(cacheFile))
        {
            _fileSystem.File.Delete(cacheFile);
        }

        int channelId = channel.Id;
        dbContext.Channels.Remove(channel);
        await dbContext.SaveChangesAsync(cancellationToken);

        // Clean up the system-owned weighted-auto-tune artifacts this channel created (#425): the
        // MultiCollection (its cascade removes the now-dangling flood schedule item) and its per-source
        // SmartCollections (cascade removes their join rows). Null OwnedByChannelId = a user collection, left
        // untouched. Non-weighted (#69 single-SmartCollection) auto-tune channels set no ownership, so their
        // pre-existing orphan-on-delete behavior is unchanged.
        await dbContext.MultiCollections
            .Where(mc => mc.OwnedByChannelId == channelId)
            .ExecuteDeleteAsync(cancellationToken);
        await dbContext.SmartCollections
            .Where(sc => sc.OwnedByChannelId == channelId)
            .ExecuteDeleteAsync(cancellationToken);

        _searchTargets.SearchTargetsChanged();

        // refresh channel list to remove channel that has no playout — post-commit side effect runs on
        // CancellationToken.None so a late request cancellation can't abort it after the delete committed (#254)
        await _workerChannel.WriteAsync(new RefreshChannelList(), CancellationToken.None);

        return Unit.Default;
    }
}
