import copy
import http.client
import queue
import socket
import threading
import time
import traceback
import uuid
from http.server import HTTPServer
from urllib.parse import urlparse, urlunparse

from lxml import etree as etree_

from sdc11073.pysoap.soapclient import HTTPReturnCodeError
from sdc11073.pysoap.soapenvelope import Soap12Envelope, ReceivedSoap12Envelope, WsAddress, WsSubscribe, \
    SoapResponseException
from sdc11073.pysoap.soapenvelope import WsaEndpointReferenceType
from .. import commlog
from .. import loghelper
from .. import observableproperties as properties
from .. import xmlparsing, isoduration
from ..httprequesthandler import HTTPRequestHandler
from ..namespaces import Prefix_Namespace as Prefix
from ..namespaces import nsmap as _global_nsmap
from ..namespaces import wseTag, wsaTag, WSA_IS_REFERENCE_PARAMETER

SUBSCRIPTION_CHECK_INTERVAL = 5  # seconds


class MyThreadingMixIn(object):

    def process_request_thread(self, request, client_address):
        """Same as in BaseServer but as a thread.

        In addition, exception handling is done here.

        """
        try:
            self.finish_request(request, client_address)
            self.shutdown_request(request)
        except Exception as ex:
            if self.dispatcher is not None:
                # only
                self.handle_error(request, client_address)
            self.shutdown_request(request)

    def process_request(self, request, client_address):
        """Start a new thread to process the request."""
        t = threading.Thread(target=self.process_request_thread,
                             args=(request, client_address),
                             name='SubscrRecv{}'.format(client_address))
        t.daemon = True
        t.start()
        self.threads.append((t, request, client_address))


class ThreadingHTTPServer(MyThreadingMixIn, HTTPServer):
    """ Each request is handled in a thread.
    Following receipe from https://pymotw.com/2/BaseHTTPServer/index.html#module-BaseHTTPServer
    """

    def __init__(self, *args, **kwargs):
        HTTPServer.__init__(self, *args, **kwargs)
        self.daemon_threads = True
        self.threads = []
        self.dispatcher = None


class ClSubscription(object):
    """ This class handles a subscription to an event source.
    It stores all key data of the subscription and can renew and unsubscribe this subscription."""
    notification = properties.ObservableProperty()
    IDENT_TAG = etree_.QName('http.local.com', 'MyClIdentifier')

    def __init__(self, dpwsHosted, actions, notification_url, endTo_url, ident, xml_validator):
        """
        :param dpwsHosted: the DPWSHosted instance this port type belongs to
        :param actions: list of subscribed actions
        :param notification_url: e.g. http://1.2.3.4:9999, or https://1.2.3.4:9999
        :param endTo_url: url for subscription end message
        :param ident: prefix for logging
        :param xml_validator: allows schema validation
        """
        self.dpwsHosted = dpwsHosted
        self._actions = actions
        self._filter = ' '.join(actions)
        self._notification_url = notification_url
        self.isSubscribed = False
        self._isSubscribedLock = threading.Lock()
        self.end_status = None  # if device sent a SubscriptionEnd message, this contains the status from the message
        self.end_reason = None  # if device sent a SubscriptionEnd message, this contains the reason from the message
        self.expireAt = None
        self.expire_minutes = None
        self.dev_reference_param = None
        self.notifyTo_identifier = etree_.Element(self.IDENT_TAG)
        self.notifyTo_identifier.text = uuid.uuid4().urn

        self._endTo_url = endTo_url
        self._endTo_identifier = etree_.Element(self.IDENT_TAG)
        self._endTo_identifier.text = uuid.uuid4().urn

        self._subscriptionManagerAddress = None
        self._logger = loghelper.getLoggerAdapter('sdc.client.subscr', ident)
        self.eventCounter = 0  # for display purpose, we count notifications
        self.cl_ident = ident
        self._device_epr = urlparse(self.dpwsHosted.endpointReferences[0].address).path
        self._xml_validator = xml_validator

    @property
    def end_to_identifier(self):
        return self._endTo_identifier

    def _mkSubscribeEnvelope(self, subscribe_epr, expire_minutes):
        soapEnvelope = Soap12Envelope(Prefix.partialMap(Prefix.S12, Prefix.WSA, Prefix.WSE))
        soapEnvelope.setAddress(WsAddress(action='http://schemas.xmlsoap.org/ws/2004/08/eventing/Subscribe',
                                          to=subscribe_epr))

        body = WsSubscribe(notifyTo=WsaEndpointReferenceType(self._notification_url,
                                                             referenceParametersNode=[self.notifyTo_identifier]),
                           endTo=WsaEndpointReferenceType(self._endTo_url,
                                                          referenceParametersNode=[self._endTo_identifier]),
                           expires=expire_minutes * 60,
                           filter_=self._filter)
        soapEnvelope.addBodyObject(body)
        return soapEnvelope

    def _handleSubscribeResponse(self, soapEnvelope):
        # Check content of response; raise Error if subscription was not successful
        try:
            msgNode = soapEnvelope.msgNode
            if msgNode.tag == wseTag('SubscribeResponse'):
                address = msgNode.xpath('wse:SubscriptionManager/wsa:Address/text()', namespaces=_global_nsmap)
                self.dev_reference_param = None

                reference_params = msgNode.xpath('wse:SubscriptionManager/wsa:ReferenceParameters',
                                                 namespaces=_global_nsmap)
                if reference_params:
                    self.dev_reference_param = reference_params[0]
                expires = msgNode.xpath('wse:Expires/text()', namespaces=_global_nsmap)

                self._subscriptionManagerAddress = urlparse(address[0])
                expireseconds = isoduration.parse_duration(expires[0])
                self.expireAt = time.time() + expireseconds
                self.isSubscribed = True
                self._logger.info('Subscribe was successful: expires at {}, address="{}"',
                                  self.expireAt, self._subscriptionManagerAddress)
            else:
                # This is a failure response or even rubbish. log it and raise error
                self._logger.error('Subscribe response has unexpected content: {}', soapEnvelope.as_xml(pretty=True))
                self.isSubscribed = False
                raise SoapResponseException(soapEnvelope)
        except AttributeError:
            self._logger.error('Subscribe response has unexpected content: {}', soapEnvelope.as_xml(pretty=True))
            self.isSubscribed = False
            raise SoapResponseException(soapEnvelope)

    def subscribe(self, expire_minutes=60):
        self._logger.info('### startSubscription "{}" ###', self._filter)
        self.eventCounter = 0
        self.expire_minutes = expire_minutes  # saved for later renewal, we will use the same interval
        # ToDo: check if there is more than one address. In that case a clever selection is needed
        address = self.dpwsHosted.endpointReferences[0].address
        soapEnvelope = self._mkSubscribeEnvelope(address, expire_minutes)
        msg = 'subscribe {}'.format(self._filter)
        try:
            resultSoapEnvelope = self.dpwsHosted.soapClient.postSoapEnvelopeTo(self._device_epr, soapEnvelope, msg=msg)
            self._handleSubscribeResponse(resultSoapEnvelope)
        except HTTPReturnCodeError as ex:
            self._logger.error('could not subscribe: {}'.format(HTTPReturnCodeError))

    def _add_device_references(self, soapEnvelope):
        """ add references for requests to device (renew, getstatus, unsubscribe)"""
        if self.dev_reference_param is not None:
            for e in self.dev_reference_param:
                e_ = copy.copy(e)
                # mandatory attribute acc. to ws_addressing SOAP Binding (https://www.w3.org/TR/2006/REC-ws-addr-soap-20060509/)
                e_.set(wsaTag('IsReferenceParameter'), 'true')
                soapEnvelope.addHeaderElement(e_)

    def _mkRenewEnvelope(self, expire_minutes):
        soapEnvelope = Soap12Envelope(Prefix.partialMap(Prefix.S12, Prefix.WSA, Prefix.WSE))
        soapEnvelope.setAddress(WsAddress(action='http://schemas.xmlsoap.org/ws/2004/08/eventing/Renew',
                                          to=urlunparse(self._subscriptionManagerAddress)))
        self._add_device_references(soapEnvelope)
        renewNode = etree_.Element(wseTag('Renew'), nsmap=Prefix.partialMap(Prefix.WSE))
        expiresNode = etree_.SubElement(renewNode, wseTag('Expires'), nsmap=Prefix.partialMap(Prefix.WSE))
        expiresNode.text = isoduration.durationString(expire_minutes * 60)
        soapEnvelope.addBodyElement(renewNode)
        return soapEnvelope

    def _handleRenewResponse(self, soapEnvelope):
        # Check content of response; raise Error if subscription was not successful
        bodyNode = soapEnvelope.bodyNode
        renewResponse = bodyNode.xpath('wse:RenewResponse', namespaces=_global_nsmap)
        if len(renewResponse) == 1:
            # this means renew was accepted
            expires = bodyNode.xpath('wse:RenewResponse/wse:Expires/text()', namespaces=_global_nsmap)
            expireseconds = isoduration.parse_duration(expires[0])
            self.expireAt = time.time() + expireseconds
        else:
            raise SoapResponseException(soapEnvelope)

    def renew(self, expire_minutes=60):
        with self._isSubscribedLock:
            if not self.isSubscribed:
                return None

            soapEnvelope = self._mkRenewEnvelope(expire_minutes)
            try:
                resultSoapEnvelope = self.dpwsHosted.soapClient.postSoapEnvelopeTo(self._subscriptionManagerAddress.path,
                                                                                   soapEnvelope, msg='renew')
                self._logger.debug('{}', resultSoapEnvelope.as_xml(pretty=True))
            except HTTPReturnCodeError as ex:
                self.isSubscribed = False
                self._logger.error('could not renew: {}'.format(HTTPReturnCodeError))
            except (http.client.HTTPException, ConnectionError) as ex:
                self._logger.warn('renew failed: {}', ex)
                self.isSubscribed = False
            except Exception as ex:
                self._logger.error('Exception in renew: {}', ex)
                self.isSubscribed = False
            else:
                try:
                    self._handleRenewResponse(resultSoapEnvelope)
                    return self.remainingSubscriptionSeconds
                except SoapResponseException as ex:
                    self.isSubscribed = False
                    self._logger.warn('renew failed: {}',
                                      etree_.tostring(ex.soapResponseEnvelope.bodyNode, pretty_print=True))

    def unsubscribe(self):
        with self._isSubscribedLock:
            if not self.isSubscribed:
                return
            self.isSubscribed = False  # handle it as unsubscribed from now on
            soapEnvelope = Soap12Envelope(Prefix.partialMap(Prefix.S12, Prefix.WSA, Prefix.WSE))
            soapEnvelope.setAddress(WsAddress(action='http://schemas.xmlsoap.org/ws/2004/08/eventing/Unsubscribe',
                                              to=urlunparse(self._subscriptionManagerAddress)))
            self._add_device_references(soapEnvelope)
            soapEnvelope.addBodyElement(etree_.Element(wseTag('Unsubscribe')))
            resultSoapEnvelope = self.dpwsHosted.soapClient.postSoapEnvelopeTo(self._subscriptionManagerAddress.path,
                                                                               soapEnvelope, msg='unsubscribe')
            responseAction = resultSoapEnvelope.address.action
            # check response: response does not contain explicit status. If action== UnsubscribeResponse all is fine.
            if responseAction == 'http://schemas.xmlsoap.org/ws/2004/08/eventing/UnsubscribeResponse':
                self._logger.info('unsubscribe: end of subscription {} was confirmed.', self._filter)
            else:
                self._logger.error('unsubscribe: unexpected response action: {}', resultSoapEnvelope.as_xml(pretty=True))
                raise RuntimeError(
                    'unsubscribe: unexpected response action: {}'.format(resultSoapEnvelope.as_xml(pretty=True)))

    def _mkGetStatusEnvelope(self):
        soapEnvelope = Soap12Envelope(Prefix.partialMap(Prefix.S12, Prefix.WSA, Prefix.WSE))
        soapEnvelope.setAddress(WsAddress(action='http://schemas.xmlsoap.org/ws/2004/08/eventing/GetStatus',
                                          to=urlunparse(self._subscriptionManagerAddress)))
        self._add_device_references(soapEnvelope)
        bodyNode = etree_.Element(wseTag('GetStatus'))
        soapEnvelope.addBodyElement(bodyNode)
        return soapEnvelope

    def getStatus(self):
        """ Sends a GetStatus Request to the device.
        :return: the remaining time of the subscription or None, if the request was not successful
        """
        with self._isSubscribedLock:
            if not self.isSubscribed:
                return None
            soapEnvelope = self._mkGetStatusEnvelope()
            try:
                resultSoapEnvelope = self.dpwsHosted.soapClient.postSoapEnvelopeTo(self._subscriptionManagerAddress.path,
                                                                                   soapEnvelope, msg='getStatus')
            except HTTPReturnCodeError as ex:
                self.isSubscribed = False
                self._logger.error('could not get status: {}'.format(HTTPReturnCodeError))
            except (http.client.HTTPException, ConnectionError) as ex:
                self.isSubscribed = False
                self._logger.warn('getStatus: Connection Error {} for subscription {}', ex, self._filter)
            except Exception as ex:
                self._logger.error('Exception in getStatus: {}', ex)
                self.isSubscribed = False
            else:
                try:
                    expiresNode = resultSoapEnvelope.msgNode.find('wse:Expires', namespaces=_global_nsmap)
                    if expiresNode is None:
                        self._logger.warn('getStatus for {}: Could not find "Expires" node! getStatus={} ', self._filter,
                                          resultSoapEnvelope.rawdata)
                        raise SoapResponseException(resultSoapEnvelope)
                    else:
                        expires = expiresNode.text
                        expiresValue = isoduration.parse_duration(expires)
                        self._logger.debug('getStatus for {}: Expires = {} = {} seconds, counter = {}', self._filter,
                                           expires,
                                           expiresValue,
                                           self.eventCounter)
                        return expiresValue
                except AttributeError:
                    self._logger.warn('No msg in envelope')

    def checkStatus(self, renewLimit):
        """ Calls getStatus and updates internal data.
        :param renewLimit: a value in seconds. If remaining duration of subscription is less than this value, it renews the subscription.
        :return: None
        """
        remainingTime = self.getStatus()
        if not self.isSubscribed:
            return
        elif abs(remainingTime - self.remainingSubscriptionSeconds) > 10:
            self._logger.warn(
                'time delta between expected expire and reported expire  > 10 seconds. Will correct own expectation.')
            self.expireAt = time.time() + remainingTime

        if self.remainingSubscriptionSeconds < renewLimit:
            self._logger.info('renewing subscription')
            self.renew()

    @property
    def remainingSubscriptionSeconds(self):
        return self.expireAt - time.time()

    def onNotification(self, soapEnvelope):
        try:
            soapEnvelope.validate_envelope(self._xml_validator)
        except etree_.DocumentInvalid as ex:
            self._logger.error('received invalid document: {}', ex)
        except Exception as ex:
            self._logger.error('error validation document: {}', ex)
        self.eventCounter += 1
        self.notification = soapEnvelope

    @property
    def shortFilterString(self):
        return xmlparsing.shortFilterString(self._actions)

    def __str__(self):
        return 'Subscription of "{}", isSubscribed={}, remaining time = {} sec., count={}'.format(
            self.shortFilterString,
            self.isSubscribed,
            int(self.remainingSubscriptionSeconds),
            self.eventCounter)


class SubscriptionManager(threading.Thread):
    """ Factory for Subscription objects, thread that automatically renews expiring subscriptions.
     """
    allSubscriptionsOkay = properties.ObservableProperty(True)  # a boolean
    keepAlive_with_renew = True  # enable as workaround if checkstatus is not supported

    def __init__(self, notification_url, endTo_url=None, checkInterval=None, log_prefix='', xml_validator=None):
        """
        :param notification_url: the destination url for notifications.
        :param endTo_url: if given the destination url for end subscription notifications; if not given, the notification_url is used.
        :param checkInterval: the interval (in seconds ) for getStatus requests. Defaults to SUBSCRIPTION_CHECK_INTERVAL
        :param log_prefix: a string that is used in log output; defaults to empty string
        :param xml_validator: helper for schema validation
         """
        super().__init__(name='Cl_SubscriptionManager{}'.format(log_prefix))
        self.daemon = True
        self._checkInterval = checkInterval or SUBSCRIPTION_CHECK_INTERVAL
        self.subscriptions = {}
        self._subscriptionsLock = threading.Lock()

        self._run = False
        self._notification_url = notification_url
        self._endTo_url = endTo_url or notification_url
        self._logger = loghelper.getLoggerAdapter('sdc.client.subscrMgr', log_prefix)
        self.log_prefix = log_prefix
        self._xml_validator = xml_validator

    def stop(self):
        self._run = False
        self.join(timeout=2)
        with self._subscriptionsLock:
            self.subscriptions.clear()

    def run(self):
        self._run = True
        try:
            while self._run:
                try:
                    for i in range(self._checkInterval):
                        time.sleep(1)
                        if not self._run:
                            return
                        # check if all subscriptions are okay
                        with self._subscriptionsLock:
                            not_okay = [s for s in self.subscriptions.values() if not s.isSubscribed]
                            subscribed = [s.isSubscribed for s in self.subscriptions.values()]
                            self.allSubscriptionsOkay = (len(not_okay) == 0)
                    with self._subscriptionsLock:
                        subscriptions = list(self.subscriptions.values())
                    for subscription in subscriptions:
                        if self.keepAlive_with_renew:
                            subscription.renew()
                        else:
                            subscription.checkStatus(renewLimit=self._checkInterval * 5)
                    self._logger.debug('##### SubscriptionManager Interval ######')
                    for subscription in subscriptions:
                        self._logger.debug('{}', subscription)
                except Exception as ex:
                    self._logger.error('##### check loop: {}', traceback.format_exc())
        finally:
            self._logger.info('terminating subscriptions check loop! self._run={}', self._run)

    def mkSubscription(self, dpwsHosted, filters):
        s = ClSubscription(dpwsHosted, filters, self._notification_url, self._endTo_url, self.log_prefix,
                           self._xml_validator)
        filter_ = ' '.join(filters)
        with self._subscriptionsLock:
            self.subscriptions[filter_] = s
        return s

    def onSubScriptionEnd(self, soapenvelope):
        subscr_ident_list = soapenvelope.headerNode.findall(ClSubscription.IDENT_TAG, namespaces=_global_nsmap)
        statuus = soapenvelope.bodyNode.xpath('wse:SubscriptionEnd/wse:Status/text()', namespaces=_global_nsmap)
        reasons = soapenvelope.bodyNode.xpath('wse:SubscriptionEnd/wse:Reason/text()', namespaces=_global_nsmap)
        if statuus:
            info = ' status={} '.format(statuus[0])
        else:
            info = ''
        if reasons:
            if len(reasons) == 1:
                info += ' reason = {}'.format(reasons[0])
            else:
                info += ' reasons = {}'.format(reasons)
        if not subscr_ident_list:
            self._logger.warn('onSubScriptionEnd: did not find any identifier in message')
            return None
        subscr_ident = subscr_ident_list[0]
        for s in self.subscriptions.values():
            if subscr_ident.text == s.end_to_identifier.text:
                self._logger.info('onSubScriptionEnd: received Subscription End for {} {}',
                                  s.shortFilterString,
                                  info)
                s.isSubscribed = False
                if len(statuus) > 0:
                    s.end_status = statuus[0]
                if len(reasons) > 0:
                    s.end_reason = reasons[0]
                return s
        self._logger.warn('onSubScriptionEnd: have no subscription for identifier = {}', subscr_ident.text)
        return None

    def unsubscribeAll(self):
        with self._subscriptionsLock:
            current_subscriptions = list(self.subscriptions.values())  # make a copy
            self.subscriptions.clear()
            for s in current_subscriptions:
                try:
                    s.unsubscribe()
                except:
                    self._logger.warn('unsubscribe error: {}\n call stack:{} ', traceback.format_exc(),
                                      traceback.format_stack())


class _DispatchError(Exception):
    def __init__(self, httpErrorcode, errorText):
        super(_DispatchError, self).__init__()
        self.httpErrorcode = httpErrorcode
        self.errorText = errorText


class SOAPNotificationsDispatcher(object):
    """ receiver of all notifications"""

    def __init__(self, log_prefix, sdc_definitions):
        self._logger = loghelper.getLoggerAdapter('sdc.client.notif_dispatch', log_prefix)
        self.log_prefix = log_prefix
        self._sdc_definitions = sdc_definitions
        self.methods = {}

    def register_function(self, action, ref, fn):
        self.methods[(ref, action)] = fn

    def dispatch(self, path, xml):
        start = time.time()

        fn, action, request = self._dispatch_process_data(xml, path)
        fn(request)

        duration = time.time() - start
        if duration > 0.005:
            self._logger.debug('action {}: duration = {:.4f}sec', action, duration)
        return ''

    def _dispatch_process_data(self, xml, path):
        normalized_xml = self._sdc_definitions.normalizeXMLText(xml)
        request = ReceivedSoap12Envelope.fromXMLString(normalized_xml)
        try:
            action = request.address.action
        except AttributeError:
            raise _DispatchError(404, 'no action in request')
        self._logger.debug('received notification path={}, action = {}', path, action)

        ref_parameters = request.headerNode.findall(ClSubscription.IDENT_TAG, namespaces=_global_nsmap)
        ref_parameters = [i for i in ref_parameters if i.get(WSA_IS_REFERENCE_PARAMETER) in ("1", "true")]

        if len(ref_parameters) != 1:
            raise _DispatchError(400, f'Expected exactly one matching reference parameter missing from request.')

        try:
            fn = self.methods[(ref_parameters[0].text, action)]
        except KeyError:
            self._logger.error('action "{}" not registered. Known:{}'.format(action, self.methods.keys()))
            raise _DispatchError(404, 'action not registered')

        return fn, action, request


class SOAPNotificationsDispatcherThreaded(SOAPNotificationsDispatcher):

    def __init__(self, ident, bicepsSchema):
        super(SOAPNotificationsDispatcherThreaded, self).__init__(ident, bicepsSchema)
        self._queue = queue.Queue(1000)
        self._worker = threading.Thread(target=self._readqueue)
        self._worker.daemon = True
        self._worker.start()

    def dispatch(self, path, xml):
        fn, action, request = self._dispatch_process_data(xml, path)
        self._queue.put((fn, request, action))
        return ''

    def _readqueue(self):
        while True:
            fn, request, action = self._queue.get()
            try:
                fn(request)
            except:
                self._logger.error(
                    'method {} for action "{}" failed:{}'.format(fn.__name__, action, traceback.format_exc()))


class SOAPNotificationsHandler(HTTPRequestHandler):
    disable_nagle_algorithm = True
    wbufsize = 0xffff  # 64k buffer to prevent tiny packages
    RESPONSE_COMPRESS_MINSIZE = 256  # bytes, compress response it it is larger than this value (and other side supports compression)

    def do_POST(self):
        """SOAP POST gateway"""
        self.server.threadObj._logger.debug('notification do_POST incoming')  # pylint: disable=protected-access
        dispatcher = self.server.dispatcher
        response_string = ''
        if dispatcher is None:
            # close this connection
            self.close_connection = 1
            self.server.threadObj._logger.warn(
                'received a POST request, but no dispatcher => returning 404 ')  # pylint:disable=protected-access
            self.send_response(404)  # not found
        else:
            request_bytes = self._read_request()

            self.server.threadObj._logger.debug('notification {} bytes',
                                                request_bytes)  # pylint: disable=protected-access
            # execute the method
            commlog.defaultLogger.logSoapSubscrMsgIn(request_bytes)
            try:
                response_string = self.server.dispatcher.dispatch(self.path, request_bytes)
                if response_string is None:
                    response_string = ''
                self.send_response(202, b'Accepted')
            except _DispatchError as ex:
                self.server.threadObj._logger.error('received a POST request, but got _DispatchError => returning {}',
                                                    ex.httpErrorcode)  # pylint:disable=protected-access
                self.send_response(ex.httpErrorcode, ex.errorText)
            except Exception as ex:
                self.server.threadObj._logger.error(
                    'received a POST request, but got Exception "{}"=> returning {}\n{}', ex, 500,
                    traceback.format_exc())  # pylint:disable=protected-access
                self.send_response(500, b'server error in dispatch')
        response_bytes = response_string.encode('utf-8')
        if len(response_bytes) > self.RESPONSE_COMPRESS_MINSIZE:
            response_bytes = self._compressIfRequired(response_bytes)

        self.send_header("Content-Type", "application/soap+xml; charset=utf-8")
        self.send_header("Content-Length", len(response_bytes))  # this is necessary for correct keep-alive handling!
        self.end_headers()
        self.wfile.write(response_bytes)


class NotificationsReceiverDispatcherThread(threading.Thread):

    def __init__(self, my_ipaddress, sslContext, log_prefix, sdc_definitions, supportedEncodings,
                 soap_notifications_handler_class=None, async_dispatch=True):
        """

        :param my_ipaddress: http server will listen on this address
        :param sslContext: http server uses this ssl context
        :param log_prefix: used for logging
        :param sdc_definitions: namespaces etc.
        :param supportedEncodings: a list of strings
        :param soap_notifications_handler_class: if None, SOAPNotificationsHandler is used,
                otherwise the provided class ( a HTTPRequestHandler).
        :param async_dispatch: if True, incoming requests are queued and response is sent (processing is done later).
                                if False, response is sent after the complete processing is done.
        """
        super(NotificationsReceiverDispatcherThread, self).__init__(
            name='Cl_NotificationsReceiver_{}'.format(log_prefix))
        self._sslContext = sslContext
        self._soap_notifications_handler_class = soap_notifications_handler_class
        self.daemon = True
        self._logger = loghelper.getLoggerAdapter('sdc.client.notif_dispatch', log_prefix)

        self._my_ipaddress = my_ipaddress
        self.my_port = None
        self.base_url = None
        self.httpd = None
        self.supportedEncodings = supportedEncodings
        # create and set up the dispatcher for notifications
        if async_dispatch:
            self.dispatcher = SOAPNotificationsDispatcherThreaded(log_prefix, sdc_definitions)
        else:
            self.dispatcher = SOAPNotificationsDispatcher(log_prefix, sdc_definitions)
        self.started_evt = threading.Event()  # helps to wait until thread has initialised is variables

    def run(self):
        try:
            myport = 0  # zero means that OS selects a free port
            self.httpd = ThreadingHTTPServer((self._my_ipaddress, myport),
                                             self._soap_notifications_handler_class or SOAPNotificationsHandler)
            # add use compression flag to the server
            setattr(self.httpd, 'supportedEncodings', self.supportedEncodings)
            self.my_port = self.httpd.server_port
            self._logger.info('starting Notification receiver on {}:{}', self._my_ipaddress, self.my_port)
            if self._sslContext:
                self.httpd.socket = self._sslContext.wrap_socket(self.httpd.socket,
                                                                 server_side=True,
                                                                 do_handshake_on_connect=False)
                self.base_url = 'https://{}:{}/'.format(self._my_ipaddress, self.my_port)
            else:
                self.base_url = 'http://{}:{}/'.format(self._my_ipaddress, self.my_port)
            self.httpd.dispatcher = self.dispatcher
            self.httpd.threadObj = self  # make logger available for SOAPNotificationsHandler
            self.started_evt.set()
            self.httpd.serve_forever()
        except Exception:
            self._logger.error(
                'Unhandled Exception at thread runtime. Thread will abort! {}'.format(traceback.format_exc()))
            raise

    def stop(self, closeAllConnections=True):
        """
        :param closeAllConnections: for testing purpose one might want to keep the connection handler threads alive.
                If param is False then they are kept alive.
        """
        self.httpd.shutdown()
        self.httpd.socket.close()
        if closeAllConnections:
            if self.httpd.dispatcher is not None:
                self.httpd.dispatcher.methods = {}
                self.httpd.dispatcher = None  # this leads to a '503' reaction in SOAPNotificationsHandler
            for thr in self.httpd.threads:
                thread, request, client_addr = thr
                if thread.is_alive():
                    try:
                        request.shutdown(socket.SHUT_RDWR)
                        request.close()
                        self._logger.info('closed socket for notifications from {}', client_addr)
                    except OSError as ex:
                        # the connection is already closed
                        continue
                    except Exception as ex:
                        self._logger.warn('error closing socket for notifications from {}: {}', client_addr, ex)
            time.sleep(0.1)
            for thr in self.httpd.threads:
                thread, request, client_addr = thr
                if thread.is_alive():
                    thread.join(2)
            del self.httpd.threads[:]
