using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Linq;

namespace Knoks.Framework.Services
{
    public class DocuSigneSignatureService : ISignatureService, IDisposable
    {
        public string ServiceProvider { get { return "DocuSign"; } }
        public string Version { get { return "1.0.0"; } }

        private readonly ILogger<DocuSigneSignatureService> _logger;
        private readonly DocuSigneSignatureSettings _settings;

        private readonly HttpClient _httpClient;
        private string _baseUrl;

        public DocuSigneSignatureService(ILogger<DocuSigneSignatureService> logger, DocuSigneSignatureSettings settings)
        {
            _logger = logger;
            _settings = settings;

            _httpClient = new HttpClient();
            _httpClient.BaseAddress = new Uri(settings.EndpointUrl);
            _httpClient.DefaultRequestHeaders.Add("X-DocuSign-Authentication", JsonConvert.SerializeObject(new
            {
                Username = settings.Username,
                Password = settings.Password,
                IntegratorKey = settings.IntegratorKey
            }));
        }

        public async Task<string> GetBaseUrl()
        {
            try
            {
                var res = await _httpClient.GetAsync("login_information");
                var jsonResult = await res.Content.ReadAsStringAsync();
                return JObject.Parse(jsonResult).SelectToken("loginAccounts[0].baseUrl").Value<string>();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
                throw;
            }
        }

        public async Task<SignatureEntityOutput> RequestSignature(SignatureEntityInput signatureEntityInput, object settings = null)
        {
            try
            {
                if (signatureEntityInput.ServiceProvider != ServiceProvider)
                    throw new InvalidOperationException($"Invalid {nameof(signatureEntityInput.ServiceProvider)} value '{signatureEntityInput.ServiceProvider}'. an expected one is '{ServiceProvider}'");

                if (signatureEntityInput.Version != Version)
                    throw new InvalidOperationException($"Invalid {nameof(signatureEntityInput.Version)} value '{signatureEntityInput.Version}'. an expected one is '{Version}'");

                var docuSignEnvelope = signatureEntityInput.Input as JObject;
                if (docuSignEnvelope == null)
                    throw new InvalidOperationException($"Invalid {nameof(signatureEntityInput.Input)} type '{signatureEntityInput.Input.GetType().FullName}'. an expected one is '{typeof(JObject).FullName}'");

                var properties = (JObject)settings;
                if (settings != null)
                    foreach (var item in (JObject)properties["JsonPlacement"])
                        ((JProperty)docuSignEnvelope.SelectToken(item.Key).Parent).Value = item.Value;

                await SetEventNotificationUrl(docuSignEnvelope, properties["LoanRequestAndUserUid"]?.Value<string>());

                if (_baseUrl == null)
                    _baseUrl = await GetBaseUrl();

                return new SignatureEntityOutput
                {
                    ServiceProvider = ServiceProvider,
                    Version = Version,
                    Output = (await SendPostRequest(_baseUrl + "/envelopes", docuSignEnvelope)).ContentObject
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
                throw;
            }
        }

        //Install and run "ngrok agent" on your local machine for redirect webhoock to localhost.
        //Additional info on https://ngrok.com/
        //Command example when application running on IIS or IIS Express: ngrok http 52281 -host-header="localhost:52281"
        //Cammand example when application running as self hosted hosted on 5000 port: ngrok http 5000
        private async Task SetEventNotificationUrl(JObject jObj, string loanRequestAndUserUid)
        {
            if (jObj.SelectToken("eventNotification.url") == null)
                return;

            var uri = new Uri(_settings.EventNotificationUrl, UriKind.Absolute);
            if (uri.Host.Equals("ngrok", StringComparison.OrdinalIgnoreCase))
            {
                using (var httpClient = new HttpClient())
                {
                    var res = JObject.Parse(await httpClient.GetStringAsync("http://127.0.0.1:4040/api/tunnels"));
                    var ngrokUrl = res["tunnels"].Where(item => item["proto"].Value<string>() == uri.Scheme).Single()["public_url"].Value<string>();
                    uri = new Uri(ngrokUrl + uri.PathAndQuery, UriKind.Absolute);
                }
            }

            jObj["eventNotification"]["url"] = uri.ToString().Replace($"{{{nameof(loanRequestAndUserUid)}}}", loanRequestAndUserUid ?? string.Empty);
        }

        public async Task<JObject> GetRecipientViewUrl(IDictionary<string, string> properties)
        {
            return (await SendPostRequest(_baseUrl + $"/envelopes/{properties["envelopeId"]}/views/recipient", JsonConvert.SerializeObject(new
            {
                authenticationMethod = properties["authenticationMethod"],
                clientUserId = properties["clientUserId"],
                email = properties["email"],
                recipientId = properties["recipientId"],
                userName = properties["name"]
            }))).ContentObject;
        }

        private class SendPostResult
        {
            public HttpRequestMessage RequestMessage;
            public HttpResponseMessage ResponseMessage;
            public string ContentString;
            public JObject ContentObject;
            public HttpStatusCode StatusCode { get { return ResponseMessage.StatusCode; } }
        }

        private async Task<SendPostResult> SendPostRequest(string url, object body, HttpStatusCode expectedStatusCode = HttpStatusCode.Created, bool throwExceptionOnUnexpectedStatusCode = true)
        {
            //- request envelope --
            var request = new HttpRequestMessage(HttpMethod.Post, url);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Content = new StringContent(body.ToString(), Encoding.UTF8, "application/json");

            //- response envelope --
            var responce = await _httpClient.SendAsync(request);
            var contentString = await responce.Content.ReadAsStringAsync();
            var contentObject = JObject.Parse(await responce.Content.ReadAsStringAsync());

            if (throwExceptionOnUnexpectedStatusCode && responce.StatusCode != HttpStatusCode.Created)
            {
                throw new InvalidOperationException(new JObject
                {
                    ["HttpStatusCode"] = (int)responce.StatusCode,
                    ["Content"] = contentObject
                }.ToString());
            }

            return new SendPostResult { RequestMessage = request, ResponseMessage = responce, ContentString = contentString, ContentObject = contentObject };
        }

        public void Dispose()
        {
            if (_httpClient != null)
                _httpClient.Dispose();
        }
    }
}