using Microsoft.Extensions.Options;
using WishNinja.Configuration;

namespace WishNinja.Services;

public record ImageSaveResult(bool Succeeded, string? FileName = null, string? Error = null);

/// <summary>
/// Saves uploaded item images (file uploads and clipboard-pasted blobs converge here) into the
/// uploads directory under the data path. Validates content type and size; names files with GUIDs.
/// </summary>
public class ImageService
{
    private static readonly Dictionary<string, string> AllowedTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        ["image/png"] = ".png",
        ["image/jpeg"] = ".jpg",
        ["image/webp"] = ".webp",
        ["image/gif"] = ".gif",
    };

    private readonly UploadOptions _uploads;
    private readonly IHttpClientFactory _httpClientFactory;
    public string UploadsDirectory { get; }

    public ImageService(IOptions<WishNinjaOptions> options, IHttpClientFactory httpClientFactory)
    {
        _uploads = options.Value.Uploads;
        _httpClientFactory = httpClientFactory;
        UploadsDirectory = Path.GetFullPath(Path.Combine(options.Value.DataPath, "uploads"));
        Directory.CreateDirectory(UploadsDirectory);
    }

    public bool IsAllowedContentType(string? contentType) =>
        contentType is not null && AllowedTypes.ContainsKey(contentType);

    public long MaxBytes => _uploads.MaxBytes;

    /// <summary>Validates and writes a stream to disk, returning the stored relative file name.</summary>
    public async Task<ImageSaveResult> SaveAsync(Stream stream, string contentType, CancellationToken ct = default)
    {
        if (!AllowedTypes.TryGetValue(contentType, out var ext))
            return new ImageSaveResult(false, Error: "Unsupported image type. Use PNG, JPEG, WebP or GIF.");

        var fileName = $"{Guid.NewGuid():N}{ext}";
        var fullPath = Path.Combine(UploadsDirectory, fileName);

        // Cap the write at MaxBytes + 1 so oversized uploads fail without buffering the whole thing.
        await using var output = File.Create(fullPath);
        var buffer = new byte[81920];
        long total = 0;
        int read;
        while ((read = await stream.ReadAsync(buffer, ct)) > 0)
        {
            total += read;
            if (total > _uploads.MaxBytes)
            {
                output.Close();
                File.Delete(fullPath);
                return new ImageSaveResult(false, Error: $"Image exceeds the {_uploads.MaxBytes / (1024 * 1024)} MB limit.");
            }
            await output.WriteAsync(buffer.AsMemory(0, read), ct);
        }

        return new ImageSaveResult(true, FileName: fileName);
    }

    /// <summary>
    /// Downloads a remote image and stores it locally (so the wishlist never depends on the
    /// external URL staying up). Validates scheme, content type and size.
    /// </summary>
    public async Task<ImageSaveResult> SaveFromUrlAsync(string url, CancellationToken ct = default)
    {
        if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out var uri) ||
            (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
            return new ImageSaveResult(false, Error: "Enter a valid http(s) image URL.");

        var client = _httpClientFactory.CreateClient("image-fetch");
        try
        {
            using var resp = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, ct);
            if (!resp.IsSuccessStatusCode)
                return new ImageSaveResult(false, Error: $"Couldn't fetch image (HTTP {(int)resp.StatusCode}).");

            // Prefer the served content type; fall back to guessing from the URL's extension.
            var contentType = resp.Content.Headers.ContentType?.MediaType;
            if (!IsAllowedContentType(contentType))
                contentType = GuessContentTypeFromPath(uri.AbsolutePath);
            if (!IsAllowedContentType(contentType))
                return new ImageSaveResult(false, Error: "That URL didn't return a supported image (PNG, JPEG, WebP or GIF).");

            if (resp.Content.Headers.ContentLength is long len && len > _uploads.MaxBytes)
                return new ImageSaveResult(false, Error: $"Image exceeds the {_uploads.MaxBytes / (1024 * 1024)} MB limit.");

            await using var stream = await resp.Content.ReadAsStreamAsync(ct);
            return await SaveAsync(stream, contentType!, ct);
        }
        catch (Exception)
        {
            return new ImageSaveResult(false, Error: "Couldn't download an image from that URL.");
        }
    }

    private static string? GuessContentTypeFromPath(string path)
    {
        var ext = Path.GetExtension(path).ToLowerInvariant();
        return ext switch
        {
            ".png" => "image/png",
            ".jpg" or ".jpeg" => "image/jpeg",
            ".webp" => "image/webp",
            ".gif" => "image/gif",
            _ => null,
        };
    }

    public void Delete(string? fileName)
    {
        if (string.IsNullOrWhiteSpace(fileName)) return;
        // Guard against path traversal — only operate on a bare file name.
        var safe = Path.GetFileName(fileName);
        var path = Path.Combine(UploadsDirectory, safe);
        if (File.Exists(path)) File.Delete(path);
    }
}
