import asyncio
import json
import http.cookies

from urllib.parse import urlsplit, parse_qsl

from aiohttp.multidict import MultiDict, CIMultiDict, MultiDictProxy

from .errors import JsonLoadError, JsonDecodeError


__all__ = [
    'Request',
    'Response',
    ]


class Response:

    def __init__(self):
        self.headers = CIMultiDict()
        self._status_code = 200
        self._cookies = http.cookies.SimpleCookie()
        self._deleted_cookies = set()

    def _copy_cookies(self):
        for cookie in self._cookies.values():
            value = cookie.output(header='')[1:]
            self.headers.add('Set-Cookie', value)

    @property
    def cookies(self):
        return self._cookies

    def set_cookie(self, name, value, *, expires=None,
                   domain=None, max_age=None, path=None,
                   secure=None, httponly=None, version=None):
        """Set or update response cookie.

        Sets new cookie or updates existent with new value.
        Also updates only those params which are not None.
        """
        if name in self._deleted_cookies:
            self._deleted_cookies.remove(name)
            self._cookies.pop(name, None)

        self._cookies[name] = value
        c = self._cookies[name]
        if expires is not None:
            c['expires'] = expires
        if domain is not None:
            c['domain'] = domain
        if max_age is not None:
            c['max-age'] = max_age
        if path is not None:
            c['path'] = path
        if secure is not None:
            c['secure'] = secure
        if httponly is not None:
            c['httponly'] = httponly
        if version is not None:
            c['version'] = version

    def del_cookie(self, name, *, domain=None, path=None):
        """Delete cookie.

        Creates new empty expired cookie.
        """
        # TODO: do we need domain/path here?
        self._cookies.pop(name, None)
        self.set_cookie(name, '', max_age=0, domain=domain, path=path)
        self._deleted_cookies.add(name)

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

    @status_code.setter
    def status_code(self, value):
        assert isinstance(value, int), "Status code must be int"
        self._status_code = value


class Request:

    def __init__(self, host, message, req_body, *,
                 session_factory=None, loop=None,
                 identity_policy=None, auth_policy=None):
        if loop is None:
            loop = asyncio.get_event_loop()
        res = urlsplit(message.path)
        self._loop = loop
        self.version = message.version
        self.method = message.method.upper()
        self.host = message.headers.get('HOST', host)
        self.host_url = 'http://' + self.host
        self.path_qs = message.path
        self.path = res.path
        self.path_url = self.host_url + self.path
        self.url = self.host_url + self.path_qs
        self.query_string = res.query
        self.args = MultiDictProxy(MultiDict(parse_qsl(res.query)))
        self.headers = message.headers
        self.matchdict = {}
        self._request_body = req_body
        self._response = Response()
        self._session_factory = session_factory
        self._session_fut = None
        self._json_body = None
        self._cookies = None
        self._on_response = []
        self._identity_policy = identity_policy
        self._auth_policy = auth_policy

    @property
    def response(self):
        """Response object."""
        return self._response

    @property
    def session(self):
        if self._session_fut is None:
            self._session_fut = fut = asyncio.Future(loop=self._loop)
            if self._session_factory is not None:
                self._session_factory(self, fut)
            else:
                fut.set_result(None)
        return self._session_fut

    @property
    def json_body(self):
        if self._json_body is None:
            if self._request_body:
                # TODO: store generated exception and
                # don't try to parse json next time
                try:
                    decoded = self._request_body.decode('utf-8')
                    self._json_body = json.loads(decoded)
                except UnicodeDecodeError as exc:
                    raise JsonDecodeError(exc.encoding,
                                          exc.object,
                                          exc.start,
                                          exc.end,
                                          "JSON body is not utf-8 encoded",
                                          )
                except ValueError as exc:
                    raise JsonLoadError(
                        "JSON body can not be decoded", decoded)
            else:
                raise JsonLoadError("Request has no body")
        return self._json_body

    @property
    def cookies(self):
        """Return request cookies.

        A read-only dictionary-like object.
        """
        if self._cookies is None:
            raw = self.headers.get('COOKIE', '')
            parsed = http.cookies.SimpleCookie(raw)
            self._cookies = MultiDictProxy(MultiDict({key: val.value
                                                      for key, val in
                                                      parsed.items()}))
        return self._cookies

    def add_response_callback(self, callback, *args, **kwargs):
        """Add callback to be trigger when request is ready to be sent.
        """
        self._on_response.append((callback, args, kwargs))

    @asyncio.coroutine
    def _call_response_callbacks(self):
        callbacks = self._on_response[:]
        for callback, args, kwargs in callbacks:
            if asyncio.iscoroutinefunction(callback):
                yield from callback(self, *args, **kwargs)
            else:
                callback(self, *args, **kwargs)
        self.response._copy_cookies()

    @property
    def identity_policy(self):
        if not self._identity_policy:
            raise AttributeError('Identity policy not set')
        return self._identity_policy

    @property
    def auth_policy(self):
        if not self._auth_policy:
            raise AttributeError('Authorization policy not set')
        return self._auth_policy