using InABox.Client.IPC;
using InABox.Clients;
using InABox.Core;

namespace InABox.IPC
{
    internal static class LocalCache
    {
        public static string Password { get; set; }
    }

    public class IPCClient<TEntity> : BaseClient<TEntity> where TEntity : Entity, new()
    {
        private IPCClientTransport _pipe;

        public IPCClient(string pipeName)
        {
            _pipe = IPCClientFactory.GetClient(pipeName);
            
            Timeout = TimeSpan.FromSeconds(300);
        }

        public override bool IsConnected() => _pipe?.Disconnected == false;

        private static string[]? _types;
        public override string[] SupportedTypes()
        {
            _types ??= CoreUtils.Entities
                .Where(x => x.GetInterfaces().Contains(typeof(IPersistent)))
                .Select(x => x.EntityName().Replace(".", "_"))
                .ToArray();
            return _types;
        }

        public override DatabaseInfo? Info()
        {
            try
            {
                var request = new InfoRequest();
                PrepareRequest(request, false);
                var response = Send(IPCMessage.Info(request), 5000)?.GetResponse<InfoResponse>();
                return response?.Info;
            }
            catch (Exception)
            {
                return new DatabaseInfo();
            }
        }
        
        private void PrepareRequest(Request request, bool doCredentials = true)
        {
            if(request is not ValidateRequest && _pipe.Disconnected)
            {
                ClientFactory.Validate(ClientFactory.UserID, LocalCache.Password);
            }

            if (doCredentials)
            {
                request.Credentials.Platform = ClientFactory.Platform;
                request.Credentials.Version = ClientFactory.Version;
                request.Credentials.Session = ClientFactory.SessionID;
            }

            Request.BeforeRequest?.Invoke(request);
        }

        private IPCMessage Send(IPCMessage request, int? timeout = null)
        {
            return _pipe.Send(request, timeout ?? Convert.ToInt32(Timeout.TotalMilliseconds));
        }

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

            PrepareRequest(request);

            var response = Send(IPCMessage.Check2FA(request))?.GetResponse<Check2FAResponse>();
            if (response != null)
            {
                return response.Status switch
                {
                    StatusCode.OK => response.Valid,
                    StatusCode.Unauthenticated => false,
                    _ => throw new IPCException(response.Messages),
                };
            }

            return false;
        }

        protected override bool DoPing()
        {
            try
            {
                var request = new PingRequest();

                PrepareRequest(request);

                var response = Send(IPCMessage.Ping(request), 10_000)?.GetResponse<PingResponse>();
                if (response != null)
                {
                    return response.Status switch
                    {
                        StatusCode.Error or StatusCode.BadServer or StatusCode.Incomplete => throw new IPCException(response.Messages),
                        _ => true
                    };
                }
            }
            catch (Exception) { }

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

            var response = Send(IPCMessage.Delete(request)).GetResponse<DeleteResponse<TEntity>>();
            switch (response.Status)
            {
                case StatusCode.OK:
                    break;
                case StatusCode.Unauthenticated:
                    throw new IPCException("Client not authenticated");
                default:
                    throw new IPCException(response.Messages);
            }
        }

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

            var response = Send(IPCMessage.MultiDelete(request)).GetResponse<MultiDeleteResponse<TEntity>>();
            switch (response.Status)
            {
                case StatusCode.OK:
                    break;
                case StatusCode.Unauthenticated:
                    throw new IPCException("Client not authenticated");
                default:
                    throw new IPCException(response.Messages);
            }
        }

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

            var response = Send(IPCMessage.Query(request)).GetResponse<QueryResponse<TEntity>>();
            if (response.Items != null)
            {
                return response.Items.ToArray<TEntity>();
            }
            else
            {
                return [];
            }
        }

        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 = Send(IPCMessage.Query(request)).GetResponse<QueryResponse<TEntity>>();

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

        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 = Send(IPCMessage.QueryMultiple(request)).GetResponse<MultiQueryResponse>();
            if (response != null)
            {
                return response.Status switch
                {
                    StatusCode.OK => response.Tables,
                    StatusCode.Unauthenticated => throw new IPCException("Client not authenticated"),
                    _ => throw new IPCException(response.Messages),
                };
            }

            return null;
        }

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

            var response = Send(IPCMessage.Save(request)).GetResponse<SaveResponse<TEntity>>();
            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 IPCException("Client not authenticated");
                default:
                    throw new IPCException(response.Messages);
            }
        }

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

            var response = Send(IPCMessage.MultiSave(request)).GetResponse<MultiSaveResponse<TEntity>>();
            switch (response.Status)
            {
                case StatusCode.OK:
                    /*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);
                    }*/
                    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);
                    }
                    break;
                case StatusCode.Unauthenticated:
                    throw new IPCException("Client not authenticated");
                default:
                    throw new IPCException(response.Messages);
            }
        }

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

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

        protected override IValidationData DoValidate(string userid, string password, Guid session)
        {
            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 { UsePIN = usePin };

            if (usePin)
            {
                request.UserID = Encryption.Encrypt(ticks, "wCq9rryEJEuHIifYrxRjxg", true);
                request.Password = Encryption.Encrypt(ticks, "7mhvLnqMwkCAzN+zNGlyyg", true);
                request.PIN = password;
            }
            else
            {
                request.UserID = userid;
                request.Password = password;
            }
            request.Credentials.Platform = ClientFactory.Platform;
            request.Credentials.Version = ClientFactory.Version;
            PrepareRequest(request, false);
            if(session != Guid.Empty)
            {
                request.Credentials.Session = session;
            }

            var response = Send(IPCMessage.Validate(request), 10000).GetResponse<ValidateResponse>();
            if (response != null)
                if (response.Status.Equals(StatusCode.OK))
                {
                    LocalCache.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 IPCException(response.Messages);
                }

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

        public override string Version()
        {
            try
            {
                var request = new VersionRequest();
                PrepareRequest(request, false);
                var response = Send(IPCMessage.Version(request)).GetResponse<VersionResponse>();
                return response.Version;
            }
            catch (Exception)
            {
                return "";
            }
        }

        public override string ReleaseNotes()
        {
            try
            {
                var request = new ReleaseNotesRequest();
                PrepareRequest(request, false);
                var response = Send(IPCMessage.ReleaseNotes(request)).GetResponse<ReleaseNotesResponse>();
                return response.ReleaseNotes;
            }
            catch (Exception)
            {
                return "";
            }
        }

        public override byte[]? Installer()
        {
            try
            {
                var request = new InstallerRequest();
                PrepareRequest(request, false);
                var response = Send(IPCMessage.Installer(request)).GetResponse<InstallerResponse>();
                return response.Installer;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}
