using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Images;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Repositories;
using Winista.Mime;

namespace ErsatzTV.Application.Images;

public class
    GetCachedImagePathHandler : IRequestHandler<GetCachedImagePath, Either<BaseError, CachedImagePathViewModel>>
{
    private static readonly MimeTypes MimeTypes = new();
    private readonly IConfigElementRepository _configElementRepository;
    private readonly IFFmpegProcessService _ffmpegProcessService;
    private readonly IImageCache _imageCache;

    public GetCachedImagePathHandler(
        IImageCache imageCache,
        IFFmpegProcessService ffmpegProcessService,
        IConfigElementRepository configElementRepository)
    {
        _imageCache = imageCache;
        _ffmpegProcessService = ffmpegProcessService;
        _configElementRepository = configElementRepository;
    }

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

    private async Task<Either<BaseError, CachedImagePathViewModel>> Handle(
        string ffmpegPath,
        GetCachedImagePath request,
        CancellationToken cancellationToken)
    {
        try
        {
            string mimeType;

            string cachePath = _imageCache.GetPathForImage(
                request.FileName,
                request.ArtworkKind,
                Optional(request.MaxHeight));

            if (cachePath == null)
            {
                return BaseError.New("Failed to generate cache path for image");
            }

            if (!File.Exists(cachePath))
            {
                if (request.MaxHeight.HasValue)
                {
                    string baseFolder = Path.GetDirectoryName(cachePath);
                    if (baseFolder != null && !Directory.Exists(baseFolder))
                    {
                        Directory.CreateDirectory(baseFolder);
                    }

                    // ffmpeg needs the extension to determine the output codec
                    string withExtension = cachePath + ".jpg";

                    string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None);

                    Command process = await _ffmpegProcessService.ResizeImage(
                        ffmpegPath,
                        originalPath,
                        withExtension,
                        request.MaxHeight.Value,
                        cancellationToken);

                    CommandResult resize = await process.ExecuteAsync(cancellationToken);

                    if (resize.ExitCode != 0)
                    {
                        return BaseError.New($"Failed to resize image; exit code {resize.ExitCode}");
                    }

                    File.Move(withExtension, cachePath);

                    mimeType = "image/jpeg";
                }
                else
                {
                    return BaseError.New($"Artwork does not exist on disk at {cachePath}");
                }
            }
            else
            {
                // Always derive the type from the stored file — never from a client-supplied value
                // (issue #283 — the old ?contentType= reflection was the stored-XSS sink). Clamp the
                // sniffed type to the image allow-list so a file whose bytes are not an accepted image
                // (a legacy cache entry poisoned before the upload sniff landed, or a hypothetical
                // polyglot) is served as a non-renderable download, never as HTML/script.
                string sniffed = MimeTypes.GetMimeTypeFromFile(cachePath)?.Name;
                mimeType = ImageContentTypes.IsAccepted(sniffed) ? sniffed : "application/octet-stream";
            }

            return new CachedImagePathViewModel(cachePath, mimeType);
        }
        catch (Exception ex)
        {
            return BaseError.New(ex.Message);
        }
    }

    private async Task<Validation<BaseError, string>> Validate(CancellationToken cancellationToken) =>
        await ValidateFFmpegPath(cancellationToken);

    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"));
}
