/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 * 
 *  http://aws.amazon.com/apache2.0
 * 
 * or in the "license" file accompanying this file. This file 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.
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Amazon.Util;
using Amazon.Runtime.Internal.Util;

#pragma warning disable 1591

namespace Amazon.Runtime.Internal.Auth
{
    public class S3Signer : AbstractAWSSigner
    {
        public delegate void RegionDetectionUpdater(IRequest request);

        private readonly bool _useSigV4;
        private readonly RegionDetectionUpdater _regionDetector;

        /// <summary>
        /// S3 signer constructor
        /// </summary>
        public S3Signer() :
            this(true, null)
        {
        }

        /// <summary>
        /// S3 signer constructor
        /// </summary>
        public S3Signer(bool useSigV4, RegionDetectionUpdater regionDetector)
        {
            _useSigV4 = useSigV4;
            _regionDetector = regionDetector;
        }

        public override ClientProtocol Protocol
        {
            get { return ClientProtocol.RestProtocol; }
        }

        public override void Sign(IRequest request, IClientConfig clientConfig, RequestMetrics metrics, string awsAccessKeyId, string awsSecretAccessKey)
        {
            Sign(request, clientConfig, metrics, new ImmutableCredentials(awsAccessKeyId, awsSecretAccessKey, ""));
        }

        public override void Sign(IRequest request, IClientConfig clientConfig, RequestMetrics metrics, ImmutableCredentials credentials)
        {
            var signer = SelectSigner(this, _useSigV4, request, clientConfig);
            var aws4Signer = signer as AWS4Signer;
            var aws4aSigner = signer as AWS4aSignerCRTWrapper;
            var useV4 = aws4Signer != null;
            var useV4a = aws4aSigner != null;

            if (useV4a)
            {
                var signingResult = aws4aSigner.SignRequest(request, clientConfig, metrics, credentials);
                if (request.UseChunkEncoding)
                {
                    request.AWS4aSignerResult = signingResult;
                }
            }
            else if (useV4)
            {
                _regionDetector?.Invoke(request);
                var signingResult = aws4Signer.SignRequest(request, clientConfig, metrics, credentials.AccessKey, credentials.SecretKey);
                request.Headers[HeaderKeys.AuthorizationHeader] = signingResult.ForAuthorizationHeader;
                if (request.UseChunkEncoding)
                    request.AWS4SignerResult = signingResult;
            }
            else
            {
                SignRequest(request, metrics, credentials.AccessKey, credentials.SecretKey);
            }
        }

        public static void SignRequest(IRequest request, RequestMetrics metrics, string awsAccessKeyId, string awsSecretAccessKey)
        {
            request.Headers[HeaderKeys.XAmzDateHeader] = AWSSDKUtils.FormattedCurrentTimestampRFC822;

            var stringToSign = BuildStringToSign(request);
            metrics.AddProperty(Metric.StringToSign, stringToSign);
            var auth = CryptoUtilFactory.CryptoInstance.HMACSign(stringToSign, awsSecretAccessKey, SigningAlgorithm.HmacSHA1);
            var authorization = string.Concat("AWS ", awsAccessKeyId, ":", auth);
            request.Headers[HeaderKeys.AuthorizationHeader] = authorization;
        }

        static string BuildStringToSign(IRequest request)
        {
            var sb = new StringBuilder("", 256);

            sb.Append(request.HttpMethod);
            sb.Append("\n");

            var headers = request.Headers;
            var parameters = request.Parameters;

            if (headers != null)
            {
                string value = null;
                if (headers.ContainsKey(HeaderKeys.ContentMD5Header) && !String.IsNullOrEmpty(value = headers[HeaderKeys.ContentMD5Header]))
                {
                    sb.Append(value);
                }
                sb.Append("\n");

                if (parameters.ContainsKey("ContentType"))
                {
                    sb.Append(parameters["ContentType"]);
                }
                else if (headers.ContainsKey(HeaderKeys.ContentTypeHeader))
                {
                    sb.Append(headers[HeaderKeys.ContentTypeHeader]);
                }
                sb.Append("\n");
            }
            else
            {
                // The headers are null, but we still need to append
                // the 2 newlines that are required by S3.
                // Without these, S3 rejects the signature.
                sb.Append("\n\n");
            }

            if (parameters.ContainsKey("Expires"))
            {
                sb.Append(parameters["Expires"]);
                if (headers != null)
                    headers.Remove(HeaderKeys.XAmzDateHeader);
            }

            IDictionary<string, string> headersAndParameters = new Dictionary<string, string>(headers);
            foreach (var pair in parameters)
            {
                // If there's a key that's both a header and a parameter then the header will take precedence.
                if (!headersAndParameters.ContainsKey(pair.Key))
                    headersAndParameters.Add(pair.Key, pair.Value);
            }
            sb.Append("\n");
            sb.Append(BuildCanonicalizedHeaders(headersAndParameters));

            var canonicalizedResource = BuildCanonicalizedResource(request);
            if (!string.IsNullOrEmpty(canonicalizedResource))
            {
                sb.Append(canonicalizedResource);
            }

            return sb.ToString();
        }

        static string BuildCanonicalizedHeaders(IDictionary<string, string> headers)
        {
            // Refer "Constructing the CanonicalizedAmzHeaders element" section at https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html.
            var sb = new StringBuilder(256);
            
            // Steps 1 and Steps 2 requires all headers to be in lowercase and lexicographically sorted by header name. StringComparer.OrdinalIgnoreCase incorrectly places '_' after lowercase chracters.
            foreach (var key in headers.Keys.OrderBy(x => x.ToLowerInvariant(), StringComparer.Ordinal))
            {
                var lowerKey = key.ToLowerInvariant();
                if (!lowerKey.StartsWith("x-amz-", StringComparison.Ordinal))
                    continue;

                // Step 5: Trim any spaces around the colon in the header (based on testing spaces at the end of value also needs to be removed).
                sb.Append(String.Concat(lowerKey, ":", headers[key]?.Trim(), "\n"));
            }

            return sb.ToString();
        }

        private static readonly HashSet<string> SignableParameters = new HashSet<string>
        (
            new[]
            {
                "response-content-type",
                "response-content-language",
                "response-expires",
                "response-cache-control",
                "response-content-disposition",
                "response-content-encoding"
            },
            StringComparer.OrdinalIgnoreCase
        );

        //This is a list of sub resources that S3 does not expect to be signed
        //and thus have to be excluded from the signer. This is only applicable to S3SigV2 signer
        //id:- subresource belongs to analytics,inventory and metrics S3 APIs
        private static readonly HashSet<string> SubResourcesSigningExclusion = new HashSet<string>
        (
            new[]
            {
                "id"
            },
            StringComparer.OrdinalIgnoreCase
        );

        static string BuildCanonicalizedResource(IRequest request)
        {
            // CanonicalResourcePrefix will hold the bucket name if we switched to virtual host addressing
            // during request preprocessing (where it would have been removed from ResourcePath)
            var sb = new StringBuilder(request.CanonicalResourcePrefix);
            sb.Append(!string.IsNullOrEmpty(request.ResourcePath)
                                ? AWSSDKUtils.ResolveResourcePath(request.ResourcePath, request.PathResources)
                                : "/");

            // form up the set of all subresources and specific query parameters that must be 
            // included in the canonical resource, then append them ordered by key to the 
            // canonicalization
            var resourcesToSign = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            if (request.SubResources.Count > 0)
            {
                foreach (var subResource in request.SubResources)
                {
                    if (!SubResourcesSigningExclusion.Contains(subResource.Key))
                    {
                        resourcesToSign.Add(subResource.Key, subResource.Value);
                    }
                }
            }

            if (request.Parameters.Count > 0)
            {
                var parameters = request.ParameterCollection.GetSortedParametersList();
                foreach (var parameter in parameters)
                {
                    if (parameter.Value != null && SignableParameters.Contains(parameter.Key))
                    {
                        resourcesToSign.Add(parameter.Key, parameter.Value);
                    }
                }
            }

            var delim = "?";
            List<KeyValuePair<string, string>> resources = new List<KeyValuePair<string, string>>();
            foreach (var kvp in resourcesToSign)
            {
                resources.Add(kvp);
            }

            resources.Sort((firstPair, nextPair) =>
            {
                return string.CompareOrdinal(firstPair.Key, nextPair.Key);
            });

            foreach (var resourceToSign in resources)
            {
                sb.AppendFormat("{0}{1}", delim, resourceToSign.Key);
                if (resourceToSign.Value != null)
                    sb.AppendFormat("={0}", resourceToSign.Value);
                delim = "&";
            }
            return sb.ToString();
        }
    }
}