using H.Pipes;
using H.Pipes.AccessControl;
using H.Pipes.Args;
using InABox.API;
using InABox.Clients;
using InABox.Core;
using InABox.Server;
using System.IO.Pipes;
using System.Reflection;
using System.Security.Principal;
using H.Formatters;

namespace InABox.IPC
{
    public class IPCServer : IDisposable
    {
        PipeServer<IPCMessage> Server;

        IPCPushState PushState = new();

        public IPCServer(string name)
        {
            Server = new PipeServer<IPCMessage>(name, formatter:new BinaryFormatter());

            #if WINDOWS
            SetPipeSecurity();
            #endif

            Server.ClientConnected += Server_ClientConnected;
            Server.ClientDisconnected += Server_ClientDisconnected;
            Server.MessageReceived += Server_MessageReceived;
            Server.ExceptionOccurred += Server_ExceptionOccurred;
        }

        private void SetPipeSecurity()
        {
            #pragma warning disable CA1416
            
            var pipeSecurity = new PipeSecurity();

            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSid, null), PipeAccessRights.ReadWrite,
                System.Security.AccessControl.AccessControlType.Allow));
            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null), PipeAccessRights.ReadWrite,
                System.Security.AccessControl.AccessControlType.Allow));
            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.ReadWrite,
                System.Security.AccessControl.AccessControlType.Allow));

            Server.SetPipeSecurity(pipeSecurity);
            
            #pragma warning restore CA1416
        }

        private void Server_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
        {
            Logger.Send(LogType.Error, "", $"Exception Occurred: {e.Exception.Message}");
        }

        public void Start()
        {
            Server.StartAsync().Wait();
        }

        private static List<Type>? _persistentRemotable;

        private static Type? GetEntity(string entityName)
        {
            _persistentRemotable ??= CoreUtils.TypeList(
                e => e.IsSubclassOf(typeof(Entity)) &&
                     e.GetInterfaces().Contains(typeof(IRemotable)) &&
                     e.GetInterfaces().Contains(typeof(IPersistent))).ToList();
            return _persistentRemotable.FirstOrDefault(x => x.Name == entityName);
        }

        private static Type? GetResponseType(Method method, string? entityName)
        {
            if(entityName != null)
            {
                var entityType = GetEntity(entityName);
                if(entityType != null)
                {
                    var response = method switch
                    {
                        Method.Query => typeof(QueryResponse<>).MakeGenericType(entityType),
                        Method.Delete => typeof(DeleteResponse<>).MakeGenericType(entityType),
                        Method.MultiDelete => typeof(MultiDeleteResponse<>).MakeGenericType(entityType),
                        Method.Save => typeof(SaveResponse<>).MakeGenericType(entityType),
                        Method.MultiSave => typeof(MultiSaveResponse<>).MakeGenericType(entityType),
                        _ => null
                    };
                    if (response != null) return response;
                }
            }
            return method switch
            {
                Method.QueryMultiple => typeof(MultiQueryResponse),
                Method.Validate => typeof(ValidateResponse),
                Method.Check2FA => typeof(Check2FAResponse),
                Method.Version => typeof(VersionResponse),
                Method.Installer => typeof(InstallerResponse),
                Method.ReleaseNotes => typeof(ReleaseNotesResponse),
                _ => null
            };
        }
        private class RequestData
        {
            public ConnectionMessageEventArgs<IPCMessage?> e { get; }

            public RequestData(ConnectionMessageEventArgs<IPCMessage?> e)
            {
                this.e = e;
            }
        }

        private IPCMessage QueryMultiple(IPCMessage request, RequestData data)
        {
            var response = RestService.QueryMultiple(request.GetRequest<MultiQueryRequest>(), true, Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Validate(IPCMessage request, RequestData data)
        {
            var response = RestService.Validate(request.GetRequest<ValidateRequest>(), Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Ping(IPCMessage request, RequestData data) => request.Respond(new PingResponse().Status(StatusCode.OK));

        private IPCMessage Info(IPCMessage request, RequestData data)
        {
            var response = RestService.Info(request.GetRequest<InfoRequest>(), Logger.New());
            return request.Respond(response);
        }
        
        private IPCMessage Check2FA(IPCMessage request, RequestData data)
        {
            var response = RestService.Check2FA(request.GetRequest<Check2FARequest>(), Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Query<T>(IPCMessage request, RequestData data) where T : Entity, new()
        {
            var response = RestService<T>.List(request.GetRequest<QueryRequest<T>>(), Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Save<T>(IPCMessage request, RequestData data) where T : Entity, new()
        {
            var response = RestService<T>.Save(request.GetRequest<SaveRequest<T>>(), Logger.New());
            return request.Respond(response);
        }
        private IPCMessage MultiSave<T>(IPCMessage request, RequestData data) where T : Entity, new()
        {
            var response = RestService<T>.MultiSave(request.GetRequest<MultiSaveRequest<T>>(), Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Delete<T>(IPCMessage request, RequestData data) where T : Entity, new()
        {
            var response = RestService<T>.Delete(request.GetRequest<DeleteRequest<T>>(), Logger.New());
            return request.Respond(response);
        }
        private IPCMessage MultiDelete<T>(IPCMessage request, RequestData data) where T : Entity, new()
        {
            var response = RestService<T>.MultiDelete(request.GetRequest<MultiDeleteRequest<T>>(), Logger.New());
            return request.Respond(response);
        }

        private IPCMessage Version(IPCMessage request, RequestData data) => 
            request.Respond(new VersionResponse { Version = UpdateData.GetUpdateVersion() });
        private IPCMessage ReleaseNotes(IPCMessage request, RequestData data) => 
            request.Respond(new ReleaseNotesResponse { ReleaseNotes = UpdateData.GetReleaseNotes() });
        private IPCMessage Installer(IPCMessage request, RequestData data) => 
            request.Respond(new InstallerResponse { Installer = UpdateData.GetUpdateInstaller() });

        private static readonly MethodInfo QueryMethod = GetMethod(nameof(Query));
        private static readonly MethodInfo SaveMethod = GetMethod(nameof(Save));
        private static readonly MethodInfo MultiSaveMethod = GetMethod(nameof(MultiSave));
        private static readonly MethodInfo DeleteMethod = GetMethod(nameof(Delete));
        private static readonly MethodInfo MultiDeleteMethod = GetMethod(nameof(MultiDelete));
        private static readonly MethodInfo QueryMultipleMethod = GetMethod(nameof(QueryMultiple));
        private static readonly MethodInfo ValidateMethod = GetMethod(nameof(Validate));
        private static readonly MethodInfo Check2FAMethod = GetMethod(nameof(Check2FA));
        private static readonly MethodInfo PingMethod = GetMethod(nameof(Ping));
        private static readonly MethodInfo InfoMethod = GetMethod(nameof(Info));
        private static readonly MethodInfo VersionMethod = GetMethod(nameof(Version));
        private static readonly MethodInfo ReleaseNotesMethod = GetMethod(nameof(ReleaseNotes));
        private static readonly MethodInfo InstallerMethod = GetMethod(nameof(Installer));

        private static MethodInfo GetMethod(string name) =>
            typeof(IPCServer).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance)
            ?? throw new Exception($"Invalid method '{name}'");

        private void Server_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<IPCMessage?> e)
        {
            Task.Run(() =>
            {
                var start = DateTime.Now;
                try
                {
                    if (e.Message == null) throw new Exception($"Invalid message");

                    var method = e.Message.Method switch
                    {
                        Method.Query => QueryMethod,
                        Method.QueryMultiple => QueryMultipleMethod,
                        Method.Delete => DeleteMethod,
                        Method.MultiDelete => MultiDeleteMethod,
                        Method.Save => SaveMethod,
                        Method.MultiSave => MultiSaveMethod,
                        Method.Check2FA => Check2FAMethod,
                        Method.Validate => ValidateMethod,
                        Method.Ping => PingMethod,
                        Method.Info => InfoMethod,
                        Method.Version => VersionMethod,
                        Method.ReleaseNotes => ReleaseNotesMethod,
                        Method.Installer => InstallerMethod,
                        Method.None or _ => throw new Exception($"Invalid method '{e.Message.Method}'")
                    };

                    if (e.Message.Type != null)
                    {
                        var entityType = GetEntity(e.Message.Type) ?? throw new Exception($"No entity '{e.Message.Type}'");
                        method = method.MakeGenericMethod(entityType);
                    }

                    var response = method.Invoke(this, new object[] { e.Message, new RequestData(e) }) as IPCMessage;
                    e.Connection.WriteAsync(response).ContinueWith(task =>
                    {
                        if (task.Exception != null)
                        {
                            Logger.Send(LogType.Error, "", $"Error in response: {CoreUtils.FormatException(task.Exception)}");
                        }
                    });
                }
                catch (Exception err)
                {
                    Logger.Send(LogType.Error, "", err.Message);
                    if (e.Message != null)
                    {
                        var responseType = GetResponseType(e.Message.Method, e.Message.Type);
                        if (responseType != null)
                        {
                            var response = (Activator.CreateInstance(responseType) as Response)!;
                            response.Status = StatusCode.Error;
                            response.Messages.Add(err.Message);
                            e.Connection.WriteAsync(e.Message.Respond(response)).ContinueWith(task =>
                            {
                                if (task.Exception != null)
                                {
                                    Logger.Send(LogType.Error, "", $"Error in response: {CoreUtils.FormatException(task.Exception)}");
                                }
                            });
                        }
                    }
                }
            });
        }

        private void Server_ClientDisconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCMessage> e)
        {
            Logger.Send(LogType.Information, "", "Client Disconnected");

            var sessionID = PushState.SessionMap.Where(x => x.Value.Connection == e.Connection).FirstOrDefault().Key;
            PushState.SessionMap.TryRemove(sessionID, out var session);

            e.Connection.DisposeAsync();
        }

        private void Server_ClientConnected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCMessage> e)
        {
            Logger.Send(LogType.Information, "", "Client Connected");
        }

        public void Dispose()
        {
            Server.DisposeAsync().AsTask().Wait();
        }
        ~IPCServer()
        {
            Dispose();
        }
    }
}
