using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
namespace Amazon.Lambda.AspNetCoreServer.Internal
{
    /// 
    /// 
    /// 
    public static class Utilities
    {
        public static void EnsureLambdaServerRegistered(IServiceCollection services)
        {
            EnsureLambdaServerRegistered(services, typeof(LambdaServer));
        }
        public static void EnsureLambdaServerRegistered(IServiceCollection services, Type serverType)
        {
            IList toRemove = new List();
            var serviceDescriptions = services.Where(x => x.ServiceType == typeof(IServer));
            int lambdaServiceCount = 0;
            // There can be more then one IServer implementation registered if the consumer called ConfigureWebHostDefaults in the Init override for the IHostBuilder.
            // This makes sure there is only one registered IServer using LambdaServer and removes any other registrations.
            foreach (var serviceDescription in serviceDescriptions)
            {
                if (serviceDescription.ImplementationType == serverType)
                {
                    lambdaServiceCount++;
                    // If more then one LambdaServer registration has occurred then remove the extra registrations.
                    if (lambdaServiceCount > 1)
                    {
                        toRemove.Add(serviceDescription);
                    }
                }                        
                // If there is an IServer registered that isn't LambdaServer then remove it. This is most likely caused
                // by leaving the UseKestrel call.
                else
                {
                    toRemove.Add(serviceDescription);
                }
            }
            foreach (var serviceDescription in toRemove)
            {
                services.Remove(serviceDescription);
            }
            if (lambdaServiceCount == 0)
            {
                services.AddSingleton(typeof(IServer), serverType);
            }
        }
        internal static Stream ConvertLambdaRequestBodyToAspNetCoreBody(string body, bool isBase64Encoded)
        {
            Byte[] binaryBody;
            if (isBase64Encoded)
            {
                binaryBody = Convert.FromBase64String(body);
            }
            else
            {
                binaryBody = UTF8Encoding.UTF8.GetBytes(body);
            }
            return new MemoryStream(binaryBody);
        }
        internal static (string body, bool isBase64Encoded) ConvertAspNetCoreBodyToLambdaBody(Stream aspNetCoreBody, ResponseContentEncoding rcEncoding)
        {
            // Do we encode the response content in Base64 or treat it as UTF-8
            if (rcEncoding == ResponseContentEncoding.Base64)
            {
                // We want to read the response content "raw" and then Base64 encode it
                byte[] bodyBytes;
                if (aspNetCoreBody is MemoryStream)
                {
                    bodyBytes = ((MemoryStream)aspNetCoreBody).ToArray();
                }
                else
                {
                    using (var ms = new MemoryStream())
                    {
                        aspNetCoreBody.CopyTo(ms);
                        bodyBytes = ms.ToArray();
                    }
                }
                return (body: Convert.ToBase64String(bodyBytes), isBase64Encoded: true);
            }
            else if (aspNetCoreBody is MemoryStream)
            {
                return (body: UTF8Encoding.UTF8.GetString(((MemoryStream)aspNetCoreBody).ToArray()), isBase64Encoded: false);
            }
            else
            {
                aspNetCoreBody.Position = 0;
                using (StreamReader reader = new StreamReader(aspNetCoreBody, Encoding.UTF8))
                {
                    return (body: reader.ReadToEnd(), isBase64Encoded: false);
                }
            }
        }
        /// 
        /// Add a '?' to the start of a non-empty query string, otherwise return null.
        /// 
        /// 
        /// The ASP.NET MVC pipeline expects the query string to be URL-escaped.  Since the value in
        ///  should already be escaped this
        /// method does not perform any escaping itself.  This ensures identical behaviour when an MVC app
        /// is run through an API Gateway with this framework or in a standalone Kestrel instance.
        /// 
        /// URL-escaped query string without initial '?'
        /// 
        public static string CreateQueryStringParametersFromHttpApiV2(string queryString)
        {
            if (string.IsNullOrEmpty(queryString))
                return null;
            return "?" + queryString;
        }
        internal static string CreateQueryStringParameters(IDictionary singleValues, IDictionary> multiValues, bool urlEncodeValue)
        {
            if (multiValues?.Count > 0)
            {
                StringBuilder sb = new StringBuilder("?");
                foreach (var kvp in multiValues)
                {
                    foreach (var value in kvp.Value)
                    {
                        if (sb.Length > 1)
                        {
                            sb.Append("&");
                        }
                        sb.Append($"{kvp.Key}={(urlEncodeValue ? WebUtility.UrlEncode(value) : value)}");
                    }
                }
                return sb.ToString();
            }
            else if (singleValues?.Count > 0)
            {
                var queryStringParameters = singleValues;
                if (queryStringParameters != null && queryStringParameters.Count > 0)
                {
                    StringBuilder sb = new StringBuilder("?");
                    foreach (var kvp in singleValues)
                    {
                        if (sb.Length > 1)
                        {
                            sb.Append("&");
                        }
                        sb.Append($"{kvp.Key}={(urlEncodeValue ? WebUtility.UrlEncode(kvp.Value) : kvp.Value)}");
                    }
                    return sb.ToString();
                }
            }
            return string.Empty;
        }
        internal static void SetHeadersCollection(IHeaderDictionary headers, IDictionary singleValues, IDictionary> multiValues)
        {
            if (multiValues?.Count > 0)
            {
                foreach (var kvp in multiValues)
                {
                    headers[kvp.Key] = new StringValues(kvp.Value.ToArray());
                }
            }
            else if (singleValues?.Count > 0)
            {
                foreach (var kvp in singleValues)
                {
                    headers[kvp.Key] = new StringValues(kvp.Value);
                }
            }
        }
        // This code is taken from the Apache 2.0 licensed ASP.NET Core repo.
        // https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Http/Shared/StreamCopyOperationInternal.cs
        internal static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel)
        {
            long? bytesRemaining = count;
            var buffer = ArrayPool.Shared.Rent(bufferSize);
            try
            {
                Debug.Assert(source != null);
                Debug.Assert(destination != null);
                Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0);
                Debug.Assert(buffer != null);
                while (true)
                {
                    // The natural end of the range.
                    if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0)
                    {
                        return;
                    }
                    cancel.ThrowIfCancellationRequested();
                    int readLength = buffer.Length;
                    if (bytesRemaining.HasValue)
                    {
                        readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength);
                    }
                    int read = await source.ReadAsync(buffer, 0, readLength, cancel);
                    if (bytesRemaining.HasValue)
                    {
                        bytesRemaining -= read;
                    }
                    // End of the source stream.
                    if (read == 0)
                    {
                        return;
                    }
                    cancel.ThrowIfCancellationRequested();
                    await destination.WriteAsync(buffer, 0, read, cancel);
                }
            }
            finally
            {
                ArrayPool.Shared.Return(buffer);
            }
        }
        internal static string DecodeResourcePath(string resourcePath) => WebUtility.UrlDecode(resourcePath
            // Convert any + signs to percent encoding before URL decoding the path.
            .Replace("+", "%2B")
            // Double-escape any %2F (encoded / characters) so that they survive URL decoding the path.
            .Replace("%2F", "%252F")
            .Replace("%2f", "%252f"));
        internal static X509Certificate2 GetX509Certificate2FromPem(string clientCertPem)
        {
            clientCertPem = clientCertPem.TrimEnd('\n');
            if (!clientCertPem.StartsWith("-----BEGIN CERTIFICATE-----") || !clientCertPem.EndsWith("-----END CERTIFICATE-----"))
            {
                throw new InvalidOperationException(
                    "Client certificate PEM was invalid. Expected to start with '-----BEGIN CERTIFICATE-----' " +
                    "and end with '-----END CERTIFICATE-----'.");
            }
            // Remove "-----BEGIN CERTIFICATE-----\n" and "-----END CERTIFICATE-----"
            clientCertPem = clientCertPem.Substring(28, clientCertPem.Length - 53);
            return new X509Certificate2(Convert.FromBase64String(clientCertPem));
        }
    }
}