import urllib.parse
import ssl
from _socket import gaierror
from datetime import datetime
from operator import itemgetter
from socket import AF_INET, AF_INET6, getaddrinfo, SOCK_STREAM, inet_pton

from funcy import pluck
from gevent.pywsgi import WSGIHandler, WSGIServer
from werkzeug.wrappers import (BaseRequest, CommonRequestDescriptorsMixin,
                               UserAgentMixin, AcceptMixin, Response)

from futile.collections import get_iterable
from futile.logging import LoggerMixin
from openmtc.model import ModelTypeError
from openmtc_cse.methoddomain.filtercriteria import parse_filter_criteria
from openmtc_onem2m.exc import (CSEError, CSEContentsUnacceptable,
                                STATUS_INTERNAL_SERVER_ERROR, CSEBadRequest,
                                STATUS_IMPERSONATION_ERROR)
from openmtc_onem2m.model import (Notification, AttributeList,
                                  get_long_attribute_name)
from openmtc_onem2m.model import get_long_member_name
from openmtc_onem2m.serializer import get_onem2m_supported_content_types
from openmtc_onem2m.serializer.util import (decode_onem2m_content,
                                            encode_onem2m_content)
from openmtc_onem2m.transport import (OneM2MErrorResponse, OneM2MOperation,
                                      OneM2MRequest, OneM2MResponse)

_method_map_from_http = {
    'POST': OneM2MOperation.create,
    'GET': OneM2MOperation.retrieve,
    'PUT': OneM2MOperation.update,
    'DELETE': OneM2MOperation.delete
}


def is_ipv4(address):
    try:
        inet_pton(AF_INET, address)
    except:
        return False
    return True


def is_ipv6(address):
    try:
        inet_pton(AF_INET6, address)
    except:
        return False
    return True


class Request(BaseRequest, CommonRequestDescriptorsMixin, UserAgentMixin,
              AcceptMixin):
    pass


class OpenMTCWSGIServer(WSGIServer, LoggerMixin):
    def do_read(self):
        return super(OpenMTCWSGIServer, self).do_read()

    def wrap_socket_and_handle(self, client_socket, address):
        try:
            return super(OpenMTCWSGIServer, self).wrap_socket_and_handle(client_socket, address)
        except ssl.SSLError as ssl_error:
            # TODO(rst): add better handling of SSL errors
            self.logger.debug(ssl_error)
            return None


class OpenMTCWSGIApplication(LoggerMixin):
    __cached_addresses = {}

    def __init__(self, request_handler, server_address, default_content_type,
                 pretty=False, require_cert=True):
        super(OpenMTCWSGIApplication, self).__init__()

        self.request_handler = request_handler
        self.__cache = set()

        if server_address == "::":
            self.__addresses = self._get_addresses(AF_INET6) | \
                               self._get_addresses(AF_INET)
            self._resolve_host = self._resolve_host_ipv6
        elif server_address in ("", "0.0.0.0"):
            self.__addresses = self._get_addresses(AF_INET)
        else:
            self.__addresses = get_iterable(server_address)

        self.logger.debug("All listening addresses: %s", self.__addresses)

        self.default_content_type = default_content_type
        self.pretty = pretty
        self.require_cert = require_cert

    def _get_addresses(self, family):
        try:
            return self.__cached_addresses[family]
        except KeyError:
            from netifaces import interfaces, ifaddresses

            addresses = self.__cached_addresses[family] = set()

            for interface in interfaces():
                try:
                    ifdata = ifaddresses(interface)[family]
                    ifaddrs = [x.split("%")[0] for x in pluck("addr", ifdata)]
                    addresses.update(ifaddrs)
                except KeyError:
                    pass

            return addresses

    def _get_addr_info(self, host, family):
        self.logger.debug("Resolving %s", host)
        try:
            info = getaddrinfo(host, 0, family, SOCK_STREAM)
            return set(map(itemgetter(0), map(itemgetter(-1), info)))
        except gaierror as e:
            self.logger.error("Failed to resolve %s: %s", host, e)
            return set()

    def _resolve_host(self, host):
        if is_ipv4(host):
            return {host}
        return self._get_addr_info(host, AF_INET)

    def _resolve_host_ipv6(self, host):
        self.logger.debug("Resolving: %s", host)
        if is_ipv6(host):
            return {host}
        # TODO: kca: optimize
        return (self._get_addr_info(host, AF_INET) |
                self._get_addr_info(host, AF_INET6))

    # environ is a dict that holds information about the request and the server
    # start_response is the function that sets status and headers on the http
    # response
    def __call__(self, environ, start_response, subject_alt_name=None):
        with Request(environ) as request:
            # item assignment not supported for Request
            # request['SubjectAltName'] = "test-cse_ae_id"
            # just for testing
            # subject_alt_name = "test_cse_ae_id"

            # handle no client certificate provided
            if subject_alt_name is False:
                if self.require_cert:
                    return Response(
                        status=403
                    )(environ, start_response)
                else:
                    subject_alt_name = None

            # our handle function for incoming requests
            response = (self._handle_options(request)
                        if request.method == "OPTIONS"
                        else self._handle_http_request(request,
                                                       subject_alt_name))
            return response(environ, start_response)

    @staticmethod
    def _handle_options(request):
        # TODO: use full list of supported encodings
        accept = request.accept_mimetypes
        if not ('*/*' in accept or not accept or
                'application/json' in accept or
                'application/xml' in accept):
            return Response('Only application/json and '
                            'application/xml are supported.',
                            status=400)
        if not ('application/json' in accept or
                'application/xml' in accept):
            return Response('Only application/json and '
                            'application/xml are supported.',
                            status=405)
        return Response(
            status=204
        )

    def map_http_request_to_onem2m_request(self, http_request):
        """Maps a HTTP request to a OneM2M request.

        :param http_request: the HTTP Request object to be mapped
        :returns: OneM2MRequest -- the resulting OneM2M request object
        :raises: OpenMTCError
        """
        self.logger.debug("Mapping HTTP request '%s' to OneM2M request",
                          http_request)

        op = _method_map_from_http[http_request.method]
        to = http_request.path[1:].lstrip('/')
        if to.startswith('~/'):
            to = to[1:]
        elif to.startswith('_/'):
            to = '/' + to[1:]

        get_header = http_request.headers.get

        # The X-M2M-Origin header shall be mapped to the From parameter of
        # request and response primitives and vice versa, if applicable.
        fr = get_header("x-m2m-origin")

        # The X-M2M-RI header shall be mapped to the Request Identifier
        # parameter of request and response primitives and vice versa.
        rqi = get_header("x-m2m-ri")

        # primitive content
        pc = decode_onem2m_content(http_request.input_stream.read(),
                                   http_request.content_type)

        # resource type
        # get out of content-type or from resource
        ty = type(pc) if pc else None
        if ty is Notification:
            op = OneM2MOperation.notify

        # The X-M2M-GID header shall be mapped to the Group Request Identifier
        # parameter of request primitives and vice versa, if applicable.
        gid = get_header("x-m2m-gid")

        # The X-M2M-RTU header shall be mapped to the notificationURI element of
        # the Response Type parameter of request primitives and vice versa, if
        # applicable. If there are more than one value in the element, then the
        # values shall be combined with "&" character.
        rt = get_header("x-m2m-rtu")

        # The X-M2M-OT header shall be mapped to the Originating Timestamp
        # parameter of request and response primitives, and vice versa, if
        # applicable.
        ot = get_header("x-m2m-ot")

        # The X-M2M-RST header shall be mapped to the Result Expiration
        # Timestamp parameter of request and response primitives, and vice
        # versa, if applicable.
        rset = get_header("x-m2m-rst")

        # The X-M2M-RET header shall be mapped to the Request Expiration
        # Timestamp parameter of request primitives and vice versa, if
        # applicable.
        rqet = get_header("x-m2m-ret")

        # The X-M2M-OET header shall be mapped to the Operation Execution Time
        # parameter of request primitives and vice versa, if applicable
        oet = get_header("x-m2m-oet")

        # The X-M2M-EC header shall be mapped to the Event Category parameter of
        #  request and response primitives, and vice versa, if applicable.
        ec = get_header("x-m2m-ec")

        # The X-M2M-CTS header shall be mapped to the Content Status parameter
        # of response primitives and vice versa, if applicable.
        rvi = get_header("x-m2m-rvi")

        # The X-M2M-CTS header shall be mapped to the Content Status parameter
        # of response primitives and vice versa, if applicable.
        vsi = get_header("x-m2m-vsi")

        onem2m_request = OneM2MRequest(op=op, to=to, fr=fr, rqi=rqi, ty=ty,
                                       pc=pc, ot=ot, rqet=rqet, rset=rset,
                                       oet=oet, rt=rt, ec=ec, gid=gid, rvi=rvi,
                                       vsi=vsi)

        not_filter_params = ('rt', 'rp', 'rcn', 'da', 'drt', 'rids', 'tids',
                             'ltids', 'tqi')
        multiple_params = ('lbl', 'ty', 'cty', 'atr')

        if http_request.query_string:
            from openmtc_cse.methoddomain.filtercriteria import filters
            params = urllib.parse.parse_qs(http_request.query_string.decode("utf-8"))
            get_param = params.get
            f_c = {}

            for param in params:
                self.logger.debug("checking '%s'", param)

                values = get_param(param)

                if param not in multiple_params and len(values) > 1:
                    raise CSEBadRequest("Multiple field names not permitted "
                                        "for parameter %s" % param)

                param_long_name = get_long_member_name(param)

                # TODO(rst): handle attributes with get_long_attribute_name
                if param in not_filter_params:
                    setattr(onem2m_request, param, values[0])
                elif param_long_name == 'attributeList':
                    onem2m_request.pc = AttributeList(
                        list(map(get_long_attribute_name, values[0].split(' '))))
                elif param_long_name and hasattr(filters, param_long_name):
                    self.logger.debug("got values for '%s' ('%s'): %s",
                                      param_long_name, param, values)

                    if param in multiple_params:
                        f_c[param_long_name] = values
                    else:
                        f_c[param_long_name] = values[0]
                else:
                    raise CSEBadRequest("Unknown parameter: %s" % param)
            onem2m_request.filter_criteria = parse_filter_criteria(f_c)

        return onem2m_request

    def map_onem2m_response_to_http_response(self, request, response):
        """Maps a OneM2M response to a HTTP response.

        :param request: the HTTP request
        :param response: the OneM2MResponse object to be mapped
        :returns: Response -- the resulting HTTP Response
        :raises: OpenMTCError
        """
        self.logger.debug("Mapping OneM2M response: %s", response)

        status_code = response.status_code

        # resourceID prefix
        if response.to.startswith('//'):
            resource_id_pre = '/'.join(response.to.split('/')[:4]) + '/'
        elif response.to.startswith('/'):
            resource_id_pre = '/'.join(response.to.split('/')[:2]) + '/'
        else:
            resource_id_pre = ''

        headers = {
            "x-m2m-ri": str(response.rqi),
            "x-m2m-rsc": str(response.rsc),
            'x-m2m-rvi': str(response.rvi)
        }

        if response.fr:
            headers['x-m2m-origin'] = str(response.fr)

        response_fields = ['ot', 'rset', 'ec', 'cts', 'cto', 'vsi']
        for f in response_fields:
            if getattr(response, f):
                headers['x-m2m-%s' % f] = str(getattr(response, f))

        try:
            headers['Content-Location'] = (resource_id_pre + response.content.resourceID)
        except (AttributeError, TypeError):
            pass

        pretty = self.pretty
        if pretty is None:
            user_agent = request.user_agent
            pretty = user_agent and ("opera" in user_agent or
                                     "mozilla" in user_agent)
        supported = get_onem2m_supported_content_types()
        if request.accept_mimetypes:
            accept = request.accept_mimetypes.best_match(supported)
            if accept is None:
                # TODO(rst): raise 406 or similar
                accept = self.default_content_type
        else:
            accept = self.default_content_type
        try:
            content_type, payload = encode_onem2m_content(response.content,
                                                          accept, pretty,
                                                          path=resource_id_pre,
                                                          fields=response.fields)
        except CSEContentsUnacceptable as e:
            status_code = e.status_code
            content_type = "text/plain"
            payload = str(e)

        return Response(
            payload,
            status=status_code,
            headers=headers,
            content_type=content_type
        )

    def map_onem2m_error_to_http_error(self, response):
        """Maps a OneM2M error response to a HTTP response.

        :param response: the OneM2MResponse object to be mapped
        :returns: Response -- the resulting Response object
        :raises: OpenMTCError
        """
        self.logger.debug("Mapping OneM2M error: %s", response)
        # This is strange, rs is defined as a string (Okay, Not Okay), and
        # status codes are optional in onem2m req

        # TODO(rst): handle this better
        try:
            status_code = response.status_code

            headers = {
                "x-m2m-ri": str(response.rqi),
                "x-m2m-rsc": str(response.rsc),
                'x-m2m-rvi': str(response.rvi)
            }
        except AttributeError:
            status_code = STATUS_INTERNAL_SERVER_ERROR.http_status_code
            headers = None

        return Response(status=status_code, headers=headers)

    def _handle_http_request(self, request, subject_alt_name):
        onem2m_request = OneM2MRequest(None, None)

        remote_ip_addr = request.environ.get("REMOTE_ADDR", None)

        # check for impersonation error
        # the from parameter of the onem2m request is verified against the
        # association info of the cert
        # subjectAltName of certificate vs. from parameter of onem2m request
        impersonation_error = False
        try:
            onem2m_request = self.map_http_request_to_onem2m_request(request)
            setattr(onem2m_request, "_remote_ip_addr", remote_ip_addr)
            setattr(onem2m_request, "_authenticated", False)
            if subject_alt_name is not None:
                setattr(onem2m_request, "_authenticated", True)
                impersonation_error = not bool(len([x for x in subject_alt_name if x[0] == 'URI' and x[1] == onem2m_request.fr]))
            if impersonation_error:
                onem2m_response = OneM2MResponse(STATUS_IMPERSONATION_ERROR,
                                                 request=onem2m_request)
                response = self.map_onem2m_response_to_http_response(
                    request, onem2m_response)

                return response

            onem2m_response = self.request_handler(onem2m_request).get()
        except OneM2MErrorResponse as error_response:
            response = self.map_onem2m_error_to_http_error(error_response)
        except CSEError as error:
            error_response = OneM2MErrorResponse(error.response_status_code,
                                                 onem2m_request)
            response = self.map_onem2m_error_to_http_error(error_response)
        except (ModelTypeError, ValueError):
            error_response = OneM2MErrorResponse(CSEBadRequest.response_status_code,
                                                 onem2m_request)
            response = self.map_onem2m_error_to_http_error(error_response)
        else:
            response = self.map_onem2m_response_to_http_response(
                request, onem2m_response)

        return response


class OpenMTCWSGIHandler(WSGIHandler, LoggerMixin):

    @staticmethod
    def _is_cert_expired(not_after):
        # TODO(rkr): timestamp issue when server checks the notAfter timestamp
        # TODO       of the certificate?
        """
        Checks, if the notAfter field of the certificate is expired.
        Note: The gevent ssl2 module is performing this check while method
                do_handshake(), but looks like without regard
                to the timestamps, therefore cert is valid for another hour.

        :param not_after:
        :return: True | False
        """
        ssl_date_fmt = r'%b %d %H:%M:%S %Y %Z'
        valid_until = datetime.strptime(not_after, ssl_date_fmt)
        now = datetime.now()

        if valid_until < now:
            return True
        else:
            return False

    def verify_path(self):
        pass

    def run_application(self):
        from ssl import SSLSocket

        if type(self.socket) == SSLSocket:
            self.logger.debug("ssl socket connection established")
            cert = self.socket.getpeercert()
            if cert:
                not_after = cert.get('notAfter', None)
                subject = cert.get('subject', None)
                subject_alt_name = cert.get('subjectAltName', None)
                if subject_alt_name is not None:
                    self.logger.debug("san: %s", subject_alt_name)
                if subject is not None:
                    self.logger.debug("subject: %s", subject)
                if not_after is not None:
                    if self._is_cert_expired(not_after):
                        # raise
                        pass
            else:
                # no client certificate provided
                subject_alt_name = False
            self.result = self.application(self.environ, self.start_response,
                                           subject_alt_name)
        else:
            self.result = self.application(self.environ, self.start_response)

        self.process_result()
