using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using InABox.Client.WebSocket;
using InABox.Core;
using InABox.Remote.Shared;
using InABox.WebSocket.Shared;
using RestSharp;
using RestSharp.Extensions;

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

    public static class URLCache
    {
        private static string? Domain { get; set; }

        private static int? Port { get; set; }

        private static string URL { get; set; } = "";

        private static bool isHTTPS;

        public static bool IsHTTPS { get => isHTTPS; }

        public static void Clear()
        {
            Domain = null;
            Port = null;
            URL = "";
            isHTTPS = false;
        }

        public static string GetURL(string domain, int port)
        {
            domain = domain.Split(new[] { "://" }, StringSplitOptions.RemoveEmptyEntries).Last();
            if (domain != Domain || port != Port)
            {
                var client = new HttpClient { BaseAddress = new Uri($"https://{domain}:{port}") };
                try
                {
                    client.GetAsync("operations").Wait();
                    URL = $"https://{domain}";
                    isHTTPS = true;
                }
                catch (Exception)
                {
                    URL = $"http://{domain}";
                    isHTTPS = false;
                }
                Port = port;
                Domain = domain;
            }
            return URL;
        }
    }

    public static class WebSocketFactory
    {
        private static Dictionary<string, WebSocketClient> Clients = new Dictionary<string, WebSocketClient>();

        public static void StartWebSocket(string url, int port, Guid session)
        {
            url = url.Split(new[] { "://" }, StringSplitOptions.RemoveEmptyEntries).Last();
            var key = $"{url}:{port}${session}";
            if (!Clients.ContainsKey(key))
            {
                Clients[key] = new WebSocketClient(url, port, session);
            }
        }
    }

    public abstract class RemoteClient<TEntity> : BaseClient<TEntity> where TEntity : Entity, new()
    {
        public RemoteClient(string url, int port, bool useSimpleEncryption = false)
        {
            URL = URLCache.GetURL(url, port);
            Port = port;
            UseSimpleEncryption = useSimpleEncryption;
        }

        public string URL { get; set; }
        public int Port { get; set; }

        public bool UseSimpleEncryption { get; set; }

        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 ValidationData DoValidate(Guid session)
        {
            return Validate(
                null, null, false, session);
        }

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

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

        private ValidationData 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", UseSimpleEncryption);
                request.Password = Encryption.Encrypt(ticks, "7mhvLnqMwkCAzN+zNGlyyg", UseSimpleEncryption);
                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.Json, false);
            if (response != null)
                if (response.Status.Equals(StatusCode.OK))
                {
                    if(response.Session != Guid.Empty)
                    {
                        var notifyRequest = new NotifyRequest();
                        // Session is required so that the server can exclude any requests from bad actors
                        notifyRequest.Credentials.Session = response.Session;
                        var notifyResponse = SendRequest<NotifyRequest, NotifyResponse>(notifyRequest, "notify", SerializationFormat.Json, false);
                        if(notifyResponse != null && notifyResponse.Status.Equals(StatusCode.OK))
                        {
                            if (notifyResponse.SocketPort.HasValue)
                            {
                                WebSocketFactory.StartWebSocket(URL, notifyResponse.SocketPort.Value, response.Session);
                            }
                        }
                    }
                    LocalCache.Password = password;
                    return new ValidationData(
                        response.ValidationResult,
                        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(
                ValidationResult.INVALID,
                "",
                Guid.Empty,
                Guid.Empty,
                Guid.Empty,
                null,
                DateTime.MinValue
            );
        }

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

        #region Query Data

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

            PrepareRequest(request);

            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", 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)
        {
            var result = new List<TEntity>();
            var request = new QueryRequest<TEntity>
            {
                Filter = filter,
                Sort = sort
            };

            PrepareRequest(request);

            var response = SendRequest<QueryRequest<TEntity>, QueryResponse<TEntity>>(request, "List", 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();

            request.TableTypes = new Dictionary<string, string>();
            request.Filters = new Dictionary<string, string>();
            request.Columns = new Dictionary<string, string>();
            request.Sorts = new Dictionary<string, string>();

            foreach (var item in queries)
            {
                request.TableTypes[item.Key] = item.Value.Type.EntityName();
                request.Filters[item.Key] = Serialization.Serialize(item.Value.Filter);
                request.Columns[item.Key] = Serialization.Serialize(item.Value.Columns);
                request.Sorts[item.Key] = Serialization.Serialize(item.Value.SortOrder);
            }

            PrepareRequest(request);

            var response = SendRequest<MultiQueryRequest, MultiQueryResponse>(request, "QueryMultiple", 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>();
            request.Item = entity;
            request.AuditNote = auditnote;

            PrepareRequest(request);

            var response = SendRequest<SaveRequest<TEntity>, SaveResponse<TEntity>>(request, "Save", SerializationFormat.Json);
            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);
                    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>();
            request.Items = items;
            request.AuditNote = auditnote;

            PrepareRequest(request);

            var response = SendRequest<MultiSaveRequest<TEntity>, MultiSaveResponse<TEntity>>(request, "MultiSave", SerializationFormat.Json);
            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);
                    }
                    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>();
            request.Item = entity;

            PrepareRequest(request);

            var response = SendRequest<DeleteRequest<TEntity>, DeleteResponse<TEntity>>(request, "Delete", SerializationFormat.Json);
            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(IList<TEntity> entities, string auditnote)
        {
            var items = entities.ToArray();
            var request = new MultiDeleteRequest<TEntity>();
            request.Items = items;
            request.AuditNote = auditnote;

            PrepareRequest(request);

            var response = SendRequest<MultiDeleteRequest<TEntity>, MultiDeleteResponse<TEntity>>(request, "MultiDelete", SerializationFormat.Json);
            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 = code };

            PrepareRequest(request);

            var response = SendRequest<Check2FARequest, Check2FAResponse>(request, "check_2fa", SerializationFormat.Json, 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 uri = new Uri(string.Format("{0}:{1}", URL, Port));
            var cli = new RestClient(uri);
            var req = new RestRequest("/classes", Method.GET) { Timeout = 20000 };

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

        #endregion
    }
}