using System.IO.Compression;
using System.Net;
using InABox.Core;
using RestSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;


namespace InABox.Clients
{
    public static class StaticRestClients
    {
        public static Dictionary<string, RestClient> Clients = new Dictionary<string, RestClient>();

        public static RestClient GetClient(string url)
        {
            var uri = new Uri(url);
            if (!Clients.TryGetValue(url, out var cli))
            {
                cli = new RestClient(uri);
                Clients.Add(url, cli);
            }
            return cli;
        }
    }

    public class RestClient<TEntity> : BaseClient<TEntity> where TEntity : Entity, new()
    {
        private bool _simpleencryption;
        private string _server;
        private bool _compression;
        private BinarySerializationSettings _binarysettings;
        
        public RestClient(string server, bool useSimpleEncryption, bool compression, BinarySerializationSettings binarySerializationSettings)
        {
            _server = server;
            _simpleencryption = useSimpleEncryption;
            _compression = compression;
            _binarysettings = binarySerializationSettings;

            RestClientCache.Check(server);
        }

        public RestClient(string server, bool simpleencryption, bool compression) : this(server, simpleencryption, compression, BinarySerializationSettings.Latest)
        {
        }

        public RestClient(string server, bool simpleencryption) : this(server, simpleencryption, false)
        {
        }

        public RestClient(string server) : this(server, false)
        {
        }

        public override bool IsConnected() => true;
        
        public static string Ping(string[] urls, out DatabaseInfo info)
        {
            var result = "";
            info = new DatabaseInfo();
            List<Task<Tuple<string,DatabaseInfo>>> pings = urls.Select(x => Task.Run(
                () => new Tuple<string, DatabaseInfo>(x, new RestClient<User>(x).Info())
            )).ToList();
            while (pings.Count > 0)
            {
                var ping = Task.WhenAny(pings).Result;
                if (ping.Status == TaskStatus.RanToCompletion && !string.IsNullOrWhiteSpace(ping.Result.Item2.Version))
                {
                    result = ping.Result.Item1;
                    info = ping.Result.Item2;
                    break;
                }
                else
                    pings.Remove(ping);
            }
            return result;
        }
        

        private void PrepareRequest(Request request)
        {
            request.Credentials.Platform = ClientFactory.Platform;
            request.Credentials.Version = ClientFactory.Version;
            request.Credentials.Session = ClientFactory.SessionID;
            Request.BeforeRequest?.Invoke(request);
        }

        protected override IValidationData DoValidate(Guid session = default)
        {
            return Validate(
                null, null, false, session);
        }

        protected override IValidationData DoValidate(string pin, Guid session = default)
        {
            return Validate(
                null, pin, true, session);
        }

        protected override IValidationData DoValidate(string userid, string password, Guid session = default)
        {
            return Validate(
                userid, password, false, session);
        }

        private IValidationData Validate(string? userid, string? password, bool usePin, Guid session = default)
        {
            var ticks = DateTime.Now.ToUniversalTime().Ticks.ToString();
            var request = new ValidateRequest();

            request.UsePIN = usePin;
            if (usePin)
            {
                request.UserID = Encryption.Encrypt(ticks, "wCq9rryEJEuHIifYrxRjxg", _simpleencryption);
                request.Password = Encryption.Encrypt(ticks, "7mhvLnqMwkCAzN+zNGlyyg", _simpleencryption);
                request.PIN = password;
            }
            else
            {
                request.UserID = userid;
                request.Password = password;
            }

            PrepareRequest(request);
            if (session != Guid.Empty)
            {
                request.Credentials.Session = session;
            }

            var response = SendRequest<ValidateRequest, ValidateResponse>(request, "validate", SerializationFormat.Binary, SerializationFormat.Binary, false);
            if (response != null)
                if (response.Status.Equals(StatusCode.OK))
                {
                    PasswordCache.Password = password;
                    return new ValidationData(
                        response.ValidationStatus,
                        response.UserID,
                        response.UserGuid,
                        response.SecurityID,
                        response.Session,
                        response.Recipient2FA,
                        response.PasswordExpiration
                    );
                }
                else if(response.Status == StatusCode.BadServer)
                {
                    throw new RemoteException(response.Messages, request);
                }

            return new ValidationData(
                ValidationStatus.INVALID,
                "",
                Guid.Empty,
                Guid.Empty,
                Guid.Empty,
                null,
                DateTime.MinValue
            );
        }

        protected TResponse SendRequest<TRequest, TResponse>(TRequest request, string Action, SerializationFormat requestFormat, SerializationFormat responseFormat, bool includeEntity = true)
            where TRequest : Request where TResponse : Response, new()
        {

            var result = default(TResponse);

            var url = RestClientCache.URL(_server);
            
            if (string.IsNullOrEmpty(url))
            {
                result = (TResponse)Activator.CreateInstance(typeof(TResponse));
                result.Status = StatusCode.BadServer;
                result.Messages.Add("Server URL not set!");
                return result;
            }

            var cli = StaticRestClients.GetClient(url);

            var cmd = string.Format(
                "{0}{1}?format={2}&responseFormat={3}&serializationVersion={4}", 
                Action, 
                includeEntity ? typeof(TEntity).Name : "",
                requestFormat,
                responseFormat,
                _binarysettings.Version
            );
            var req = new RestRequest(cmd, Method.Post)
            {
                Timeout = TimeSpan.FromMilliseconds(Timeout.TotalMilliseconds),
            };
            //Log("  * {0}{1}() Creating Uri, Client and RestRequest took {2}ms", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds);
            //sw.Restart();

            //req.AdvancedResponseWriter = (stream, response) =>
            //{
            //    //Log("  * {0}{1}() Response from Server took {2}ms ({3} bytes)", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds, response.ContentLength);
            //    //length = response.ContentLength;
            //    //sw.Restart();
            //    try
            //    {
            //        if (responseFormat == SerializationFormat.Binary && typeof(TResponse).HasInterface<ISerializeBinary>())
            //        {
            //            result = (TResponse)Serialization.ReadBinary(typeof(TResponse), stream, _binarysettings);
            //        }
            //        else
            //        {
            //            result = Serialization.Deserialize<TResponse>(stream, true);
            //        }
            //    }
            //    catch (Exception e)
            //    {
            //        Logger.Send(LogType.Information, "", $"Error deserializing response: {e.Message}");
            //    }
            //    //Log("  * {0}{1}() Deserializing Stream took {2}ms ({3} bytes)", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds, response.ContentLength);
            //};

            if(requestFormat == SerializationFormat.Binary && request is ISerializeBinary binary)
            {
                var data = binary.WriteBinary(_binarysettings);

                req.AddOrUpdateParameter("application/octet-stream", data, ParameterType.RequestBody);
                req.RequestFormat = DataFormat.None;
            }
            else
            {
                var json = Serialization.Serialize(request);

                req.AddOrUpdateParameter("application/json", json, ParameterType.RequestBody);
                req.RequestFormat = DataFormat.Json;
            }
            try
            {
                //sw.Restart();
                var res = cli.Execute(req);
                //Log("  * {0}{1}() returns {2} bytes in {3}ms", Action, typeof(TEntity).Name, res.ContentLength, sw.ElapsedMilliseconds);
                if (result == null)
                {
                    if (res.ErrorException == null)
                    {
                        if (res.StatusCode != HttpStatusCode.OK)
                            throw new Exception(String.Format("HTTP Request returns {0} {1}" + (int)res.StatusCode, CoreUtils.SplitCamelCase(res.StatusCode.ToString())));
                        
                        try
                        {
                            Stream stream;

                            if (_compression)
                            {
                                //sw.Restart();
                                var comp = Serialization.Deserialize<CompressedResponse>(res.Content, true);
                                var bytes = Convert.FromBase64String(comp.Response);
                                var ms = new MemoryStream(bytes);

                                stream = new MemoryStream();
                                using (var decompressionStream = new DeflateStream(ms, CompressionMode.Decompress))
                                {
                                    decompressionStream.CopyTo(stream);
                                }
                            }
                            else
                            {
                                stream = new MemoryStream(res.RawBytes);
                            }

                            if (responseFormat == SerializationFormat.Binary && typeof(TResponse).HasInterface<ISerializeBinary>())
                            {
                                result = (TResponse)Serialization.ReadBinary(typeof(TResponse), stream, _binarysettings);
                            }
                            else
                            {
                                result = Serialization.Deserialize<TResponse>(stream, true);
                            }

                            stream.Dispose();
                        }
                        catch (Exception eDeserialize)
                        {
                            throw new Exception(string.Format("Unable to deserialize response!\n\n{0}\n\n{1}", eDeserialize.Message, res.Content));
                        }
                    }
                    else
                    {
                        // Connectivity
                        result = new TResponse();
                        result.Status = StatusCode.BadServer;
                        result.Messages.Add(res.ErrorMessage);
                    }
                }
            }
            catch (Exception err)
            {
                result = new TResponse();
                result.Status = StatusCode.BadServer;
                result.Messages.Add(err.Message);
                if (err.InnerException != null)
                    result.Messages.Add("- " + err.InnerException.Message);
            }

            return result;
        }

        #region Query Data

        protected override CoreTable DoQuery(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort = null, CoreRange? range = null)
        {
            var request = new QueryRequest<TEntity>(filter, columns, sort, range);

            PrepareRequest(request);

            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Binary, SerializationFormat.Binary);

            if (response != null)
            {
                return response.Status switch
                {
                    StatusCode.OK => response.Items,
                    StatusCode.Unauthenticated => throw new RemoteException("Client not authenticated", StatusCode.Unauthenticated, request),
                    _ => throw new RemoteException(response.Messages, request),
                };
            }

            return null;
            //throw new Exception("Response is null");
        }

        #endregion

        #region Load

        protected override TEntity[] DoLoad(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
        {
            var result = new List<TEntity>();
            var request = new QueryRequest<TEntity>(filter, null, sort, range);

            PrepareRequest(request);

            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", SerializationFormat.Binary, SerializationFormat.Binary);
            if (response.Items != null)
                foreach (var row in response.Items.Rows)
                    result.Add(row.ToObject<TEntity>());
            return result.ToArray();
        }

        #endregion

        #region MultipleTables

        protected override Dictionary<string, CoreTable> DoQueryMultiple(Dictionary<string, IQueryDef> queries)
        {
            var request = new MultiQueryRequest();

            foreach (var item in queries)
            {
                request.AddQuery(item.Key, item.Value);
            }

            PrepareRequest(request);

            var response = SendRequest<MultiQueryRequest, MultiQueryResponse>(request, "QueryMultiple", SerializationFormat.Binary, SerializationFormat.Binary, false);
            if (response != null)
            {
                return response.Status switch
                {
                    StatusCode.OK => response.Tables,
                    StatusCode.Unauthenticated => throw new RemoteException("Client not authenticated", request),
                    _ => throw new RemoteException(response.Messages, request),
                };
            }

            return null;
            //throw new Exception("Response is null");
        }

        #endregion

        #region Save

        protected override void DoSave(TEntity entity, string auditnote)
        {
            var request = new SaveRequest<TEntity>(entity, auditnote);
            request.ReturnOnlyChanged = true;

            PrepareRequest(request);

            var response = SendRequest<SaveRequest<TEntity>, SaveResponse<TEntity>>(request, "Save", SerializationFormat.Binary, SerializationFormat.Binary);
            switch (response.Status)
            {
                case StatusCode.OK:
                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                    entity.SetObserving(false);
                    foreach (var prop in props.Keys)
                    {
                        var value = CoreUtils.GetPropertyValue(response.Item, prop);
                        CoreUtils.SetPropertyValue(entity, prop, value);
                    }

                    entity.CommitChanges();
                    entity.SetObserving(true);*/

                    entity.SetObserving(false);
                    foreach (var (key, value) in response.ChangedValues)
                    {
                        if(CoreUtils.TryGetProperty<TEntity>(key, out var property))
                        {
                            CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
                        }
                    }
                    entity.CommitChanges();
                    entity.SetObserving(true);

                    break;
                case StatusCode.Unauthenticated:
                    throw new RemoteException("Client not authenticated", request);
                default:
                    throw new RemoteException(response.Messages, request);
            }
        }

        protected override void DoSave(IEnumerable<TEntity> entities, string auditnote)
        {
            var items = entities.ToArray();
            var request = new MultiSaveRequest<TEntity>(items, auditnote);
            request.ReturnOnlyChanged = true;

            PrepareRequest(request);

            var response = SendRequest<MultiSaveRequest<TEntity>, MultiSaveResponse<TEntity>>(request, "MultiSave", SerializationFormat.Binary, SerializationFormat.Binary);
            switch (response.Status)
            {
                case StatusCode.OK:
                    for(int i = 0; i < items.Length; ++i)
                    {
                        var entity = items[i];
                        var changedValues = response.ChangedValues[i];

                        entity.SetObserving(false);
                        foreach (var (key, value) in changedValues)
                        {
                            if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
                            {
                                CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
                            }
                        }
                        entity.CommitChanges();
                        entity.SetObserving(true);
                    }

                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                    for (var i = 0; i < items.Length; i++)
                    {
                        items[i].SetObserving(false);
                        foreach (var prop in props.Keys)
                        {
                            var value = CoreUtils.GetPropertyValue(response.Items[i], prop);
                            CoreUtils.SetPropertyValue(items[i], prop, value);
                        }

                        //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
                        items[i].CommitChanges();
                        items[i].SetObserving(true);
                    }*/
                    break;
                case StatusCode.Unauthenticated:
                    throw new RemoteException("Client not authenticated", request);
                default:
                    throw new RemoteException(response.Messages, request);
            }
        }

        #endregion

        #region Delete

        protected override void DoDelete(TEntity entity, string auditnote)
        {
            var request = new DeleteRequest<TEntity>(entity, auditnote);

            PrepareRequest(request);

            var response = SendRequest<DeleteRequest<TEntity>, DeleteResponse<TEntity>>(request, "Delete", SerializationFormat.Binary, SerializationFormat.Binary);
            switch (response.Status)
            {
                case StatusCode.OK:
                    break;
                case StatusCode.Unauthenticated:
                    throw new RemoteException("Client not authenticated", request);
                default:
                    throw new RemoteException(response.Messages, request);
            }
        }

        protected override void DoDelete(IEnumerable<TEntity> entities, string auditnote)
        {
            var items = entities.AsArray();
            var request = new MultiDeleteRequest<TEntity>(items, auditnote);

            PrepareRequest(request);

            var response = SendRequest<MultiDeleteRequest<TEntity>, MultiDeleteResponse<TEntity>>(request, "MultiDelete", SerializationFormat.Binary, SerializationFormat.Binary);
            switch (response.Status)
            {
                case StatusCode.OK:
                    break;
                case StatusCode.Unauthenticated:
                    throw new RemoteException("Client not authenticated", request);
                default:
                    throw new RemoteException(response.Messages, request);
            }
        }

        #endregion

        #region 2FA

        protected override bool DoCheck2FA(string code, Guid? session)
        {
            var request = new Check2FARequest(code);

            PrepareRequest(request);

            var response = SendRequest<Check2FARequest, Check2FAResponse>(request, "check_2fa", SerializationFormat.Binary, SerializationFormat.Binary, false);
            if (response != null)
            {
                return response.Status switch
                {
                    StatusCode.OK => response.Valid,
                    StatusCode.Unauthenticated => false,
                    _ => throw new RemoteException(response.Messages, request),
                };
            }

            return false;
        }

        #endregion

        #region Ping

        protected override bool DoPing()
        {
            var cli = StaticRestClients.GetClient(RestClientCache.URL(_server));
            var req = new RestRequest("/ping", Method.Get) { Timeout = TimeSpan.FromSeconds(20) };

            try
            {
                var res = cli.Execute(req);
                if (res.StatusCode != HttpStatusCode.OK || res.ErrorException != null)
                    return false;
                return true;
            }
            catch
            {
                return false;
            }
        }

        #endregion
        
        public override IEnumerable<string> SupportedTypes()
        {
            var result = new List<string>();

            var cli = StaticRestClients.GetClient(RestClientCache.URL(_server));
            var req = new RestRequest("/classes", Method.Get) { Timeout = TimeSpan.FromSeconds(20) };

            try
            {
                var res = cli.Execute(req);
                if (res.ErrorException == null)
                {
                    var list = res.Content.Trim('[', ']').Split(',');
                    foreach (var operation in list)
                    {
                        var trimmed = operation.Trim('"');
                        if (!result.Contains(trimmed)) result.Add(trimmed);
                        //if (svc.Equals("Comal_Classes_Login"))
                        //    result.Add("InABox_Core_Login");
                    }
                }
            }
            catch (Exception e)
            {
            }

            req = null;
            cli = null;
            return result.ToArray();
        }

        public override DatabaseInfo Info()
        {
            return RestClientCache.Info(_server);
        }

        public override string Version()
        {
            var url = RestClientCache.URL(_server);

            if (string.IsNullOrEmpty(url))
            {
                return "";
            }

            var cli = StaticRestClients.GetClient(url);

            var versionrequest = new RestRequest("/version", Method.Get);
            return cli.Execute(versionrequest).Content ?? "";
        }

        public override string ReleaseNotes()
        {
            var url = RestClientCache.URL(_server);

            if (string.IsNullOrEmpty(url))
            {
                return "";
            }

            var cli = StaticRestClients.GetClient(url);

            var versionrequest = new RestRequest("/releasenotes", Method.Get);
            return cli.Execute(versionrequest).Content ?? "";
        }

        public override byte[]? Installer()
        {
            var url = RestClientCache.URL(_server);

            if (string.IsNullOrEmpty(url))
            {
                return null;
            }

            var cli = StaticRestClients.GetClient(url);

            var versionrequest = new RestRequest("/install", Method.Get);
            return cli.Execute(versionrequest).RawBytes;
        }
    }
}