"""
  DIRAC class to execute services

  In the most common case, DIRAC services are executed using the dirac-service command.
  dirac-service accepts a list positional arguments. These arguments have the form:
  [DIRAC System Name]/[DIRAC Service Name]
  dirac-service then:
  - produces a instance of ServiceReactor
  - loads the required modules using the ServiceReactor.loadAgentModules method
  - starts the execution loop using the ServiceReactor.serve() method

  Service modules must be placed under the Service directory of a DIRAC System.
  DIRAC Systems are called XXXSystem where XXX is the [DIRAC System Name], and
  must inherit from the base class RequestHandler

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import time
import socket
import signal
import os
import sys
import multiprocessing

try:
    import selectors
except ImportError:
    import selectors2 as selectors

from DIRAC import gLogger, S_OK, S_ERROR
from DIRAC.Core.DISET.private.Service import Service
from DIRAC.Core.DISET.private.GatewayService import GatewayService
from DIRAC.Core.DISET.RequestHandler import RequestHandler
from DIRAC.Core.Utilities import Time
from DIRAC.Core.Base.private.ModuleLoader import ModuleLoader
from DIRAC.Core.DISET.private.Protocols import gProtocolDict
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
from DIRAC.ConfigurationSystem.Client import PathFinder


__RCSID__ = "$Id$"

#: Time during which the service does not accept new requests and handles those in the queue, if the backlog is too large
#: This sleep is repeated for as long as Service.wantsThrottle is truthy
THROTTLE_SERVICE_SLEEP_SECONDS = 0.25


class ServiceReactor(object):

    __transportExtraKeywords = {
        "SSLSessionTimeout": False,
        "IgnoreCRLs": False,
        "PacketTimeout": "timeout",
        "SocketBacklog": False,
    }

    def __init__(self):
        self.__services = {}
        self.__alive = True
        self.__loader = ModuleLoader("Service", PathFinder.getServiceSection, RequestHandler, moduleSuffix="Handler")
        self.__maxFD = 0
        self.__listeningConnections = {}
        self.__stats = ReactorStats()
        self.__processes = []

    def initialize(self, servicesList):
        try:
            servicesList.remove(GatewayService.GATEWAY_NAME)
            self.__services[GatewayService.GATEWAY_NAME] = GatewayService()
        except ValueError:
            # No GW in the service list
            pass

        result = self.__loader.loadModules(servicesList)
        if not result["OK"]:
            return result
        self.__serviceModules = self.__loader.getModules()

        for serviceName in self.__serviceModules:
            self.__services[serviceName] = Service(self.__serviceModules[serviceName])

        # Loop again to include the GW in case there is one (included in the __init__)
        for serviceName in self.__services:
            gLogger.info("Initializing %s" % serviceName)
            result = self.__services[serviceName].initialize()
            if not result["OK"]:
                return result
        return S_OK()

    def closeListeningConnections(self):
        gLogger.info("Closing listening connections...")
        for svcName in self.__listeningConnections:
            if "transport" in self.__listeningConnections[svcName]:
                try:
                    self.__listeningConnections[svcName]["transport"].close()
                except Exception:
                    pass
                del self.__listeningConnections[svcName]["transport"]
        gLogger.info("Connections closed")

    def __createListeners(self):
        for serviceName in self.__services:
            svcCfg = self.__services[serviceName].getConfig()
            protocol = svcCfg.getProtocol()
            port = svcCfg.getPort()
            if not port:
                return S_ERROR("No port defined for service %s" % serviceName)
            if protocol not in gProtocolDict:
                return S_ERROR("Protocol %s is not known for service %s" % (protocol, serviceName))
            self.__listeningConnections[serviceName] = {"port": port, "protocol": protocol}
            transportArgs = {}
            for kw in ServiceReactor.__transportExtraKeywords:
                value = svcCfg.getOption(kw)
                if value:
                    ikw = ServiceReactor.__transportExtraKeywords[kw]
                    if ikw:
                        kw = ikw
                    if kw == "timeout":
                        value = int(value)
                    transportArgs[kw] = value
            gLogger.verbose("Initializing %s transport" % protocol, svcCfg.getURL())
            transport = gProtocolDict[protocol]["transport"](("", port), bServerMode=True, **transportArgs)
            retVal = transport.initAsServer()
            if not retVal["OK"]:
                return S_ERROR(
                    "Cannot start listening connection for service %s: %s" % (serviceName, retVal["Message"])
                )
            self.__listeningConnections[serviceName]["transport"] = transport
            self.__listeningConnections[serviceName]["socket"] = transport.getSocket()
        return S_OK()

    def stopChildProcesses(self, _sig, frame):
        """
        It is used to properly stop the service when more than one process are used.
        In principle this is doing the job of runsv, becuase runsv only send a sigterm to the parent process...

        :param int _sig: the signal sent to the process
        :param object frame: execution frame which contains the child processes
        """

        handler = frame.f_locals.get("self")
        if handler and isinstance(handler, ServiceReactor):
            handler.stopAllProcess()

        for child in frame.f_locals.get("children", []):
            gLogger.info("Stopping child processes: %d" % child)
            os.kill(child, signal.SIGTERM)

        sys.exit(0)

    def serve(self):
        result = self.__createListeners()
        if not result["OK"]:
            self.__closeListeningConnections()
            return result
        for svcName in self.__listeningConnections:
            gLogger.always("Listening at %s" % self.__services[svcName].getConfig().getURL())

        isMultiProcessingAllowed = False
        for svcName in self.__listeningConnections:
            if self.__services[svcName].getConfig().getCloneProcesses() > 0:
                isMultiProcessingAllowed = True
                break
        if isMultiProcessingAllowed:
            signal.signal(signal.SIGTERM, self.stopChildProcesses)
            signal.signal(signal.SIGINT, self.stopChildProcesses)
            for svcName in self.__listeningConnections:
                clones = self.__services[svcName].getConfig().getCloneProcesses()
                for i in range(1, clones):
                    p = multiprocessing.Process(target=self.__startCloneProcess, args=(svcName, i))
                    self.__processes.append(p)
                    p.start()
                    gLogger.always("Started clone process %s for %s" % (i, svcName))

        while self.__alive:
            self.__acceptIncomingConnection()

    def stopAllProcess(self):
        """
        It stops all the running processes.
        """
        for process in self.__processes:
            gLogger.info("Stopping: PID=%d, name=%s, parentPid=%d" % (process.pid, process.name, process._parent_pid))
            if process.is_alive():
                process.terminate()
                self.__processes.remove(process)

    # This function runs in a different process
    def __startCloneProcess(self, svcName, i):
        self.__services[svcName].setCloneProcessId(i)
        self.__alive = i
        while self.__alive:
            self.__acceptIncomingConnection(svcName)

    def __getListeningSelector(self, svcName=False):
        sel = selectors.DefaultSelector()
        if svcName:
            sel.register(self.__listeningConnections[svcName]["socket"], selectors.EVENT_READ, svcName)
        else:
            for svcName, svcInfo in self.__listeningConnections.items():
                sel.register(svcInfo["socket"], selectors.EVENT_READ, svcName)
        return sel

    def __acceptIncomingConnection(self, svcName=False):
        """
        This method just gets the incoming connection, checks IP address
        and generates job. SSL/TLS handshake and execution of the remote call
        are made by Service._processInThread() (in another thread) so
        the service can accept other clients while another thread handling remote call

        :param str svcName=False: Name of a service if you use multiple
                                  services at the same time
        """
        sel = self.__getListeningSelector(svcName)
        while self.__alive:
            try:
                events = sel.select(timeout=10)
                if len(events) == 0:
                    return
                for key, event in events:
                    if event & selectors.EVENT_READ:
                        svcName = key.data
                        retVal = self.__listeningConnections[svcName]["transport"].acceptConnection()
                        if not retVal["OK"]:
                            gLogger.warn("Error while accepting a connection: ", retVal["Message"])
                            return
                        clientTransport = retVal["Value"]
            except socket.error:
                return
            self.__maxFD = max(self.__maxFD, clientTransport.oSocket.fileno())
            # Is it banned?
            clientIP = clientTransport.getRemoteAddress()[0]
            if clientIP in Registry.getBannedIPs():
                gLogger.warn("Client connected from banned ip %s" % clientIP)
                clientTransport.close()
                continue
            # Handle connection
            self.__stats.connectionStablished()
            self.__services[svcName].handleConnection(clientTransport)
            while self.__services[svcName].wantsThrottle:
                gLogger.warn("Sleeping as service requested throttling", svcName)
                time.sleep(THROTTLE_SERVICE_SLEEP_SECONDS)
            # Renew context?
            now = time.time()
            renewed = False
            for svcName in self.__listeningConnections:
                tr = self.__listeningConnections[svcName]["transport"]
                if now - tr.latestServerRenewTime() > self.__services[svcName].getConfig().getContextLifeTime():
                    result = tr.renewServerContext()
                    if result["OK"]:
                        renewed = True
            if renewed:
                sel = self.__getListeningSelector()

    def __closeListeningConnections(self):
        for svcName in self.__listeningConnections:
            lc = self.__listeningConnections[svcName]
            if "transport" in lc and lc["transport"]:
                lc["transport"].close()


class ReactorStats(object):
    def __init__(self):
        self.__connections = 0
        self.__established = 0
        self.__startTime = Time.dateTime()

    def connectionStablished(self):
        self.__connections += 1
