using ErsatzTV.Core;
using ErsatzTV.Core.Api.Artwork;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Images;
using ErsatzTV.Core.Interfaces.Images;

namespace ErsatzTV.Application.Artworks;

public class UploadArtworkHandler : IRequestHandler<UploadArtwork, Either<BaseError, ArtworkUploadResponseModel>>
{
    private readonly IImageCache _imageCache;
    private readonly IRemoteImageValidator _validator;

    public UploadArtworkHandler(IImageCache imageCache, IRemoteImageValidator validator)
    {
        _imageCache = imageCache;
        _validator = validator;
    }

    public async Task<Either<BaseError, ArtworkUploadResponseModel>> Handle(
        UploadArtwork request,
        CancellationToken cancellationToken)
    {
        // Buffer the upload so we can sniff its true format before storing it. The request body is
        // already bounded by the Kestrel MaxRequestBodySize / the controller's size check, so this
        // is a bounded read.
        byte[] bytes;
        await using (var buffer = new MemoryStream())
        {
            await request.Stream.CopyToAsync(buffer, cancellationToken);
            bytes = buffer.ToArray();
        }

        // Derive the content type from the actual bytes, never from the client-declared value
        // (issue #283 — a spoofed image/png header let a <script> payload be stored and later served
        // as HTML). A payload that isn't a supported raster image is rejected here.
        Option<string> maybeContentType = ImageContentTypes.DetectContentType(bytes);
        if (maybeContentType.IsNone)
        {
            return BaseError.New(
                $"Uploaded file is not a supported image; supported types are: {string.Join(", ", ImageContentTypes.Accepted)}");
        }

        string contentType = maybeContentType.IfNone(string.Empty);

        // One rule: anything entering the logo cache is decode-budget-checked. A supported format is
        // not enough — a small header can declare a multi-gigabyte canvas (a decompression bomb), so
        // reject it here before it lands in the cache. The synthetic upload:// Uri is only for the
        // exception message text. (ersatztv#525)
        using (var probe = new MemoryStream(bytes, writable: false))
        {
            try
            {
                await _validator.Validate(probe, new Uri("upload://artwork"), cancellationToken);
            }
            catch (Exception ex)
            {
                return BaseError.New($"Image cannot be used: {ex.Message}");
            }
        }

        using var toCache = new MemoryStream(bytes, writable: false);
        Either<BaseError, string> maybeFileName = await _imageCache.SaveArtworkToCache(
            toCache,
            request.ArtworkKind);

        return maybeFileName.Map(fileName => new ArtworkUploadResponseModel(
            BuildPath(request.ArtworkKind, fileName),
            contentType));
    }

    // Mirror the on-disk conventions the Blazor editors use so the returned path is a drop-in
    // for ArtworkContentTypeModel.Path: channel logos are addressed as "iptv/logos/{file}"
    // (see ChannelEditor.UploadLogo), watermarks by the bare cache file name (see WatermarkEditor).
    private static string BuildPath(ArtworkKind artworkKind, string fileName) =>
        artworkKind switch
        {
            ArtworkKind.Logo => $"iptv/logos/{fileName}",
            _ => fileName
        };
}
