using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Tessitura.Service.Contract;

namespace Tessitura.Service
{
    public interface IRestClient
    {
        string ProcessHeaderUser { get; set; }
        IRestClient AtUrl(string uriString);
        Task<RestResponse<NoResponse>> Delete();
        IRestClient DropHeader(string headerkey);
        Task<RestResponse<T>> Get<T>();
        string GetBaseUrl();
        Task<RestResponse<TResponse>> Post<TRequest, TResponse>(TRequest data);
        Task<BatchResponse> PostBatch<TRequest, TResponse>(TRequest data, Dictionary<int, Type> returnTypeByRequestId);
        Task<RestResponse<TResponse>> Put<TRequest, TResponse>(TRequest data);
        void SetTimeoutInSeconds(int timeout);

        void SetCredentials(UserCredentials userCredentials);
        IRestClient ValidatingResponse();
        IRestClient WithHeader(string headerkey, string headerValue);

    }

    public class RestClient : IRestClient
    {
        private string uri;
        private readonly string baseUri;
        private int networkTimeout;
        private bool validateResponse;
        private readonly Dictionary<string, string> headers = new Dictionary<string, string>();
        private readonly DataContractHelper dataContractHelper = new DataContractHelper();
        private HttpClient httpClient;

        public string ProcessHeaderUser { get; set; }

        public RestClient(string baseUri, int timeout, bool shouldValidateResponse)
        {
            this.baseUri = baseUri;
            validateResponse = shouldValidateResponse;
            networkTimeout = timeout;
            ProcessHeaderUser = string.Empty;
            CreateHttpClient();
        }

        public void SetTimeoutInSeconds(int timeout)
        {
            networkTimeout = timeout;
            CreateHttpClient();
        }

        public void SetCredentials(UserCredentials userCredentials)
        {
            if (headers.ContainsKey("Authorization"))
            {
                headers.Remove("Authorization");
            }
            headers.Add("Authorization", userCredentials.GetAuthHeaderString());
        }

        public IRestClient WithHeader(string headerkey, string headerValue)
        {
            headers.Add(headerkey, headerValue);
            return this;
        }

        public IRestClient DropHeader(string headerkey)
        {
            headers.Remove(headerkey);
            return this;
        }

        public string GetBaseUrl()
        {
            return baseUri;
        }

        public IRestClient AtUrl(string uriString)
        {
            uri = baseUri + uriString;
            return this;
        }

        public IRestClient ValidatingResponse()
        {
            validateResponse = true;
            return this;
        }

        public async Task<RestResponse<T>> Get<T>()
        {
            try
            {
                var requestMessage = CreateRequestMessage(HttpMethod.Get, uri);
                var response = await httpClient.SendAsync(requestMessage);

                return await ConstructRestReponse<T>(response);
            }
            catch (Exception ex)
            {
                var errorMessages = new ErrorMessages {new ErrorMessage {Code = "", Description = ex.ToString(), Details = ex.Source, ErrorPath = ex.StackTrace}};
                return new RestResponse<T>(HttpStatusCode.BadRequest, default(T), errorMessages, "", HttpStatusCode.BadRequest.ToString());
            }
        }

        public async Task<RestResponse<TResponse>> Post<TRequest, TResponse>(TRequest data)
        {
            try
            {
                var requestMessage = CreateRequestMessage(HttpMethod.Post, uri, data);
                var response = await httpClient.SendAsync(requestMessage);
                return await ConstructRestReponse<TResponse>(response);
            }
            catch (Exception ex)
            {
                var errorMessages = new ErrorMessages {new ErrorMessage {Code = "", Description = ex.ToString(), Details = ex.Source, ErrorPath = ex.StackTrace}};
                return new RestResponse<TResponse>(HttpStatusCode.BadRequest, default(TResponse), errorMessages, "", HttpStatusCode.BadRequest.ToString());
            }
        }

        public async Task<BatchResponse> PostBatch<TRequest, TResponse>(TRequest data, Dictionary<int, Type> returnTypeByRequestId)
        {
            try
            {
                var requestMessage = CreateRequestMessage(HttpMethod.Post, uri, data);
                var response = await httpClient.SendAsync(requestMessage);
                return await ConstructBatchRestReponse<TResponse>(response, returnTypeByRequestId);
            }
            catch (Exception ex)
            {
                return new BatchResponse
                {
                    BatchFailed = true,
                    Responses = new List<Response>
                    {
                        new Response
                        {
                            ErrorMessages = new ErrorMessages
                            {
                                new ErrorMessage
                                {
                                    ErrorPath = ex.Source,
                                    Description = ex.Message,
                                    Details = ex.StackTrace,
                                    Code = "BatchRestCall"
                                }
                            }
                        }
                    }
                };
            }
        }

        public async Task<RestResponse<TResponse>> Put<TRequest, TResponse>(TRequest data)
        {
            try
            {
                var requestMessage = CreateRequestMessage(HttpMethod.Put, uri, data);
                var response = await httpClient.SendAsync(requestMessage);
                return await ConstructRestReponse<TResponse>(response);
            }
            catch (Exception ex)
            {
                var errorMessages = new ErrorMessages {new ErrorMessage {Code = "", Description = ex.ToString(), Details = ex.Source, ErrorPath = ex.StackTrace}};
                return new RestResponse<TResponse>(HttpStatusCode.BadRequest, default(TResponse), errorMessages, "", HttpStatusCode.BadRequest.ToString());
            }
        }

        public async Task<RestResponse<NoResponse>> Delete()
        {
            try
            {
                var requestMessage = CreateRequestMessage(HttpMethod.Delete, uri);
                var response = await httpClient.SendAsync(requestMessage);
                return await ConstructRestReponse<NoResponse>(response);
            }
            catch (Exception ex)
            {
                var errorMessages = new ErrorMessages {new ErrorMessage {Code = "", Description = ex.ToString(), Details = ex.Source, ErrorPath = ex.StackTrace}};
                return new RestResponse<NoResponse>(HttpStatusCode.BadRequest, new NoResponse(), errorMessages, "", HttpStatusCode.BadRequest.ToString());
            }
        }

        private HttpRequestMessage CreateRequestMessage(HttpMethod method, string uri)
        {
            var requestMessage = new HttpRequestMessage(method, uri);

            if (!string.IsNullOrEmpty(ProcessHeaderUser))
            {
                requestMessage.Headers.Add("TNProcessingHeader", ProcessHeaderUser);
            }

            foreach (var header in headers)
            {
                requestMessage.Headers.Add(header.Key, header.Value);
            }

            return requestMessage;
        }

        private HttpRequestMessage CreateRequestMessage<TRequest>(HttpMethod method, string uri, TRequest data)
        {
            var requestMessage = CreateRequestMessage(method, uri);
            requestMessage.Content = new StringContent(dataContractHelper.Serialize(data, ContentType.Json), Encoding.UTF8, "application/json");
            return requestMessage;
        }

        private async Task<RestResponse<T>> ConstructRestReponse<T>(HttpResponseMessage response)
        {
            var responseObj = default(T);
            var responseBody = await response.Content.ReadAsStringAsync();

            if (IsValidStatusCode(response.StatusCode) && !string.IsNullOrEmpty(responseBody))
            {
                if (typeof (T) == typeof (string) && response.Content.Headers.ContentType.MediaType.ToLower() != "text/xml")
                {
                    if (!responseBody.StartsWith("<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/\">") && !responseBody.StartsWith("<string>"))
                    {
                        return new RestResponse<T>(response.StatusCode, responseObj, null, responseBody, response.StatusCode.ToString());
                    }
                }
                responseObj = dataContractHelper.Deserialize<T>(responseBody, ContentType.Json);
            }
            else if (!string.IsNullOrEmpty(responseBody))
            {
                if (responseBody.StartsWith("<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/\">"))
                {
                    throw new Exception(responseBody.Replace("<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/\">", "").Replace("</string>", ""));
                }
                var messages = dataContractHelper.Deserialize<ErrorMessages>(responseBody, ContentType.Json);
                return new RestResponse<T>(response.StatusCode, responseObj, messages, responseBody, response.StatusCode.ToString());
            }

            var restResponse = new RestResponse<T>(response.StatusCode, responseObj, null, responseBody, response.StatusCode.ToString());
            if (validateResponse)
            {
                ValidateResponse(restResponse);
            }
            return restResponse;
        }

        private async Task<BatchResponse> ConstructBatchRestReponse<T>(HttpResponseMessage httpResponseMessage, IReadOnlyDictionary<int, Type> returnTypeByRequestId)
        {
            var responseBody = await httpResponseMessage.Content.ReadAsStringAsync();
            var batchResponse = dataContractHelper.Deserialize<BatchResponse>(responseBody, ContentType.Json, typeof (Constituent).GetTypeInfo().Assembly.ExportedTypes.Where(x => x.Namespace == "Tessitura.Service.Contract"));
            if (batchResponse != null && batchResponse.Responses != null)
            {
                foreach (var response in batchResponse.Responses)
                {
                    var jObject = response.ResponseObject as JObject;
                    if (jObject != null)
                    {
                        var objectType = returnTypeByRequestId[response.RequestId];
                        response.ResponseObject = jObject.ToObject(objectType);
                    }
                    else
                    {
                        var jArray = response.ResponseObject as JArray;
                        if (jArray != null)
                        {
                            var objectType = returnTypeByRequestId[response.RequestId];
                            response.ResponseObject = jArray.ToObject(objectType);
                        }
                    }
                }
            }
            return batchResponse;
        }

        public static void ValidateResponse<T>(RestResponse<T> response)
        {
            if (IsValidStatusCode(response.StatusCode))
            {
                return;
            }
            if (response.ErrorMessages == null)
            {
                throw new RestResponseException(response.StatusCode, response.StatusDescription, response.ResponseBody);
            }
            throw new RestResponseException(response.StatusCode, response.StatusDescription, response.ErrorMessages, response.ResponseBody);
        }

        public static bool IsValidStatusCode(HttpStatusCode httpStatusCode)
        {
            return ((int) httpStatusCode >= 200 && (int) httpStatusCode < 300) || httpStatusCode == HttpStatusCode.NotModified;
        }


        private void CreateHttpClient()
        {

            httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            if (networkTimeout > 0)
            {
                httpClient.Timeout = new TimeSpan(0, 0, networkTimeout);
            }
        }
    }
}