using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using BO.AppServer.Metadata.Dto;
using Quadarax.Foundation.Core.Data.Interface;
using Quadarax.Foundation.Core.Data.Interface.Entity;
using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
using Quadarax.Foundation.Core.Json;
using Quadarax.Foundation.Core.Logging;
using Quadarax.Foundation.Core.Object;
using Quadarax.Foundation.Core.Value.Extensions;

namespace BO.Connector
{
    public abstract class AbstractConnection : DisposableObject
    {
        #region *** Private fields ***
        private HttpClient _client;
        private Uri _uriApiBase;
        private ILogHandler _log;
        private TimeSpan _timeout;
        private string _ticket;
        private Binder _binder;
        private bool _isDumpContentEnabled;

        protected abstract string ApiName { get; }
        public bool IsOpen => !string.IsNullOrEmpty(_ticket);
        protected string Ticket => _ticket;
        #endregion


        #region *** Constructors ***
        protected AbstractConnection(Uri uriApiBase, TimeSpan timeout, ILogHandler log = null, bool isDumpContentEnabled = false)
        {
            _uriApiBase = uriApiBase ?? throw new ArgumentNullException(nameof(uriApiBase));
            _uriApiBase = new Uri(_uriApiBase.AbsoluteUri + ApiName + "/");
            _log = log;
            _timeout = timeout;
            _binder = new Binder(new FileSystem());
            _binder.SerializerOptions.IgnoreReadOnlyProperties = true;
            _binder.SerializerOptions.IgnoreNullValues = true;
            _isDumpContentEnabled = isDumpContentEnabled;
        }
        #endregion

        #region *** Public Operations ***
        public void Open(string userName, string userPassword, string magicKey)
        {
            Log(LogSeverityEnum.Trace, $"Opening Connector '{ApiName}' open with magicKey='{magicKey}'");
            _client = CreateClient();
            InitTicket(magicKey);
            Login(userName, userPassword);
            OnOpening();
            Log(LogSeverityEnum.Debug, $"Connector '{ApiName}' open for user '{userName}' with timeout setting {_timeout}.");
        }


        public void Close()
        {
            _client?.CancelPendingRequests();
            _client?.Dispose();
            _client = null;
            _ticket = string.Empty;
            Log(LogSeverityEnum.Debug, $"Connector '{ApiName}' closed.");
        }

        public bool Ping(out TimeSpan duration, out string status)
        {
            CheckIsOpen();
            var start = DateTime.Now;
            var ping = Task.Run(() =>CallRequestAsync<ResultValueDto<PingDto>>($"{_ticket}/ping", RestCallTypeEnum.Get, null, null, true));
            ping.Wait();
            duration = DateTime.Now - start;
            if (ping.Result == null)
            {
                status = string.Empty;
                return false;
            }
            status = ping.Result.Value.Status;
            return ping.Result.IsSuccess;
        }
        #endregion


        #region *** Virtuals & Overrides ***
        protected override void OnDisposing()
        {
            _client?.Dispose();
        }
        #endregion

        #region *** Private Operations ***

        protected void CheckIsOpen()
        {
            if (!IsOpen)
                throw new InvalidOperationException(
                    "Connector is not open to call specific operation. Call Open() first.");
        }
        protected void Log(LogSeverityEnum type, string message, Exception e = null)
        {
            _log?.Log(type, message, e);
        }

        protected void InitTicket(string magicKey)
        {
            var result = Task.Run(() => CallRequestAsync<ResultValueDto<AuthDto>>($"{magicKey}/auth", RestCallTypeEnum.Get));;
            result.Wait();
            if (!result.Result.IsSuccess)
                throw result.Result.ToAggregateException();
            _ticket = result.Result.Value.Ticket;
        }

        protected void Login(string userName, string userPassword)
        {
            var headerAtts = new Dictionary<string, string>();
            headerAtts.Add("userName",userName);
            headerAtts.Add("userPassword",userPassword);
            var result = Task.Run(() => CallRequestAsync<ResultValueDto<UserRDto>>($"{_ticket}/login", RestCallTypeEnum.Post, new ResultPlain(), headerAtts));
            result.Wait();
            if (!result.Result.IsSuccess)
                throw result.Result.ToAggregateException();
            Log(LogSeverityEnum.Info,$"User '{result.Result.Value.Name}' is logged in.");
        }

        protected IDictionary<string, string> GetPagingHeaderAttributes(PagingDto paging, IDictionary<string,string> source = null)
        {
            if (paging == null || paging.IsDisabled)
                return null;//paging = new PagingDto();

            if (source == null)
                source = new Dictionary<string, string>();


            source.Add(new KeyValuePair<string, string>("page", paging.Page.ToString()));
            source.Add(new KeyValuePair<string, string>("pageSize", paging.PageSize.ToString()));
            return source;
        }

        protected IDictionary<string, string> GetApiPasswordHeaderAttributes(string apiKeyPassword, IDictionary<string,string> source = null)
        {
            if (source == null)
                source = new Dictionary<string, string>();

            source.Add(new KeyValuePair<string, string>("apiKeyPassword", apiKeyPassword));
            return source;
        }

        protected IDictionary<string, string> GetOverwriteHeaderAttributes(bool overwrite, IDictionary<string,string> source = null)
        {
            if (source == null)
                source = new Dictionary<string, string>();

            source.Add(new KeyValuePair<string, string>("overwrite", overwrite.ToString()));
            return source;
        }

        protected IDictionary<string, string> GetForceHeaderAttributes(bool force, IDictionary<string,string> source = null)
        {
            if (source == null)
                source = new Dictionary<string, string>();

            source.Add(new KeyValuePair<string, string>("force", force.ToString()));
            return source;
        }

        protected IDictionary<string, string> GetUserContextHeaderAttributes(string userNameContext, IDictionary<string,string> source = null)
        {
            if (source == null)
                source = new Dictionary<string, string>();

            source.Add(new KeyValuePair<string, string>("context", userNameContext));
            return source;
        }



        protected async Task<Stream> CallGetStreamAsync(string relativeUrlApiCall,
            IDictionary<string, string> headerAttributes = null, bool silent = false)
        {
            Log(LogSeverityEnum.Trace, $"Calling REST API GET STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall}");
            if (headerAttributes != null)
            {
                foreach (var key in headerAttributes.Keys)
                {
                    _client.DefaultRequestHeaders.Remove(key);
                    _client.DefaultRequestHeaders.Add(key, headerAttributes[key]);
                }
            }

            return await _client.GetStreamAsync(relativeUrlApiCall);
        }

        protected async Task<TResult> CallPostStreamMultipartAsync<TResult>(string relativeUrlApiCall,
            string fileName,Stream stream,string streamMimeType, IDictionary<string, string> headerAttributes = null, bool silent = false)
            where TResult : ResultDto
        {
            Log(LogSeverityEnum.Trace, $"Calling REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall}");


            var boundary = Guid.NewGuid().ToString();
            var mpc = new MultipartFormDataContent(boundary);
            mpc.Headers.Remove("Content-Type");
            mpc.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=" + boundary);


            var stc = new StreamContent(stream);
            stc.Headers.ContentType = new MediaTypeHeaderValue(streamMimeType);
            mpc.Add(stc, "content", fileName);


            using (var rq = new HttpRequestMessage())
            {
                rq.Content = mpc;
                rq.Method = new System.Net.Http.HttpMethod("POST");
                rq.RequestUri = new Uri(_uriApiBase.AbsoluteUri + relativeUrlApiCall);
                rq.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/octet-stream"));
                if (headerAttributes != null)
                    foreach (var key in headerAttributes.Keys)
                        rq.Headers.Add(key, headerAttributes[key]);

                var start = DateTime.Now;
                var resp = await _client.SendAsync(rq);
                //var resp = await cli.PostAsync(relativeUrlApiCall, rqdMulti);


                Log(LogSeverityEnum.Trace,
                    $"REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall} done @ {(DateTime.Now - start).ToReadableString()}");
                if (!resp.IsSuccessStatusCode)
                {
                    var message =
                        $"Calling REST API POST-MULTI-STREAM {_uriApiBase.AbsoluteUri + relativeUrlApiCall} ends with result code '{resp.StatusCode}'";
                    Log(LogSeverityEnum.Warn, message);
                    throw new Exception(message);
                }

                var content = await resp.Content.ReadAsStringAsync();
                if (_isDumpContentEnabled)
                    Log(LogSeverityEnum.Trace, content);

                var result = (TResult)_binder.LoadFromString(content, typeof(TResult));
                if (result.IsSuccess) return result;
                if (silent) return result;

                // handle errors
                var excps = new List<Exception>();
                foreach (var err in result.Errors)
                {
                    var exc = new Exception($"[{err.Code}] {err.Message}");
                    exc.Data.Add("Code", err.Code);
                    excps.Add(exc);
                }

                throw new AggregateException(excps);
            }
        }


        protected async Task<TResult> CallRequestAsync<TResult>(string relativeUrlApiCall,RestCallTypeEnum method, IDto data = null,IDictionary<string,string> headerAttributes = null, bool silent = false) 
            where TResult : ResultDto
        {
            if (data == null)
                if(method != RestCallTypeEnum.Get && method != RestCallTypeEnum.GetStream && method != RestCallTypeEnum.Delete)
                    throw new ArgumentNullException(nameof(data), $"For REST API method '{method}' data must be specified!");

            Log(LogSeverityEnum.Trace, $"Calling REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall}");
            var start = DateTime.Now;
            HttpResponseMessage resp = null;

            SetDefaultClientHeaders(_client);
            HttpContentHeaders reqHeaders = null;
            switch (method)
            {
                case RestCallTypeEnum.Get:
                    if (headerAttributes != null)
                    {
                        foreach (var key in headerAttributes.Keys)
                        {
                            _client.DefaultRequestHeaders.Remove(key);
                            _client.DefaultRequestHeaders.Add(key,headerAttributes[key]);
                        }
                    }
                    resp = await _client.GetAsync(relativeUrlApiCall);
                    break;
                case RestCallTypeEnum.Post:
                    var req = new StringContent(_binder.SaveToString(data));
                    reqHeaders = req.Headers;
                    if (headerAttributes!=null)
                        foreach (var key in headerAttributes.Keys)
                        {
                            //if (req.Headers.Contains(key))
                                req.Headers.Remove(key);

                            req.Headers.Add(key, headerAttributes[key]);
                        }

                    req.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                    resp = await _client.PostAsync(relativeUrlApiCall, req);
                    break;
                case RestCallTypeEnum.Put:
                    req = new StringContent(_binder.SaveToString(data));
                    reqHeaders = req.Headers;
                    if (headerAttributes!=null)
                        foreach (var key in headerAttributes.Keys)
                        {

                         //   if (req.Headers.Contains(key))
                                req.Headers.Remove(key);

                            req.Headers.Add(key, headerAttributes[key]);
                        }
                    req.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                    resp = await _client.PutAsync(relativeUrlApiCall, req);
                    break;
                case RestCallTypeEnum.Delete:
                    using (var delRq = new HttpRequestMessage(HttpMethod.Delete, relativeUrlApiCall))
                    {
                        if (headerAttributes != null)
                            foreach (var key in headerAttributes.Keys)
                            {
                                delRq.Headers.Remove(key);
                                delRq.Headers.Add(key, headerAttributes[key]);
                            }

                        resp = await _client.SendAsync(delRq);
                    }

                    break;
                default:
                    throw new NotSupportedException($"{method} REST API operation is not implemented.");
            }
            

            // may remove paging headers after call
            /*
            if (headerAttributes != null)
                foreach (var key in headerAttributes.Keys)
                    _client.DefaultRequestHeaders.Remove(key);
            */

            Log(LogSeverityEnum.Trace, $"REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall} done @ {(DateTime.Now - start).ToReadableString()}");
            if (!resp.IsSuccessStatusCode)
            {
                var message = $"Calling REST API {method} {_uriApiBase.AbsoluteUri + relativeUrlApiCall} ends with result code '{resp.StatusCode}'";
                Log(LogSeverityEnum.Warn, message);
                throw new Exception(message);
            }

            var content = await resp.Content.ReadAsStringAsync();
            if (_isDumpContentEnabled)
            {
                Log(LogSeverityEnum.Trace, DumpHttpClient(method,relativeUrlApiCall, _client, reqHeaders));
                Log(LogSeverityEnum.Trace, content);
            }

            var result = (TResult)_binder.LoadFromString(content, typeof(TResult));
            if (result.IsSuccess) return result;
            // No data exception pass result
            if (!result.IsSuccess && result.Errors.Any(x => x.Code == 1099)) return result;
            if (silent) return result;

            // handle errors
            var excps = new List<Exception>();
            foreach (var err in result.Errors)
            {
                var exc = new Exception($"[{err.Code}] {err.Message}");
                exc.Data.Add("Code", err.Code);
                excps.Add(exc);
            }

            throw new AggregateException(excps);
        }


        protected virtual void OnOpening()
        {
        }

        private HttpClient CreateClient()
        {
            var client = new HttpClient();
            client.BaseAddress = new Uri(_uriApiBase, ApiName);
            client.Timeout = _timeout;
            SetDefaultClientHeaders(client);
            return client;
        }

        private void SetDefaultClientHeaders(HttpClient client)
        {
            client.DefaultRequestHeaders.Clear();
            client.DefaultRequestHeaders.Add("User-Agent", "BO.Connector." + ApiName);
            // Add an Accept header for JSON format.
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("multipart/mixed"));
            //client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
        }

        private string DumpHttpClient(RestCallTypeEnum method, string relativeUrl, HttpClient client, HttpContentHeaders headers)
        {
            var sb = new StringBuilder();
            sb.Append($"RQ {method.ToString().ToUpper()} to url '{_client.BaseAddress + relativeUrl}':").AppendLine();
            sb.Append($"rq version: {_client.DefaultRequestVersion}").AppendLine();
            sb.Append($"rq timeout: {_client.Timeout.ToReadableString()}").AppendLine();
            sb.Append("Headers:").AppendLine();
            foreach (var header in _client.DefaultRequestHeaders)
                sb.Append(header.Key).Append(" : ").Append(string.Join(",", header.Value)).AppendLine();
            sb.Append(DumpHttpHeaders(headers));
            return sb.ToString();
        }

        private string DumpHttpHeaders(HttpContentHeaders headers)
        {
            var sb = new StringBuilder();
            sb.Append("Headers:").AppendLine();
            if (headers != null)
            {
                foreach (var header in headers)
                    sb.Append(header.Key).Append(" : ").Append(string.Join(",", header.Value)).AppendLine();
            }

            return sb.ToString();
        }
        #endregion

    }
}
