from smpp.twisted.protocol import SMPPServerProtocol
from smpp.pdu import pdu_types

from zope.interface import Interface

from twisted.internet.protocol import ServerFactory
# Jasmin update: direct import of UsernamePassword instead of cred
from twisted.cred.credentials import UsernamePassword
from twisted.cred import error
from twisted.internet import defer

import logging
import collections


LOG_CATEGORY="smpp.twisted.server"

#pylint: disable=inherit-non-class
class IAuthenticatedSMPP(Interface):
    pass

# Jasmin update: using UsernamePassword instead of cred.credentials.UsernamePassword
class UsernameAndPasswordAndIP(UsernamePassword):
    def __init__(self, username, password, client_ip_address):
        self.username = username
        self.password = password
        self.client_ip_address = client_ip_address

class SMPPServerFactory(ServerFactory):

    protocol = SMPPServerProtocol

    def __init__(self, config, auth_portal):
        self.config = config
        self.log = logging.getLogger(LOG_CATEGORY)
        # A dict of protocol instances for each of the current connections,
        # indexed by system_id
        self.bound_connections = {}
        self._auth_portal = auth_portal

    def getConfig(self):
        return self.config

    def getBoundConnectionCount(self, system_id):
        if system_id in self.bound_connections:
            return self.bound_connections[system_id].getMaxTransmitReceiveBindCount()
        else:
            return 0

    def getBoundConnectionCountsStr(self, system_id):
        if system_id in self.bound_connections:
            bind_counts = self.bound_connections[system_id].getBindingCountByType()
            bound_connections_count = []
            for key, value in bind_counts.items():
                bound_connections_count.append("%s: %d" % (key, value))
            bound_connections_str = ', '.join(bound_connections_count)
            return bound_connections_str
        else:
            return '0'

    def addBoundConnection(self, connection):
        """
        Add a protocol instance to the list of current connections.
        @param connection: An instance of SMPPServerProtocol
        """
        system_id = connection.system_id
        self.log.debug('Adding SMPP binding for %s' % system_id)
        if not system_id in self.bound_connections:
            self.bound_connections[system_id] = SMPPBindManager(system_id)
        self.bound_connections[system_id].addBinding(connection)
        bind_type = connection.bind_type
        self.log.info("Added %s bind for '%s'. Active binds: %s. Max binds: %s" % (bind_type, system_id, self.getBoundConnectionCountsStr(system_id), self.config.systems[system_id]['max_bindings']))

    def removeConnection(self, connection):
        """
        Remove a protocol instance (SMPP binding) from the list of current connections.
        @param connection: An instance of SMPPServerProtocol
        """
        if connection.system_id is None:
            self.log.debug("SMPP connection attempt failed without binding.")
        else:
            system_id = connection.system_id
            bind_type = connection.bind_type
            self.bound_connections[system_id].removeBinding(connection)
            self.log.info("Dropped %s bind for '%s'. Active binds: %s. Max binds: %s" % (bind_type, system_id, self.getBoundConnectionCountsStr(system_id), self.config.systems[system_id]['max_bindings']))
            # If this is the last binding for this service then remove the BindManager
            if self.bound_connections[system_id].getBindingCount() == 0:
                self.bound_connections.pop(system_id)

    def getBoundConnections(self, system_id):
        return self.bound_connections.get(system_id)

    def login(self, system_id, password, client_ip_address):
        if self._auth_portal is not None:
            return self._auth_portal.login(
                UsernameAndPasswordAndIP(system_id, password, client_ip_address),
                None,
                IAuthenticatedSMPP
            )
        raise error.UnauthorizedLogin()

    def canOpenNewConnection(self, system_id, bind_type):
        """
        Checks if the gateway with the specified system_id can open a new
        connection, as it hasn't exceeded its maximum number of bindings.
        @param bind_type: One of smpp.pdu.pdu_types.CommandId
        """
        existing_bindings_for_id = self.getBoundConnections(system_id)
        if existing_bindings_for_id:
            connections_count = existing_bindings_for_id.getBindingCountForType(bind_type)
            return connections_count < self.config.systems[system_id]['max_bindings']
        else:
            # No existing bindings for this system_id
            return self.config.systems[system_id]['max_bindings'] > 0

    def unbindGateway(self, system_id):
        """ Unbinds and disconnects all the bindings for the given system_id.  """
        bind_mgr = self.getBoundConnections(system_id)
        if bind_mgr:
            unbinds_list = []
            for bind in bind_mgr:
                unbinds_list.append(bind.getDisconnectedDeferred())
                bind.unbindAndDisconnect()
            d = defer.DeferredList(unbinds_list)
        else:
            d = defer.succeed(None)

        return d

    def unbindAndRemoveGateway(self, system_id):
        '''
        Removes a running gateway from the config so they will be unable to rebind.
        Any attempt to bind while unbinding will receive a ESME_RBINDFAIL error.
        '''
        self.config.systems[system_id]['max_bindings'] = 0
        d = self.unbindGateway(system_id)
        d.addCallback(self.removeGatewayFromConfig, system_id)
        return d

    def removeGatewayFromConfig(self, deferred_res, system_id):
        self.config.systems.pop(system_id)
        return deferred_res

class SMPPBindManager:

    def __init__(self, system_id):
        self.system_id = system_id
        self._binds = {pdu_types.CommandId.bind_transceiver: [],
                       pdu_types.CommandId.bind_transmitter: [],
                       pdu_types.CommandId.bind_receiver: []}
        # A queue of the most recent bindings used for delivering messages
        self._delivery_binding_history = collections.deque()

    def addBinding(self, connection):
        """ @param connection: An instance of SMPPServerProtocol """

        bind_type = connection.bind_type
        self._binds[bind_type].append(connection)

    def removeBinding(self, connection):
        """ @param connection: An instance of SMPPServerProtocol """
        bind_type = connection.bind_type
        self._binds[bind_type].remove(connection)

    def getMaxTransmitReceiveBindCount(self):
        return len(self._binds[pdu_types.CommandId.bind_transceiver]) + \
               max(len(self._binds[pdu_types.CommandId.bind_transmitter]),
                   len(self._binds[pdu_types.CommandId.bind_receiver]))

    def getBindingCount(self):
        return sum(len(v) for v in self._binds.values())

    def getBindingCountByType(self):
        ret = {}
        for key, value in self._binds.items():
            ret[key] = len(value)
        return ret

    def __len__(self):
        return self.getBindingCount()

    def __iter__(self):
        vals = []
        [vals.extend(type) for type in self._binds.values()]
        return vals.__iter__()

    def getBindingCountForType(self, bind_type):
        """
        Sum transceiver binds plus receiver or transmitter depending on this type
        @param bind_type: One of smpp.pdu.pdu_types.CommandId
        """
        if bind_type == pdu_types.CommandId.bind_transceiver:
            # Sum of current transceiver binds plus greater of current transmitter or receiver binds
            connections_count = self.getMaxTransmitReceiveBindCount()
        else:
            # Sum of transceiver binds plus existing binds of this type
            connections_count = sum([len(self._binds[bt]) for bt in (pdu_types.CommandId.bind_transceiver, bind_type)])
        return connections_count

    def getNextBindingForDelivery(self):
        """
        Messages inbound (MO) that are to be forwarded to
        the client systems can be sent via transceiver and
        receiver bindings. Call this method to determine which
        binding to send down next so that traffic travels equally
        down the different binds.
        @return smpp protocol or None
        """
        binding = None
        # If we now have more trx/rx bindings than have been used
        # then iterate through our trx/rx binds until we find one
        # that hasn't yet been used
        if len(self._delivery_binding_history) < self.getBindingCountForType(pdu_types.CommandId.bind_receiver):
            for binding in self._binds[pdu_types.CommandId.bind_receiver] + self._binds[pdu_types.CommandId.bind_transceiver]:
                if not binding in self._delivery_binding_history:
                    break
            else:
                binding = None

        # Otherwise send on the last trx/rx binding delivered on, as
        # long as it is still bound
        while binding is None and self._delivery_binding_history:
            # get last binding used
            _binding = self._delivery_binding_history.popleft()
            # check it is still bound
            if _binding in self._binds[_binding.bind_type]:
                # If so then use it
                binding = _binding

        if binding is not None:
            self._delivery_binding_history.append(binding)
        return binding
