using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using GitHub.Services.Common;

namespace GitHub.Services.Common
{
    /// <summary>
    /// Provides common settings for a <c>VssHttpMessageHandler</c> instance.
    /// </summary>
    public class VssHttpRequestSettings
    {
        /// <summary>
        /// Initializes a new <c>VssHttpRequestSettings</c> instance with compression enabled.
        /// </summary>
        public VssHttpRequestSettings()
            : this(Guid.NewGuid())
        {
        }

        /// <summary>
        /// Initializes a new <c>VssHttpRequestSettings</c> instance with compression enabled.
        /// </summary>
        public VssHttpRequestSettings(Guid sessionId)
        {
            this.AllowAutoRedirect = false;
            this.CompressionEnabled = true;
            this.ExpectContinue = true;
            this.BypassProxyOnLocal = true;
            this.MaxContentBufferSize = c_defaultContentBufferSize;
            this.SendTimeout = s_defaultTimeout;
            if (!String.IsNullOrEmpty(CultureInfo.CurrentUICulture.Name)) // InvariantCulture for example has an empty name.
            {
                this.AcceptLanguages.Add(CultureInfo.CurrentUICulture);
            }
            this.SessionId = sessionId;
            this.SuppressFedAuthRedirects = true;
            this.ClientCertificateManager = null;
            this.ServerCertificateValidationCallback = null;
            this.UseHttp11 = false;

            // If different, we'll also add CurrentCulture to the request headers,
            // but UICulture was added first, so it gets first preference
            if (!CultureInfo.CurrentCulture.Equals(CultureInfo.CurrentUICulture) && !String.IsNullOrEmpty(CultureInfo.CurrentCulture.Name))
            {
                this.AcceptLanguages.Add(CultureInfo.CurrentCulture);
            }

            this.MaxRetryRequest = c_defaultMaxRetry;

#if DEBUG
            string customClientRequestTimeout = Environment.GetEnvironmentVariable("VSS_Client_Request_Timeout");
            if (!string.IsNullOrEmpty(customClientRequestTimeout) && int.TryParse(customClientRequestTimeout, out int customTimeout))
            {
                // avoid disrupting a debug session due to the request timing out by setting a custom timeout.
                this.SendTimeout = TimeSpan.FromSeconds(customTimeout);
            }
#endif
        }

        /// <summary>
        /// Initializes a new <c>VssHttpRequestSettings</c> instance with compression enabled.
        /// </summary>
        /// <remarks>The e2eId argument is not used.</remarks>
        [Obsolete]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public VssHttpRequestSettings(Guid sessionId, Guid e2eId)
            : this(sessionId)
        {
        }

        /// <summary>
        /// Copy Constructor
        /// </summary>
        /// <param name="copy"></param>
        protected VssHttpRequestSettings(VssHttpRequestSettings copy)
        {
            this.AllowAutoRedirect = copy.AllowAutoRedirect;
            this.CompressionEnabled = copy.CompressionEnabled;
            this.ExpectContinue = copy.ExpectContinue;
            this.BypassProxyOnLocal = copy.BypassProxyOnLocal;
            this.MaxContentBufferSize = copy.MaxContentBufferSize;
            this.SendTimeout = copy.SendTimeout;
            this.m_acceptLanguages = new List<CultureInfo>(copy.AcceptLanguages);
            this.SessionId = copy.SessionId;
            this.AgentId = copy.AgentId;
            this.SuppressFedAuthRedirects = copy.SuppressFedAuthRedirects;
            this.UserAgent = new List<ProductInfoHeaderValue>(copy.UserAgent);
            this.OperationName = copy.OperationName;
            this.ClientCertificateManager = copy.ClientCertificateManager;
            this.ServerCertificateValidationCallback = copy.ServerCertificateValidationCallback;
            this.MaxRetryRequest = copy.MaxRetryRequest;
            this.UseHttp11 = copy.UseHttp11;
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not HttpClientHandler should follow redirect on outgoing requests. 
        /// </summary>
        public Boolean AllowAutoRedirect
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not compression should be used on outgoing requests. 
        /// The default value is true.
        /// </summary>
        [DefaultValue(true)]
        public Boolean CompressionEnabled
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not the Expect: 100-continue header should be sent on
        /// outgoing requess. The default value is true.
        /// </summary>
        [DefaultValue(true)]
        public Boolean ExpectContinue
        {
            get;
            set;
        }

        /// <summary>
        /// Sets whether to bypass web proxies if the call is local
        /// </summary>
        public Boolean BypassProxyOnLocal
        {
            get;
            set;
        }

        /// <summary>
        /// The .NET Core 2.1 runtime switched its HTTP default from HTTP 1.1 to HTTP 2.
        /// This causes problems with some versions of the Curl handler on Linux.
        /// See GitHub issue https://github.com/dotnet/corefx/issues/32376
        /// If true, requests generated by this client will use HTTP 1.1.
        /// </summary>
        public Boolean UseHttp11
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the maximum size allowed for response content buffering.
        /// </summary>
        [DefaultValue(c_defaultContentBufferSize)]
        public Int32 MaxContentBufferSize
        {
            get
            {
                return m_maxContentBufferSize;
            }
            set
            {
                ArgumentUtility.CheckForOutOfRange(value, nameof(value), 0, c_maxAllowedContentBufferSize);
                m_maxContentBufferSize = value;
            }
        }

        /// <summary>
        /// Timespan to wait before timing out a request. Defaults to 100 seconds
        /// </summary>
        public TimeSpan SendTimeout
        {
            get;
            set;
        }

        /// <summary>
        /// Provides a hint to the server requesting that rather than getting 302 redirects as part of FedAuth flows 401 and 403 are passed through.
        /// </summary>
        [DefaultValue(true)]
        public Boolean SuppressFedAuthRedirects
        {
            get;
            set;
        }

        /// <summary>
        /// User-Agent header passed along in the request,
        /// For multiple values, the order in the list is the order
        /// in which they will appear in the header
        /// </summary>
        public List<ProductInfoHeaderValue> UserAgent
        {
            get;
            set;
        }

        /// <summary>
        /// The name of the culture is passed in the Accept-Language header
        /// </summary>
        public ICollection<CultureInfo> AcceptLanguages
        {
            get
            {
                return m_acceptLanguages;
            }
        }

        /// <summary>
        /// A unique identifier for the user session
        /// </summary>
        public Guid SessionId
        {
            get;
            set;
        }

        /// <summary>
        /// End to End ID which gets propagated everywhere unchanged
        /// </summary>
        public Guid E2EId
        {
            get;
            set;
        }

        /// <summary>
        /// This is a kind of combination between SessionId and UserAgent.
        /// If supplied, the value should be a string that uniquely identifies
        /// this application running on this particular machine.
        /// The server will then use this value
        /// to correlate user requests, even if the process restarts.
        /// </summary>
        public String AgentId
        {
            get;
            set;
        }

        /// <summary>
        /// An optional string that is sent in the SessionId header used to group a set of operations together
        /// </summary>
        public String OperationName
        {
            get;
            set;
        }

        /// <summary>
        /// Optional implementation used to gather client certificates
        /// for connections that require them
        /// </summary>
        public IVssClientCertificateManager ClientCertificateManager
        {
            get;
            set;
        }

        /// <summary>
        /// Optional implementation used to validate server certificate validation
        /// </summary>
        public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateValidationCallback
        {
            get;
            set;
        }

        /// <summary>
        /// Number of times to retry a request that has an ambient failure
        /// </summary>
        /// <remarks>
        /// This property is only used by VssConnection, so only relevant on the client
        /// </remarks>
        [DefaultValue(c_defaultMaxRetry)]
        public Int32 MaxRetryRequest
        {
            get;
            set;
        }

        protected internal virtual Boolean IsHostLocal(String hostName)
        {
            //base class always returns false. See VssClientHttpRequestSettings for override
            return false;
        }

        protected internal virtual Boolean ApplyTo(HttpRequestMessage request)
        {
            // Make sure we only apply the settings to the request once
            if (request.Options.TryGetValue<object>(PropertyName, out _))
            {
                return false;
            }

            request.Options.Set(new HttpRequestOptionsKey<VssHttpRequestSettings>(PropertyName), this);

            if (this.AcceptLanguages != null && this.AcceptLanguages.Count > 0)
            {
                // An empty or null CultureInfo name will cause an ArgumentNullException in the
                // StringWithQualityHeaderValue constructor. CultureInfo.InvariantCulture is an example of
                // a CultureInfo that has an empty name.
                foreach (CultureInfo culture in this.AcceptLanguages.Where(a => !String.IsNullOrEmpty(a.Name)))
                {
                    request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(culture.Name));
                }
            }

            if (this.UserAgent != null)
            {
                foreach (var headerVal in this.UserAgent)
                {
                    if (!request.Headers.UserAgent.Contains(headerVal))
                    {
                        request.Headers.UserAgent.Add(headerVal);
                    }
                }
            }

            if (this.SuppressFedAuthRedirects)
            {
                request.Headers.Add(Internal.HttpHeaders.TfsFedAuthRedirect, "Suppress");
            }

            // Record the command, if we have it.  Otherwise, just record the session ID.
            if (!request.Headers.Contains(Internal.HttpHeaders.TfsSessionHeader))
            {
                if (!String.IsNullOrEmpty(this.OperationName))
                {
                    request.Headers.Add(Internal.HttpHeaders.TfsSessionHeader, String.Concat(this.SessionId.ToString("D"), ", ", this.OperationName));
                }
                else
                {
                    request.Headers.Add(Internal.HttpHeaders.TfsSessionHeader, this.SessionId.ToString("D"));
                }
            }

            if (!String.IsNullOrEmpty(this.AgentId))
            {
                request.Headers.Add(Internal.HttpHeaders.VssAgentHeader, this.AgentId);
            }

            // Content is being sent as chunked by default in dotnet5.4, which differs than the .net 4.5 behaviour.
            if (request.Content != null && !request.Content.Headers.ContentLength.HasValue && !request.Headers.TransferEncodingChunked.HasValue)
            {
                request.Content.Headers.ContentLength = request.Content.ReadAsByteArrayAsync().Result.Length;
            }

            return true;
        }

        /// <summary>
        /// Gets the encoding used for outgoing requests.
        /// </summary>
        public static Encoding Encoding
        {
            get
            {
                return s_encoding.Value;
            }
        }

        /// <summary>
        /// Gets the property name used to reference this object.
        /// </summary>
        public const String PropertyName = "MS.VS.RequestSettings";

        /// <summary>
        /// Gets the property name used to reference the completion option for a specific request.
        /// </summary>
        public const String HttpCompletionOptionPropertyName = "MS.VS.HttpCompletionOption";

        /// <summary>
        /// Header to include the light weight response client option.
        /// </summary>
        public const string LightweightHeader = "lightweight";

        /// <summary>
        /// Header to include the exclude urls client option.
        /// </summary>
        public const string ExcludeUrlsHeader = "excludeUrls";

        private Int32 m_maxContentBufferSize;
        private ICollection<CultureInfo> m_acceptLanguages = new List<CultureInfo>();
        private static Lazy<Encoding> s_encoding = new Lazy<Encoding>(() => new UTF8Encoding(false), LazyThreadSafetyMode.PublicationOnly);
        private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(100); //default WebAPI timeout
        private const Int32 c_defaultMaxRetry = 3;

        // We will buffer a maximum of 1024MB in the message handler
        private const Int32 c_maxAllowedContentBufferSize = 1024 * 1024 * 1024;

        // We will buffer, by default, up to 512MB in the message handler
        private const Int32 c_defaultContentBufferSize = 1024 * 1024 * 512;
    }
}
