#nullable enable
using System.Security.Cryptography;
using System.Text;
using Baya.Application.Contracts.Common;
using Microsoft.Extensions.Options;

namespace Baya.Infrastructure.CrossCutting.Seams.Real;

/// <summary>
/// Real <see cref="IObjectStorage"/> over an S3-compatible store — MinIO / AWS S3 / ArvanCloud
/// (refinement-phase-8, 5.5). Selected by <c>Seams:ObjectStorage:Provider = s3</c>. Makes the b6 signed-URL
/// document contract and the REQ-006 avatar path real (local disk + <c>file://</c> today). Server-side
/// put/get/delete are AWS-SigV4-authenticated requests; <see cref="GetUrl"/> returns a time-limited
/// <b>presigned GET</b> so a document is fetched directly from storage without proxying bytes through the API.
///
/// <para>Signing is implemented directly against SigV4 (HMAC-SHA256, all in the BCL) so no AWS SDK dependency is
/// pulled in. Uploads use <c>UNSIGNED-PAYLOAD</c> so a blob stream is never buffered to hash it.</para>
/// </summary>
public sealed class S3ObjectStorage : IObjectStorage
{
    private const string Algorithm = "AWS4-HMAC-SHA256";
    private const string UnsignedPayload = "UNSIGNED-PAYLOAD";
    private const string Service = "s3";

    private readonly HttpClient _httpClient;
    private readonly ObjectStorageOptions _options;
    private readonly Uri _serviceUri;

    public S3ObjectStorage(HttpClient httpClient, IOptions<SeamOptions> options)
    {
        _httpClient = httpClient;
        _options = options.Value.ObjectStorage;
        _serviceUri = new Uri(_options.ServiceUrl.TrimEnd('/'), UriKind.Absolute);
    }

    public async ValueTask PutAsync(string key, Stream content, string contentType, CancellationToken cancellationToken = default)
    {
        var (uri, host, canonicalUri) = ResolveObject(key);
        using var request = new HttpRequestMessage(HttpMethod.Put, uri)
        {
            Content = new StreamContent(content),
        };
        request.Content.Headers.TryAddWithoutValidation("Content-Type", contentType);
        SignRequest(request, "PUT", host, canonicalUri);

        using var response = await _httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();
    }

    public async ValueTask<Stream?> GetAsync(string key, CancellationToken cancellationToken = default)
    {
        var (uri, host, canonicalUri) = ResolveObject(key);
        using var request = new HttpRequestMessage(HttpMethod.Get, uri);
        SignRequest(request, "GET", host, canonicalUri);

        var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            response.Dispose();
            return null;
        }

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStreamAsync(cancellationToken);
    }

    public async ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default)
    {
        var (uri, host, canonicalUri) = ResolveObject(key);
        using var request = new HttpRequestMessage(HttpMethod.Delete, uri);
        SignRequest(request, "DELETE", host, canonicalUri);

        using var response = await _httpClient.SendAsync(request, cancellationToken);
        if (response.StatusCode != System.Net.HttpStatusCode.NotFound)
            response.EnsureSuccessStatusCode();
    }

    /// <summary>A presigned GET URL valid for <c>PresignExpirySeconds</c> — the real form of the b6 signed-URL contract.</summary>
    public string GetUrl(string key) => Presign(key, TimeSpan.FromSeconds(_options.PresignExpirySeconds));

    // ---- URL / object resolution -------------------------------------------------------------------------

    private (Uri Uri, string Host, string CanonicalUri) ResolveObject(string key)
    {
        var encodedKey = EncodeKey(key);
        if (_options.UsePathStyle)
        {
            var host = _serviceUri.Authority;
            var canonicalUri = $"/{_options.Bucket}/{encodedKey}";
            return (new Uri($"{_serviceUri.Scheme}://{host}{canonicalUri}"), host, canonicalUri);
        }
        else
        {
            var host = $"{_options.Bucket}.{_serviceUri.Authority}";
            var canonicalUri = $"/{encodedKey}";
            return (new Uri($"{_serviceUri.Scheme}://{host}{canonicalUri}"), host, canonicalUri);
        }
    }

    // ---- SigV4: authenticated request (header auth) ------------------------------------------------------

    private void SignRequest(HttpRequestMessage request, string method, string host, string canonicalUri)
    {
        var now = DateTime.UtcNow;
        var amzDate = now.ToString("yyyyMMddTHHmmssZ");
        var dateStamp = now.ToString("yyyyMMdd");
        var scope = $"{dateStamp}/{_options.Region}/{Service}/aws4_request";

        var canonicalHeaders = $"host:{host}\nx-amz-content-sha256:{UnsignedPayload}\nx-amz-date:{amzDate}\n";
        const string signedHeaders = "host;x-amz-content-sha256;x-amz-date";

        var canonicalRequest = string.Join('\n',
            method, canonicalUri, string.Empty, canonicalHeaders, signedHeaders, UnsignedPayload);
        var stringToSign = string.Join('\n', Algorithm, amzDate, scope, Hex(Sha256(canonicalRequest)));
        var signature = Hex(Hmac(SigningKey(dateStamp), stringToSign));

        request.Headers.TryAddWithoutValidation("x-amz-date", amzDate);
        request.Headers.TryAddWithoutValidation("x-amz-content-sha256", UnsignedPayload);
        request.Headers.TryAddWithoutValidation("Authorization",
            $"{Algorithm} Credential={_options.AccessKey}/{scope}, SignedHeaders={signedHeaders}, Signature={signature}");
    }

    // ---- SigV4: presigned URL (query auth) ---------------------------------------------------------------

    private string Presign(string key, TimeSpan expiry)
    {
        var (uri, host, canonicalUri) = ResolveObject(key);
        var now = DateTime.UtcNow;
        var amzDate = now.ToString("yyyyMMddTHHmmssZ");
        var dateStamp = now.ToString("yyyyMMdd");
        var scope = $"{dateStamp}/{_options.Region}/{Service}/aws4_request";

        // Query keys must be sorted; each value URL-encoded (RFC3986). host is the only signed header.
        var query = new SortedDictionary<string, string>(StringComparer.Ordinal)
        {
            ["X-Amz-Algorithm"] = Algorithm,
            ["X-Amz-Credential"] = $"{_options.AccessKey}/{scope}",
            ["X-Amz-Date"] = amzDate,
            ["X-Amz-Expires"] = ((int)expiry.TotalSeconds).ToString(),
            ["X-Amz-SignedHeaders"] = "host",
        };
        var canonicalQuery = string.Join('&', query.Select(kvp => $"{RfcEncode(kvp.Key)}={RfcEncode(kvp.Value)}"));

        var canonicalHeaders = $"host:{host}\n";
        var canonicalRequest = string.Join('\n',
            "GET", canonicalUri, canonicalQuery, canonicalHeaders, "host", UnsignedPayload);
        var stringToSign = string.Join('\n', Algorithm, amzDate, scope, Hex(Sha256(canonicalRequest)));
        var signature = Hex(Hmac(SigningKey(dateStamp), stringToSign));

        return $"{uri.Scheme}://{host}{canonicalUri}?{canonicalQuery}&X-Amz-Signature={signature}";
    }

    // ---- crypto primitives -------------------------------------------------------------------------------

    private byte[] SigningKey(string dateStamp)
    {
        var kDate = Hmac(Encoding.UTF8.GetBytes($"AWS4{_options.SecretKey}"), dateStamp);
        var kRegion = Hmac(kDate, _options.Region);
        var kService = Hmac(kRegion, Service);
        return Hmac(kService, "aws4_request");
    }

    private static byte[] Hmac(byte[] key, string data) => HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(data));
    private static byte[] Sha256(string data) => SHA256.HashData(Encoding.UTF8.GetBytes(data));
    private static string Hex(byte[] bytes) => Convert.ToHexStringLower(bytes);

    // Encode an object key preserving '/' as a path separator (each segment RFC3986-encoded).
    private static string EncodeKey(string key)
    {
        var normalized = key.Replace('\\', '/').TrimStart('/');
        return string.Join('/', normalized
            .Split('/', StringSplitOptions.RemoveEmptyEntries)
            .Where(s => s != "." && s != "..")
            .Select(RfcEncode));
    }

    // RFC 3986 unreserved set — AWS canonicalization encodes everything else.
    private static string RfcEncode(string value)
    {
        const string unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~";
        var sb = new StringBuilder(value.Length);
        foreach (var b in Encoding.UTF8.GetBytes(value))
        {
            var c = (char)b;
            if (unreserved.IndexOf(c) >= 0) sb.Append(c);
            else sb.Append('%').Append(b.ToString("X2"));
        }
        return sb.ToString();
    }
}
