using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace GmRelay.Web.Services.Portfolio.Covers;

public sealed class LocalPortfolioCoverStorage : IPortfolioCoverStorage
{
    public const long MaxBytes = 5 * 1024 * 1024;

    private static readonly Regex SafeKeyPattern = new(
        "^[a-f0-9]{32}\\.(jpg|png|webp)$",
        RegexOptions.Compiled | RegexOptions.CultureInvariant);

    private static readonly byte[] JpegSignature = [0xFF, 0xD8, 0xFF];
    private static readonly byte[] PngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
    private static readonly byte[] RiffMarker = "RIFF"u8.ToArray();
    private static readonly byte[] WebpMarker = "WEBP"u8.ToArray();

    private readonly string _storagePath;
    private readonly ILogger<LocalPortfolioCoverStorage> _logger;

    public LocalPortfolioCoverStorage(PortfolioCoverStorageOptions options)
        : this(options, logger: null)
    {
    }

    public LocalPortfolioCoverStorage(PortfolioCoverStorageOptions options, ILogger<LocalPortfolioCoverStorage>? logger)
    {
        ArgumentNullException.ThrowIfNull(options);
        if (string.IsNullOrWhiteSpace(options.StoragePath))
        {
            throw new InvalidOperationException("PortfolioCovers:StoragePath must be configured.");
        }

        _storagePath = options.StoragePath;
        _logger = logger ?? NullLogger<LocalPortfolioCoverStorage>.Instance;
    }

    public async Task<PortfolioCoverUploadResult> SaveAsync(
        Stream content,
        string contentType,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(content);
        if (string.IsNullOrWhiteSpace(contentType))
        {
            throw new InvalidOperationException("Content type must be provided.");
        }

        var extension = NormalizeExtension(contentType);

        // Buffer the stream so we can reject oversize uploads before writing to disk
        // and so we have the bytes we need for signature validation.
        await using var buffer = new MemoryStream();
        await content.CopyToAsync(buffer, cancellationToken);
        if (buffer.Length > MaxBytes)
        {
            throw new InvalidOperationException(
                $"Cover image exceeds the {MaxBytes}-byte size limit.");
        }

        var signature = buffer.GetBuffer();
        var signatureLength = (int)buffer.Length;
        ValidateSignature(extension, signature, signatureLength);

        Directory.CreateDirectory(_storagePath);
        var finalName = Guid.NewGuid().ToString("N") + extension;
        var finalPath = Path.Combine(_storagePath, finalName);
        var tempPath = finalPath + ".tmp";

        try
        {
            await using (var tempStream = new FileStream(
                tempPath,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None))
            {
                buffer.Position = 0;
                await buffer.CopyToAsync(tempStream, cancellationToken);
                await tempStream.FlushAsync(cancellationToken);
            }

            File.Move(tempPath, finalPath, overwrite: false);
        }
        catch
        {
            TryDelete(tempPath);
            throw;
        }

        return new PortfolioCoverUploadResult(finalName, ResolveContentType(extension));
    }

    public Task DeleteIfExistsAsync(string storageKey, CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(storageKey);
        EnsureSafeKey(storageKey);

        var path = Path.Combine(_storagePath, storageKey);
        TryDelete(path);
        return Task.CompletedTask;
    }

    public string GetPublicPath(string storageKey)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(storageKey);
        return "/portfolio-covers/" + Uri.EscapeDataString(storageKey);
    }

    private static void ValidateSignature(string extension, byte[] data, int length)
    {
        var isValid = extension switch
        {
            ".jpg" => StartsWith(data, length, JpegSignature),
            ".png" => StartsWith(data, length, PngSignature),
            ".webp" => StartsWith(data, length, RiffMarker)
                && ContainsAt(data, RiffMarker.Length + 4, WebpMarker),
            _ => false
        };

        if (!isValid)
        {
            throw new InvalidOperationException(
                $"Cover signature does not match the declared content type.");
        }
    }

    private static bool StartsWith(byte[] data, int length, byte[] prefix)
    {
        if (length < prefix.Length)
        {
            return false;
        }

        for (var i = 0; i < prefix.Length; i++)
        {
            if (data[i] != prefix[i])
            {
                return false;
            }
        }

        return true;
    }

    private static bool ContainsAt(byte[] data, int offset, byte[] needle)
    {
        if (offset + needle.Length > data.Length)
        {
            return false;
        }

        for (var i = 0; i < needle.Length; i++)
        {
            if (data[offset + i] != needle[i])
            {
                return false;
            }
        }

        return true;
    }

    private static string NormalizeExtension(string contentType)
    {
        var normalized = contentType.Trim().ToLowerInvariant();
        return normalized switch
        {
            "image/jpeg" or "image/jpg" => ".jpg",
            "image/png" => ".png",
            "image/webp" => ".webp",
            _ => throw new InvalidOperationException(
                $"Unsupported cover content type: '{contentType}'.")
        };
    }

    private static string ResolveContentType(string extension) => extension switch
    {
        ".jpg" => "image/jpeg",
        ".png" => "image/png",
        ".webp" => "image/webp",
        _ => "application/octet-stream"
    };

    private static void EnsureSafeKey(string storageKey)
    {
        if (!SafeKeyPattern.IsMatch(storageKey))
        {
            throw new InvalidOperationException("Cover storage key is not in the expected format.");
        }
    }

    private void TryDelete(string path)
    {
        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to delete cover file '{Path}'.", path);
        }
    }
}
