from datetime import date as datedate, datetime, timedelta
import email.utils
import http.client as httplib
from http.cookies import SimpleCookie
import time
import calendar

from .common_helpers import (
    ts_props,
    touni,
    cookie_encode,
    parse_date,
    HeaderDict, HeaderProperty,
)
from .errors import OmbottException


def http_date(value):
    if isinstance(value, str):
        return value
    if isinstance(value, datetime):
        # aware datetime.datetime is converted to UTC time
        # naive datetime.datetime is treated as UTC time
        value = value.utctimetuple()
    elif isinstance(value, datedate):
        # datetime.date is naive, and is treated as UTC time
        value = value.timetuple()
    if not isinstance(value, (int, float)):
        # convert struct_time in UTC to UNIX timestamp
        value = calendar.timegm(value)
    return email.utils.formatdate(value, usegmt=True)


_response_slots = (
    '_status_line', '_status_code', 'headers', '_headers',
    '_cookies', 'body', '_ts_props'
)


class BaseResponse:
    """ Storage class for a response body as well as headers and cookies.

        This class does support dict-like case-insensitive item-access to
        headers, but is NOT a dict. Most notably, iterating over a response
        yields parts of the body and not the headers.

        :param body: The response body as one of the supported types.
        :param status: Either an HTTP status code (e.g. 200) or a status line
                       including the reason phrase (e.g. '200 OK').
        :param headers: A dictionary or a list of name-value pairs.

        Additional keyword arguments are added to the list of headers.
        Underscores in the header name are replaced with dashes.
    """

    headers: HeaderDict = None

    __slots__ = ()

    default_status = 200
    default_content_type = 'text/html; charset=UTF-8'

    # Header blacklist for specific response codes
    # (rfc2616 section 10.2.3 and 10.3.5)
    bad_headers = {
        204: {'Content-Type'},
        304: {'Allow', 'Content-Encoding', 'Content-Language',
              'Content-Length', 'Content-Range', 'Content-Type',
              'Content-Md5', 'Last-Modified'}
    }

    def __new__(cls, *a, **kw):
        self = super().__new__(cls, *a, **kw)
        self.headers = HeaderDict()
        return self

    def __init__(self, body='', status=None, headers=None, **more_headers):
        self._status_line = None
        self._status_code = None
        self._cookies = None
        self._headers = {}
        self.headers.dict = self._headers
        self.body = body
        self.status = status or self.default_status
        if headers:
            if isinstance(headers, dict):
                headers = headers.items()
            for name, value in headers:
                self.headers.append(name, value)

        if more_headers:
            for name, value in more_headers.items():
                self.headers.append(name, value)

    def copy(self, cls=None):
        ''' Returns a copy of self. '''
        cls = cls or BaseResponse
        assert issubclass(cls, BaseResponse)
        copy = cls(status = self.status, headers = self.headers.copy().dict)
        if self._cookies:
            copy._cookies = SimpleCookie()
            copy._cookies.load(self._cookies.output(header=''))
        return copy

    def __iter__(self):
        return iter(self.body)

    def close(self):
        close = getattr(self.body, 'close', None)
        if close:
            close()

    @property
    def status_line(self):
        ''' The HTTP status line as a string (e.g. ``404 Not Found``).'''
        return self._status_line

    @property
    def status_code(self):
        ''' The HTTP status code as an integer (e.g. 404).'''
        return self._status_code

    @property
    def status(self):
        """ A writeable property to change the HTTP response status.

            It accepts either a numeric code (100-999) or a string with
            a custom reason phrase (e.g. "404 Brain not found").
            Both :data:`status_line` and :data:`status_code` are updated
            accordingly. The return value is always a status string.

        """
        return self._status_line

    @status.setter
    def status(self, status):
        if isinstance(status, int):
            code, status = status, _HTTP_STATUS_LINES.get(status)
        elif ' ' in status:
            status = status.strip()
            code = int(status.split()[0])
        else:
            raise ValueError('String status line without a reason phrase.')
        if not 100 <= code <= 999:
            raise ValueError('Status code out of range.')
        self._status_code = code
        self._status_line = str(status or ('%d Unknown' % code))

    @property
    def headerlist(self):
        """ WSGI conform list of (header, value) tuples. """
        headers = self._headers.items()
        bad_headers = self.bad_headers.get(self._status_code)
        if bad_headers:
            headers = (h for h in headers if h[0] not in bad_headers)
            need_ctype = False
        else:
            need_ctype = 'Content-Type' not in self._headers
        out = [
            (name, val.encode('utf8').decode('latin1'))
            for (name, vals) in headers
            for val in (vals if isinstance(vals, list) else [vals])
        ]
        if need_ctype:
            out.append(('Content-Type', self.default_content_type))
        if self._cookies:
            for c in self._cookies.values():
                out.append(
                    ('Set-Cookie', c.OutputString().encode('utf8').decode('latin1'))
                )
        return out

    content_type = HeaderProperty('Content-Type')
    content_length = HeaderProperty('Content-Length', reader=int)
    expires = HeaderProperty(
        'Expires',
        reader=lambda x: datetime.utcfromtimestamp(parse_date(x)),
        writer=lambda x: http_date(x)
    )

    @property
    def charset(self, default='UTF-8'):
        """ Return the charset specified in the content-type header (default: utf8). """
        if 'charset=' in self.content_type:
            return self.content_type.split('charset=')[-1].split(';')[0].strip()
        return default

    def set_cookie(self, name, value, secret=None, **options):
        ''' Create a new cookie or replace an old one. If the `secret` parameter is
            set, create a `Signed Cookie` (described below).

            :param name: the name of the cookie.
            :param value: the value of the cookie.
            :param secret: a signature key required for signed cookies.

            Additionally, this method accepts all RFC 2109 attributes that are
            supported by :class:`cookie.Morsel`, including:

            :param max_age: maximum age in seconds. (default: None)
            :param expires: a datetime object or UNIX timestamp. (default: None)
            :param domain: the domain that is allowed to read the cookie.
              (default: current domain)
            :param path: limits the cookie to a given path (default: current path)
            :param secure: limit the cookie to HTTPS connections (default: off).
            :param httponly: prevents client-side javascript to read this cookie
              (default: off, requires Python 2.6 or newer).

            If neither `expires` nor `max_age` is set (default), the cookie will
            expire at the end of the browser session (as soon as the browser
            window is closed).

            Signed cookies may store any pickle-able object and are
            cryptographically signed to prevent manipulation. Keep in mind that
            cookies are limited to 4kb in most browsers.

            Warning: Signed cookies are not encrypted (the client can still see
            the content) and not copy-protected (the client can restore an old
            cookie). The main intention is to make pickling and unpickling
            save, not to store secret information at client side.
        '''
        if not self._cookies:
            self._cookies = SimpleCookie()

        if secret:
            value = touni(cookie_encode((name, value), secret))
        elif not isinstance(value, str):
            raise TypeError('Secret key missing for non-string Cookie.')

        if len(value) > 4096:
            raise ValueError('Cookie value to long.')

        self._cookies[name] = value

        for key, value in options.items():
            if key == 'max_age':
                if isinstance(value, timedelta):
                    value = value.seconds + value.days * 24 * 3600
            if key == 'expires':
                value = http_date(value)
            self._cookies[name][key.replace('_', '-')] = value

    def delete_cookie(self, key, **kwargs):
        ''' Delete a cookie. Be sure to use the same `domain` and `path`
            settings as used to create the cookie. '''
        kwargs['max_age'] = -1
        kwargs['expires'] = 0
        self.set_cookie(key, '', **kwargs)

    def __repr__(self):
        out = []
        for name, value in self.headerlist:
            out.append('%s: %s' % (name.title(), value.strip()))
        return '\n'.join(out)


@ts_props(
    '_status_line', '_status_code',
    '_headers', '_cookies', 'body',
    store_name='_ts_props'
)
class Response(BaseResponse):
    __slots__ = _response_slots


###############################################################################
# Exceptions & Commons ########################################################
###############################################################################
class HTTPResponse(BaseResponse, OmbottException):

    __slots__ = _response_slots

    def __init__(self, body='', status=None, headers=None, **more_headers):
        super().__init__(body, status, headers, **more_headers)

    def apply(self, response):
        response._status_code = self._status_code
        response._status_line = self._status_line
        response._headers.clear()
        response._headers.update(self._headers)
        if self._cookies:
            response._cookies = self._cookies
        response.body = self.body


class HTTPError(HTTPResponse):
    __slots__ = ('exception', 'traceback')
    default_status = 500

    def __init__(self, status=None, body=None, exception=None, traceback=None,
                 **options):
        self.exception = exception
        self.traceback = traceback
        super().__init__(body, status, **options)


#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
HTTP_CODES = httplib.responses
HTTP_CODES[418] = "I'm a teapot"  # RFC 2324
HTTP_CODES[422] = "Unprocessable Entity"  # RFC 4918
HTTP_CODES[428] = "Precondition Required"
HTTP_CODES[429] = "Too Many Requests"
HTTP_CODES[431] = "Request Header Fields Too Large"
HTTP_CODES[511] = "Network Authentication Required"
_HTTP_STATUS_LINES = {code: f'{code} {msg}' for (code, msg) in HTTP_CODES.items()}
