using Capnp.Rpc;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using FabAccessAPI.Schema;
using NLog;
using S22.Sasl;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace FabAccessAPI
{
    public class API : IAPI
    {
        #region Logger
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();
        #endregion

        #region Private Members
        /// <summary>
        /// Internal client to connect to a server with TCP and RPC
        /// </summary>
        private TcpRpcClient _TcpRpcClient;

        /// <summary>
        /// Private ConnectionData
        /// </summary>
        private ConnectionData _ConnectionData;

        /// <summary>
        /// Private ServerData
        /// </summary>
        private ServerData _ServerData;

        /// <summary>
        /// Private Session
        /// </summary>
        private Session _Session;

        /// <summary>
        /// Private Bootstrap
        /// </summary>
        private IBootstrap _Bootstrap;

        /// <summary>
        /// Timer to check connection status
        /// </summary>
        private readonly Timer _ConnectionHeatbeat;

        /// <summary>
        /// Semaphore to connect only once
        /// </summary>
        private static SemaphoreSlim _ConnectSemaphore = new SemaphoreSlim(1, 1);
        #endregion

        #region Constructors

        public API()
        {
            _ConnectionHeatbeat = new Timer(Heartbeat, null, 1000, 1000);
        }
        #endregion

        #region Members
        /// <summary>
        /// State of the conneciton, can the API-Service connect to a server
        /// </summary>
        public bool CanConnect
        {
            get
            {
                return _ConnectionData != null;
            }
        }

        /// <summary>
        /// State of the conneciton, is the API-Service connecting to a server
        /// </summary>
        public bool IsConnecting
        {
            get
            {
                return _TcpRpcClient != null && _ConnectionData != null;
            }
        }

        /// <summary>
        /// State of the conneciton, is the API-Service connected to a server
        /// </summary>
        public bool IsConnected
        {
            get
            {
                return _TcpRpcClient != null && _TcpRpcClient.State == ConnectionState.Active;
            }
        }

        /// <summary>
        /// Information about the connection
        /// </summary>
        /// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connected to a server </exception>
        public ConnectionData ConnectionData
        { 
            get
            {
                if(_ConnectionData == null || !IsConnecting)
                {
                    throw new InvalidOperationException();
                }
                else
                {
                    return _ConnectionData;
                }
            }
            private set
            {
                _ConnectionData = value;
            }
        }

        /// <summary>
        /// Information about the server
        /// Is only avalible if the API-Service is connected
        /// </summary>
        /// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
        public ServerData ServerData
        {
            get
            {
                if (_ServerData == null || !IsConnected)
                {
                    throw new InvalidOperationException();
                }
                else
                {
                    return _ServerData;
                }
            }
            private set
            {
                _ServerData = value;
            }
        }
        #endregion

        #region Events
        /// <summary>
        /// Event on changes in connection status
        /// </summary>
        public event EventHandler<ConnectionStatusChanged> ConnectionStatusChanged;

        /// <summary>
        /// Unbind all handlers from EventHandler<ConnectionStatusChanged>
        /// </summary>
        public void UnbindEventHandler()
        {
            if (ConnectionStatusChanged != null)
            {
                Log.Trace("Eventhandlers unbinded");
                foreach (Delegate d in ConnectionStatusChanged.GetInvocationList())
                {
                    ConnectionStatusChanged -= (EventHandler<ConnectionStatusChanged>)d;
                }
            }
        }

        /// <summary>
        /// Eventhandler for TcpRpcConnectionChanged
        /// Track connection loss and publish i in ConnectionStatusChanged
        /// </summary>
        public void OnTcpRpcConnectionChanged(object sender, ConnectionStateChange args)
        {
            if (args.LastState == ConnectionState.Active && args.NewState == ConnectionState.Down)
            {
                Log.Trace("TcpRpcClient Event ConnectionLoss");
                ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.ConnectionLoss);
            }
        }
        #endregion

        #region Session
        /// <summary>
        /// Get session after connection
        /// </summary>
        /// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
        public Session Session
        {
            get
            {
                if (_Session == null || !IsConnected)
                {
                    throw new InvalidOperationException();
                }
                else
                {
                    return _Session;
                }
            }
            private set
            {
                _Session = value;
            }
        }

        #endregion

        #region Methods
        /// <summary>
        /// Connect to server with ConnectionData
        /// If connection lost, the API-Server will try to reconnect
        /// </summary>
        /// <param name="connectionData"> Data to establish a connection to a server </param>
        /// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
        /// <exception cref="AuthenticationException"> When API-Service can connect to a server but can not authenticate </exception>
        /// <exception cref="InvalidOperationException"> When API-Service is allready connected </exception>
        public async Task Connect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
        {
            await _ConnectSemaphore.WaitAsync();
            try
            {
                if (IsConnected)
                {
                    Log.Warn("API already connected");
                    throw new InvalidOperationException();
                }

                if (tcpRpcClient == null)
                {
                    tcpRpcClient = new TcpRpcClient();
                }

                try
                {
                    await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);

                    _Bootstrap = tcpRpcClient.GetMain<IBootstrap>();
                    ServerData = await _GetServerData(_Bootstrap);

                    Session = await _Authenticate(connectionData).ConfigureAwait(false);
                    ConnectionData = connectionData;

                    _TcpRpcClient = tcpRpcClient;
                    tcpRpcClient.ConnectionStateChanged += OnTcpRpcConnectionChanged;
                    ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Connected);
                    Log.Info("API connected");
                }
                catch (System.Exception ex)
                {
                    Log.Warn(ex, "API connect failed");
                    throw ex;
                }
            }
            finally
            {
                _ConnectSemaphore.Release();
            }
        }

        /// <summary>
        /// Disconnect from a server
        /// </summary>
        /// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connect </exception>
        public Task Disconnect()
        {
            if (IsConnected)
            {
                _TcpRpcClient.Dispose();
            }

            _Bootstrap = null;
            _TcpRpcClient = null;

            Session = null;
            ConnectionData = null;
            ServerData = null;

            ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Disconnected);

            Log.Info("API disconnected");
            return Task.CompletedTask;
        }

        /// <summary>
        /// Try to connect to a server and get ServerData
        /// The connection is not maintained
        /// </summary>
        /// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
        public async Task<ServerData> TryToConnect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
        {
            if (tcpRpcClient == null)
            {
                tcpRpcClient = new TcpRpcClient();
            }

            await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
            IBootstrap bootstrap = tcpRpcClient.GetMain<IBootstrap>();

            ServerData serverData = await _GetServerData(bootstrap).ConfigureAwait(false);

            tcpRpcClient.Dispose();

            return serverData;
        }

        /// <summary>
        /// Public Wrapper to run HeartbeatAsync
        /// </summary>
        public void Heartbeat(object state)
        {
            _ = HeartbeatAsync();
        }
        #endregion

        #region Private Methods


        private async Task HeartbeatAsync()
        {
            if(!IsConnected && CanConnect)
            {
                await Connect(ConnectionData).ConfigureAwait(false);
            }
        }

        /// <summary>
        /// Validate Certificate
        /// TODO: Do some validation
        /// </summary>
        private bool _RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            // TODO Cert Check
            return true;
        }

        /// <summary>
        /// Injects SSL as Midlayer in TCPRPCConnection
        /// </summary>
        /// <exception cref="ConnectionException"></exception>
        private Stream InjectSSL(Stream tcpstream)
        {
            SslStream sslStream = new SslStream(tcpstream, false, new RemoteCertificateValidationCallback(_RemoteCertificateValidationCallback));
            try
            {
                sslStream.ReadTimeout = 2000;
                sslStream.AuthenticateAsClient("bffhd");
                sslStream.ReadTimeout = -1;
                
                return sslStream;
            }
            catch (System.Security.Authentication.AuthenticationException exception)
            {
                sslStream.Close();
                Log.Warn(exception);
                throw new ConnectionException("TLS failed", exception);
            }
            catch(IOException exception)
            {
                sslStream.Close();
                Log.Warn(exception);
                throw new ConnectionException("TLS failed", new Exceptions.TimeoutException("TLS timeout", exception));
            }
        }

        /// <summary>
        /// Connect async to a server with ConnectionData
        /// </summary>
        /// <exception cref="ConnectionException">Based on RPC Exception</exception>
        private async Task _ConnectAsync(TcpRpcClient tcprpcClient, ConnectionData connectionData)
        {
            tcprpcClient.InjectMidlayer(InjectSSL);
            
            try
            {
                Task timeoutTask = Task.Delay(3000);
                tcprpcClient.Connect(connectionData.Host.Host, connectionData.Host.Port);
                await await Task.WhenAny(tcprpcClient.WhenConnected, timeoutTask);

                if (timeoutTask.IsCompleted)
                {
                    Exceptions.TimeoutException timeoutException = new Exceptions.TimeoutException();
                    Log.Warn(timeoutException);
                    throw new ConnectionException("Connection timeout", timeoutException);
                }
            }
            catch (RpcException exception) when (string.Equals(exception.Message, "TcpRpcClient is unable to connect", StringComparison.Ordinal))
            {
                Log.Warn(exception);
                throw new ConnectionException("RPC Connecting failed", exception);
            }
        }

        /// <summary>
        /// Authenticate connection with ConnectionData
        /// </summary>
        /// <param name="connectionData"> Data to establish a connection to a server </param>
        /// <exception cref="AuthenticationException"></exception>
        private async Task<Session> _Authenticate(ConnectionData connectionData)
        {
            IAuthentication? authentication = await _Bootstrap.CreateSession(SASLMechanism.ToString(connectionData.Mechanism)).ConfigureAwait(false);

            try
            {
                return await _SASLAuthenticate(authentication, SASLMechanism.ToString(connectionData.Mechanism), connectionData.Properties).ConfigureAwait(false);
            }
            catch (System.Exception exception)
            {
                Log.Warn(exception, "API authenticating failed");
                AuthenticationException authenticationException = new AuthenticationException("Authentication failed", exception);

                throw authenticationException;
            }
        }

        /// <summary>
        /// Authenticate with SASL
        /// </summary>
        /// <exception cref="BadMechanismException"></exception>
        /// <exception cref="InvalidCredentialsException"></exception>
        /// <exception cref="AuthenticationFailedException"></exception>
        private async Task<Session> _SASLAuthenticate(IAuthentication authentication, string mech, Dictionary<string, object> properties)
        {
            SaslMechanism? saslMechanism = SaslFactory.Create(mech);
            foreach (KeyValuePair<string, object> entry in properties)
            {
                saslMechanism.Properties.Add(entry.Key, entry.Value);
            }

            byte[] data = new byte[0];

            if (saslMechanism.HasInitial)
            {
                data = saslMechanism.GetResponse(new byte[0]);
            }

            Response? response = await authentication.Step(data);
            while (!saslMechanism.IsCompleted)
            {
                if (response.Failed != null)
                {
                    break;
                }
                if (response.Challenge != null)
                {
                    byte[]? additional = saslMechanism.GetResponse(response.Challenge.ToArray());
                    response = await authentication.Step(additional);
                }
                else
                {
                    throw new AuthenticationFailedException();
                }
            }

            if (response.Successful != null)
            {
                return response.Successful.Session;
            }
            else if (response.Failed != null)
            {
                switch (response.Failed.Code)
                {
                    case Response.Error.badMechanism:
                        throw new BadMechanismException();
                    case Response.Error.invalidCredentials:
                        throw new InvalidCredentialsException();
                    case Response.Error.aborted:
                    case Response.Error.failed:
                    default:
                        throw new AuthenticationFailedException(response.Failed.AdditionalData.ToArray());
                }
            }
            else
            {
                throw new AuthenticationFailedException();
            }
        }

        /// <summary>
        /// Get ServerData from server with tcprpcconnection
        /// </summary>
        private async Task<ServerData> _GetServerData(IBootstrap bootstrap)
        {
            ServerData serverData = new ServerData()
            {
                APIVersion = await bootstrap.GetAPIVersion().ConfigureAwait(false),
                Mechanisms = new List<string>(await bootstrap.Mechanisms().ConfigureAwait(false)),
                ServerName = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item1,
                ServerRelease = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item2,
            };

            return serverData;
        }
        #endregion
    }
}
