#region License

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#endregion

using System;
using System.Text;
using System.Threading.Tasks;
using Amazon.Runtime;
using Amazon.Runtime.Internal;
using Amazon.Runtime.Internal.Auth;
using Amazon.Runtime.Internal.Util;

namespace Gremlin.Net.Driver
{
    /// <summary>
    ///     Provides factory methods for built-in request interceptors.
    /// </summary>
    public static class Auth
    {
        /// <summary>
        ///     Returns a request interceptor that adds an HTTP Basic Authentication header.
        ///     The credentials are pre-computed once and set on every request.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns>A request interceptor delegate.</returns>
        public static Func<HttpRequestContext, Task> Basic(string username, string password)
        {
            var encoded = Convert.ToBase64String(
                Encoding.UTF8.GetBytes(username + ":" + password));
            var headerValue = "Basic " + encoded;

            return context =>
            {
                context.Headers["Authorization"] = headerValue;
                return Task.CompletedTask;
            };
        }

        /// <summary>
        ///     Returns a request interceptor that signs requests using AWS Signature Version 4.
        ///     If <paramref name="credentials"/> is null, the default AWS credential chain is
        ///     used and the resolved provider is cached on first use (the provider itself handles
        ///     credential refresh for expiring credentials like STS).
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="service">The service name.</param>
        /// <param name="credentials">
        ///     Optional AWS credentials. When null, the default credential chain is used.
        /// </param>
        /// <returns>A request interceptor delegate.</returns>
        public static Func<HttpRequestContext, Task> Sigv4(
            string region, string service, AWSCredentials? credentials = null)
        {
            // Cache the credential provider once when using the default chain.
            AWSCredentials? cachedProvider = credentials;
            var cacheLock = new object();

            // Create signer and config once — both are stateless and thread-safe
            var signer = new AWS4Signer();
            var clientConfig = new SigningClientConfig
            {
                AuthenticationRegion = region,
                AuthenticationServiceName = service,
            };

            return async context =>
            {
                if (cachedProvider == null)
                {
                    lock (cacheLock)
                    {
                        // FallbackCredentialsFactory only has a sync API, but this runs once.
                        cachedProvider ??= FallbackCredentialsFactory.GetCredentials();
                    }
                }

                // Ensure the body is serialized before signing so we have bytes to hash.
                context.SerializeBody();

                // Use the async path — important for credential providers that perform
                // network I/O (e.g. IMDS on EC2, ECS task role endpoint).
                var immutableCreds = await cachedProvider.GetCredentialsAsync()
                    .ConfigureAwait(false);
                SignRequest(context, immutableCreds, signer, clientConfig);
            };
        }


        private static void SignRequest(HttpRequestContext context,
            ImmutableCredentials credentials, AWS4Signer signer, SigningClientConfig clientConfig)
        {
            // Build a DefaultRequest from the HttpRequestContext for the AWS SDK signer.
            var endpointUri = new Uri(context.Uri.GetLeftPart(UriPartial.Authority));
            var awsRequest = new DefaultRequest(new NullRequest(), clientConfig.AuthenticationServiceName)
            {
                HttpMethod = context.Method,
                Endpoint = endpointUri,
                ResourcePath = context.Uri.AbsolutePath,
                Content = context.Body is byte[] bytes
                    ? bytes
                    : throw new InvalidOperationException(
                        "SigV4 signing requires Body to be byte[]. " +
                        "Ensure SerializeBody() was called before signing."),
                AuthenticationRegion = clientConfig.AuthenticationRegion,
                OverrideSigningServiceName = clientConfig.AuthenticationServiceName,
            };

            // Signed header set: host (derived from the endpoint), x-amz-content-sha256 and
            // x-amz-date, plus x-amz-security-token for session credentials (all set below). The
            // request's own headers (accept, content-type, accept-encoding, ...) are deliberately
            // NOT copied into the signature: the HTTP client may add or rewrite transport-managed
            // headers after this interceptor runs, so a signature covering them would not match
            // the bytes sent.

            // Copy query parameters
            var query = context.Uri.Query;
            if (!string.IsNullOrEmpty(query))
            {
                // Remove leading '?'
                var queryString = query.StartsWith("?") ? query.Substring(1) : query;
                foreach (var param in queryString.Split('&'))
                {
                    if (string.IsNullOrEmpty(param)) continue;
                    var parts = param.Split(new[] { '=' }, 2);
                    var key = Uri.UnescapeDataString(parts[0]);
                    var value = parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : "";
                    awsRequest.Parameters[key] = value;
                }
            }

            // Set content hash header before signing
            var payloadHash = context.GetPayloadHash();
            awsRequest.Headers["x-amz-content-sha256"] = payloadHash;

            // For temporary credentials, add the session token BEFORE signing so it is bound into
            // the signature rather than sent as a weaker present-but-unsigned header.
            if (!string.IsNullOrEmpty(credentials.Token))
            {
                awsRequest.Headers["X-Amz-Security-Token"] = credentials.Token;
            }

            // Sign the request
            signer.Sign(awsRequest, clientConfig, new RequestMetrics(), credentials);

            // Copy signed headers back to context. Cherry-pick the known SigV4 headers
            // because the .NET Dictionary is case-sensitive and the AWS SDK may use
            // different casing than what interceptors expect.
            context.Headers["Host"] = endpointUri.Host;
            if (awsRequest.Headers.ContainsKey("Authorization"))
            {
                context.Headers["Authorization"] = awsRequest.Headers["Authorization"];
            }
            if (awsRequest.Headers.ContainsKey("X-Amz-Date"))
            {
                context.Headers["X-Amz-Date"] = awsRequest.Headers["X-Amz-Date"];
            }
            context.Headers["x-amz-content-sha256"] = payloadHash;

            // Copy back the session token that was signed above (for temporary credentials), so
            // the header on the wire matches exactly what the signature covered.
            if (awsRequest.Headers.ContainsKey("X-Amz-Security-Token"))
            {
                context.Headers["X-Amz-Security-Token"] = awsRequest.Headers["X-Amz-Security-Token"];
            }
        }

        /// <summary>
        ///     A minimal AmazonWebServiceRequest implementation required by DefaultRequest.
        /// </summary>
        private class NullRequest : AmazonWebServiceRequest
        {
        }

        /// <summary>
        ///     A minimal ClientConfig implementation for SigV4 signing.
        /// </summary>
        private class SigningClientConfig : ClientConfig
        {
            public override string RegionEndpointServiceName => "execute-api";
            public override string ServiceVersion => "2024-01-01";
            public override string UserAgent => "gremlin-dotnet";
        }
    }
}
