import logging
import re
import uuid

from pyquery import PyQuery
from multidict import CIMultiDict

from . import utils
from .contact import Contact
from .auth import Auth

FIRST_LINE_PATTERN = {
    'request': {
        'regex': re.compile(r'(?P<method>[A-Za-z]+) (?P<to_uri>.+) SIP/2.0'),
        'str': '{method} {to_uri} SIP/2.0'},
    'response': {
        'regex': re.compile(r'SIP/2.0 (?P<status_code>[0-9]{3}) (?P<status_message>.+)'),
        'str': 'SIP/2.0 {status_code} {status_message}'},
}


LOG = logging.getLogger(__name__)


class Message:
    def __init__(self,
                 headers=None,
                 payload=None,
                 from_details=None,
                 to_details=None,
                 contact_details=None,
                 ):

        if headers:
            self.headers = headers
        else:
            self.headers = CIMultiDict()

        if from_details:
            self._from_details = from_details
        elif 'From' not in self.headers:
            raise ValueError('From header or from_details is required')

        if to_details:
            self._to_details = to_details
        elif 'To' not in self.headers:
            raise ValueError('To header or to_details is required')

        if contact_details:
            self._contact_details = contact_details

        self._payload = payload
        self._raw_payload = None

        if 'Via' not in self.headers:
            self.headers['Via'] = 'SIP/2.0/%(protocol)s ' + \
                utils.format_host_and_port(self.contact_details['uri']['host'],
                                           self.contact_details['uri']['port']) + \
                ';branch=%s' % utils.gen_branch(10)

    @property
    def auth(self):
        if not hasattr(self, '_auth'):
            self._auth = Auth.from_message(self)
        return self._auth

    @property
    def payload(self):
        if self._payload:
            return self._payload
        elif self._raw_payload:
            self._payload = self._raw_payload.decode()
            return self._payload
        else:
            return ''

    @payload.setter
    def payload(self, payload):
        self._payload = payload

    @property
    def from_details(self):
        if not hasattr(self, '_from_details'):
            self._from_details = Contact.from_header(self.headers['From'])
        return self._from_details

    @from_details.setter
    def from_details(self, from_details):
        self._from_details = from_details

    @property
    def to_details(self):
        if not hasattr(self, '_to_details'):
            self._to_details = Contact.from_header(self.headers['To'])
        return self._to_details

    @to_details.setter
    def to_details(self, to_details):
        self._to_details = to_details

    @property
    def contact_details(self):
        if not hasattr(self, '_contact_details'):
            if 'Contact' in self.headers:
                self._contact_details = Contact.from_header(self.headers['Contact'])
            else:
                self._contact_details = None
        return self._contact_details

    @contact_details.setter
    def contact_details(self, contact_details):
        self._contact_details = contact_details

    @property
    def content_type(self):
        return self.headers['Content-Type']

    @content_type.setter
    def content_type(self, content_type):
        self.headers['Content-Type'] = content_type

    @property
    def cseq(self):
        if not hasattr(self, '_cseq'):
            self._cseq = int(self.headers['CSeq'].split(' ')[0])
        return self._cseq

    @cseq.setter
    def cseq(self, cseq):
        self._cseq = int(cseq)

    @property
    def method(self):
        if not hasattr(self, '_method'):
            self._method = self.headers['CSeq'].split(' ')[1]
        return self._method

    @method.setter
    def method(self, method):
        self._method = method

    def __str__(self):
        if self._payload:
            self._raw_payload = self._payload.encode()
        elif not self._raw_payload:
            self._raw_payload = b''

        msg = self._make_headers()
        return msg + self.payload

    def encode(self, *args, **kwargs):
        if self._payload:
            self._raw_payload = self._payload.encode(*args, **kwargs)
        elif not self._raw_payload:
            self._raw_payload = b''

        msg = self._make_headers()
        return msg.encode(*args, **kwargs) + self._raw_payload

    def _make_headers(self):
        if hasattr(self, '_from_details'):
            self.headers['From'] = str(self.from_details)

        if hasattr(self, '_to_details'):
            self.headers['To'] = str(self.to_details)

        if hasattr(self, '_contact_details'):
            self.headers['Contact'] = str(self.contact_details)

        if hasattr(self, '_cseq'):
            self.headers['CSeq'] = '%s %s' % (self.cseq, self.method)
        elif hasattr(self, '_method'):
            self.headers['CSeq'] = '%s %s' % (self.cseq, self.method)

        self.headers['Content-Length'] = str(len(self._raw_payload))
        if 'Max-Forwards' not in self.headers:
            self.headers['Max-Forwards'] = '70'
        if 'Call-ID' not in self.headers:
            self.headers['Call-ID'] = uuid.uuid4()

        return self._format_headers()

    def _format_headers(self):
        msg = []
        for k, v in sorted(self.headers.items()):
            if k == 'Via':
                if isinstance(v, (list, tuple)):
                    msg = ['%s: %s' % (k, i) for i in v] + msg
                else:
                    msg.insert(0, '%s: %s' % (k, v))
            else:
                if isinstance(v, (list, tuple)):
                    msg.extend(['%s: %s' % (k, i) for i in v])
                else:
                    msg.append('%s: %s' % (k, v))
        msg.append(utils.EOL)
        return utils.EOL.join(msg)

    def parsed_xml(self):
        if 'Content-Type' not in self.headers:
            return None
        if not self.headers['Content-Type'].endswith('+xml'):
            return None
        return PyQuery(self.payload).remove_namespaces()

    @classmethod
    def from_raw_headers(cls, raw_headers):
        headers = CIMultiDict()
        decoded_headers = raw_headers.decode().split(utils.EOL)
        for line in decoded_headers[1:]:
            k, v = line.split(': ', 1)
            if k in headers:
                o = headers.setdefault(k, [])
                if not isinstance(o, list):
                    o = [o]
                o.append(v)
                headers[k] = o
            else:
                headers[k] = v

        m = FIRST_LINE_PATTERN['response']['regex'].match(decoded_headers[0])
        if m:
            d = m.groupdict()
            return Response(status_code=int(d['status_code']),
                            status_message=d['status_message'],
                            headers=headers,
                            first_line=decoded_headers[0])
        else:
            m = FIRST_LINE_PATTERN['request']['regex'].match(decoded_headers[0])
            if m:
                d = m.groupdict()
                cseq, _ = headers['CSeq'].split()
                return Request(method=d['method'],
                               headers=headers,
                               cseq=int(cseq),
                               first_line=decoded_headers[0])
            else:
                LOG.debug(decoded_headers)
                raise ValueError('Not a SIP message')


class Request(Message):
    def __init__(self,
                 method,
                 cseq,
                 from_details=None,
                 to_details=None,
                 contact_details=None,
                 headers=None,
                 payload=None,
                 first_line=None
                 ):

        super().__init__(
            headers=headers,
            payload=payload,
            from_details=from_details,
            to_details=to_details,
            contact_details=contact_details
        )

        self._method = method.upper()
        self._cseq = cseq

        if not first_line:
            self._first_line = FIRST_LINE_PATTERN['request']['str'].format(
                method=self.method,
                to_uri=str(self.to_details['uri'].short_uri())
            )
        else:
            self._first_line = first_line

    @property
    def to_details(self):
        if not hasattr(self, '_to_details'):
            self._to_details = Contact.from_header(self.headers['To'])
        return self._to_details

    @to_details.setter
    def to_details(self, to_details):
        self._to_details = to_details
        self._first_line = FIRST_LINE_PATTERN['request']['str'].format(method=self.method,
                                                                       to_uri=str(self._to_details['uri'].short_uri()))

    def __str__(self):
        return '%s%s%s' % (self._first_line, utils.EOL, super().__str__())

    def encode(self, *args, **kwargs):
        return self._first_line.encode(*args, **kwargs) + utils.BYTES_EOL + super().encode(*args, **kwargs)


class Response(Message):
    def __init__(self,
                 status_code,
                 status_message=None,
                 headers=None,
                 from_details=None,
                 to_details=None,
                 contact_details=None,
                 payload=None,
                 cseq=None,
                 method=None,
                 first_line=None
                 ):

        super().__init__(
            headers=headers,
            payload=payload,
            from_details=from_details,
            to_details=to_details,
            contact_details=contact_details
        )

        if not status_message:
            status_message = utils.STATUS[int(status_code)]

        if cseq:
            self._cseq = cseq
        elif 'CSeq' not in self.headers:
            raise ValueError('"CSeq" header or cseq is required')

        if method:
            self._method = method
        elif 'CSeq' not in self.headers:
            raise ValueError('"CSeq" header or method is required')

        self._status_code = status_code
        self._status_message = status_message

        if not first_line:
            self._first_line = FIRST_LINE_PATTERN['response']['str'].format(
                status_code=self._status_code,
                status_message=self._status_message
            )
        else:
            self._first_line = first_line

    @property
    def status_code(self):
        return self._status_code

    @status_code.setter
    def status_code(self, status_code):
        self._status_code = status_code
        self._first_line = FIRST_LINE_PATTERN['response']['str'].format(
            status_code=self._status_code,
            status_message=self._status_message
        )

    @property
    def status_message(self):
        return self._status_message

    @status_message.setter
    def status_message(self, status_message):
        self._status_message = status_message
        self._first_line = FIRST_LINE_PATTERN['response']['str'].format(
            status_code=self._status_code,
            status_message=self._status_message
        )

    @classmethod
    def from_request(cls, request, status_code, status_message, payload=None, headers=None):

        if not headers:
            headers = CIMultiDict()

        if 'Via' not in headers:
            headers['Via'] = request.headers['Via']

        return Response(
            status_code=status_code,
            status_message=status_message,
            cseq=request.cseq,
            method=request.method,
            headers=headers,
            from_details=request.from_details,
            to_details=request.to_details,
            contact_details=request.contact_details,
            payload=payload,
        )

    def __str__(self):
        return '%s%s%s' % (self._first_line, utils.EOL, super().__str__())

    def encode(self, *args, **kwargs):
        return self._first_line.encode(*args, **kwargs) + utils.BYTES_EOL + super().encode(*args, **kwargs)