using H.Formatters;
using H.Pipes;
using H.Pipes.AccessControl;
using InABox.API;
using InABox.Clients;
using InABox.Core;
using InABox.IPC.Shared;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipes;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;

namespace Piping
{
    using PipeResponse = PipeRequest;

    public class PipeIPCServer : IDisposable
    {
        PipeServer<PipeRequest> Server;

        public PipeIPCServer(string name)
        {
            Server = new PipeServer<PipeRequest>(name);

            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);

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

        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),
                _ => null
            };
        }

        private static PipeResponse QueryMultiple(PipeRequest request)
        {
            var response = RestService.QueryMultiple(request.GetRequest<MultiQueryRequest>(), true);
            return request.Respond(response);
        }

        private static PipeResponse Validate(PipeRequest request)
        {
            var response = RestService.Validate(request.GetRequest<ValidateRequest>());
            return request.Respond(response);
        }

        private static PipeResponse Ping(PipeRequest request) => request.Respond(new PingResponse().Status(StatusCode.OK));

        private static PipeResponse Info(PipeRequest request)
        {
            var response = RestService.Info(request.GetRequest<InfoRequest>());
            return request.Respond(response);
        }
        
        private static PipeResponse Check2FA(PipeRequest request)
        {
            var response = RestService.Check2FA(request.GetRequest<Check2FARequest>());
            return request.Respond(response);
        }

        private static PipeResponse Query<T>(PipeRequest request) where T : Entity, new()
        {
            var response = RestService<T>.List(request.GetRequest<QueryRequest<T>>());
            return request.Respond(response);
        }

        private static PipeResponse Save<T>(PipeRequest request) where T : Entity, new()
        {
            var response = RestService<T>.Save(request.GetRequest<SaveRequest<T>>());
            return request.Respond(response);
        }
        private static PipeResponse MultiSave<T>(PipeRequest request) where T : Entity, new()
        {
            var response = RestService<T>.MultiSave(request.GetRequest<MultiSaveRequest<T>>());
            return request.Respond(response);
        }

        private static PipeResponse Delete<T>(PipeRequest request) where T : Entity, new()
        {
            var response = RestService<T>.Delete(request.GetRequest<DeleteRequest<T>>());
            return request.Respond(response);
        }
        private static PipeResponse MultiDelete<T>(PipeRequest request) where T : Entity, new()
        {
            var response = RestService<T>.MultiDelete(request.GetRequest<MultiDeleteRequest<T>>());
            return request.Respond(response);
        }

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

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

        private void Server_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<PipeRequest?> 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.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(null, new object[] { e.Message }) as PipeResponse;
                    e.Connection.WriteAsync(response);
                }
                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));
                        }
                    }
                }
            });
        }

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

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

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