from __future__ import print_function, unicode_literals

try:
    from urllib.parse import urljoin, urlparse, urlunparse
except ImportError:
    from urlparse import urljoin, urlparse, urlunparse

try:
    from urllib.request import urlopen, Request
    from urllib.error import HTTPError
except ImportError:
    from urllib2 import urlopen, Request, HTTPError

import json

from .camera import UnifiVideoCamera
from .recording import UnifiVideoRecording
from .collections import UnifiVideoCollection

from distutils.version import LooseVersion

try:
    type(unicode)
except NameError:
    unicode = str

endpoints = {
    'login': 'login',
    'cameras': 'camera',
    'recordings': lambda x: 'recording?idsOnly=false&' \
        'sortBy=startTime&sort=desc&limit={}'.format(x),
    'bootstrap': 'bootstrap',
}

class UnifiVideoVersionError(ValueError):
    """Unsupported UniFi Video version"""

    def __init__(self, message=None):
        if not message:
            message = 'Unsupported UniFi Video version'
        super(UnifiVideoVersionError, self).__init__(message)


class UnifiVideoAPI(object):
    """Encapsulates a single UniFi Video server.

    Arguments:
        api_key (str): UniFi Video API key
        username (str): UniFi Video account username
        password (str): UniFi Video account pasword
        addr (str): UniFi Video host address
        port (int): UniFi Video host port
        schema (str): Protocol schema to use. Valid values: `http`, `https`
        verify_cert (bool): Whether to verify UniFi Video's TLS cert when
            connecting over HTTPS
        check_ufv_version (bool): Set to ``False`` to use with untested
            UniFi Video versions

    Note:

        At minimum, you have to

        - provide either an API key or a username:password pair
        - set the host address and port to wherever your UniFi Video
          is listening at

    Attributes:
        _data (dict): UniFi Video "bootstrap" JSON as a dict
        base_url (str): API base URL
        api_key (str or NoneType): API key (from input params)
        username (str or NoneType): Username (from input params)
        password (str or NoneType): Password (from input params)
        name (str or NoneType): UniFi Video server name
        version (str or NoneType): UniFi Video version
        jsession_av (str or NoneType): UniFi Video session ID

        cameras (:class:`~unifi_video.collections.UnifiVideoCollection`):
            Collection of :class:`~unifi_video.camera.UnifiVideoCamera` objects

        recordings (:class:`~unifi_video.collections.UnifiVideoCollection`):
            Collection of :class:`~unifi_video.recording.UnifiVideoRecording`
            objects
    """

    _supported_ufv_versions = []
    _supported_ufv_version_ranges = [
        ['3.9.12', '3.10.11'],
    ]

    def __init__(self, api_key=None, username=None, password=None,
            addr='localhost', port=7080, schema='http', verify_cert=True,
            check_ufv_version=True):

        if not verify_cert and schema == 'https':
            import ssl
            self._ssl_context = ssl._create_unverified_context()

        if not api_key and not (username and password):
            raise ValueError('To init {}, provide either API key ' \
                'or username password pair'.format(type(self).__name__))

        self.api_key = api_key
        self.login_attempts = 0
        self.jsession_av = None
        self.username = username
        self.password = password
        self.base_url = '{}://{}:{}/api/2.0/'.format(schema, addr, port)
        self._version_stickler = check_ufv_version

        self._load_data(self.get(endpoints['bootstrap']))

        self.cameras = UnifiVideoCollection(UnifiVideoCamera)
        self.recordings = UnifiVideoCollection(UnifiVideoRecording)
        self.refresh_cameras()
        self.refresh_recordings()

    def _load_data(self, data):
        if not isinstance(data, dict):
            raise ValueError('Server responded with unknown bootstrap data')
        self._data = data.get('data', [{}])
        self.name = self._data[0].get('nvrName', None)
        self.version = self._data[0].get('systemInfo', {}).get('version', None)

        self._is_supported = False

        if self.version in UnifiVideoAPI._supported_ufv_versions:
            self._is_supported = True
        else:
            v_actual = LooseVersion(self.version)
            for curr_version in UnifiVideoAPI._supported_ufv_version_ranges:
                v_low = LooseVersion(curr_version[0])
                v_high = LooseVersion(curr_version[1])
                try:
                    if v_actual >= v_low and v_actual <= v_high:
                        self._is_supported = True
                        break
                except TypeError as e:
                    break

        if self._version_stickler and not self._is_supported:
            raise UnifiVideoVersionError()

    def _ensure_headers(self, req):
        req.add_header('Content-Type', 'application/json')
        if self.jsession_av:
            req.add_header('Cookie', 'JSESSIONID_AV={}'\
                .format(self.jsession_av))

    def _build_req(self, url, data=None, method=None):
        url = urljoin(self.base_url, url)
        if self.api_key:
            _s, _nloc, _path, _params, _q, _f = urlparse(url)
            _q = '{}&apiKey={}'.format(_q, self.api_key) if len(_q) \
                else 'apiKey={}'.format(self.api_key)
            url = urlunparse((_s, _nloc, _path, _params, _q, _f))
        req = Request(url, bytes(json.dumps(data).encode('utf8'))) \
            if data else Request(url)
        self._ensure_headers(req)
        if method:
            req.get_method = lambda: method
        return req

    def _parse_cookies(self, res, return_existing=False):
        if 'Set-Cookie' not in res.headers:
            return False
        cookies = res.headers['Set-Cookie'].split(',')
        for cookie in cookies:
            for part in cookie.split(';'):
                if 'JSESSIONID_AV' in part:
                    self.jsession_av = part\
                        .replace('JSESSIONID_AV=', '').strip()
                    return True

    def _urlopen(self, req):
        try:
            return urlopen(req, context=self._ssl_context)
        except AttributeError:
            return urlopen(req)

    def _get_response_content(self, res, raw=False):
        try:
            if res.headers['Content-Type'] == 'application/json':
                return json.loads(res.read().decode('utf8'))
            raise KeyError
        except KeyError:
            upstream_filename = None

            if 'Content-Disposition' in res.headers:
                for part in res.headers['Content-Disposition'].split(';'):
                    part = part.strip()
                    if part.startswith('filename='):
                        upstream_filename = part.split('filename=').pop()

            if isinstance(raw, str) or isinstance(raw, unicode):
                filename = raw if len(raw) else upstream_filename
                with open(filename, 'wb') as f:
                    while True:
                        chunk = res.read(4096)
                        if not chunk:
                            break
                        f.write(chunk)
                    f.truncate()
                    return True
            elif isinstance(raw, bool):
                return res.read()
            else:
                try:
                    return res.read().decode('utf8')
                except UnicodeDecodeError:
                    return res.read()

    def _handle_http_401(self, url, raw):
        if self.api_key:
            raise ValueError('Invalid API key')
        elif self.login():
            return self.get(url, raw)

    def get(self, url, raw=False):
        """Send GET request.

        Arguments:
            url (str): API endpoint (relative to the API base URL)
            raw (str or bool): Set `str` filename if you want to save the
                response to a file.  Set to ``True``  if you want the to
                return raw response data.

        Returns:
            Response JSON (as `dict`) when `Content-Type` response header is
            `application/json`

            ``True`` if ``raw`` is `str` (filename) and a file was
            successfully written to

            Raw response body (as `bytes`) if the `raw` input param is of type
            `bool`

            ``False`` on HTTP 4xx - 5xx

        :rtype: NoneType, bool, dict, bytes
        """

        req = self._build_req(url)
        try:
            res = self._urlopen(req)
            self._parse_cookies(res)
            return self._get_response_content(res, raw)
        except HTTPError as err:
            if err.code == 401 and self.login_attempts == 0:
                return self._handle_http_401(url, raw)
            return False

    def post(self, url, data=None, raw=False, _method=None):
        """Send POST request.

        Args:
            url (str): API endpoint (relative to the API base URL)
            data (dict or NoneType): Request body
            raw (str or bool): Filename (`str`) if you want the response
                saved to a file, ``True`` (`bool`) if you want the response
                body as return value

        Returns:
            See :func:`~unifi_video.api.get`.

        """

        if data:
            req = self._build_req(url, data, _method)
        else:
            req = self._build_req(url, method=_method)
        try:
            res = self._urlopen(req)
            self._parse_cookies(res)
            return self._get_response_content(res, raw)
        except HTTPError as err:
            if err.code == 401 and url != 'login' and self.login_attempts == 0:
                return self._handle_http_401(url, raw)
            return False

    def put(self, url, data=None, raw=False):
        """Send PUT request.

        Thin wrapper around :func:`~unifi_video.api.post`; the
        same parameter/return semantics apply here.
        """

        return self.post(url, data, raw, 'PUT')

    def delete(self, url, data=None, raw=False):
        """Send DELETE request.

        Thin wrapper around :func:`~unifi_video.api.post`; the
        same parameter/return semantics apply here.
        """

        return self.post(url, data, raw, 'DELETE')

    def login(self):
        self.login_attempts = 1
        res_data = self.post(endpoints['login'], {
            'username': self.username,
            'password': self.password})
        if res_data:
            self.login_attempts = 0
            return True
        else:
            return False

    def refresh_cameras(self):
        """GET cameras from the server and update ``self.cameras``.
        """

        cameras = self.get(endpoints['cameras'])
        if isinstance(cameras, dict):
            for camera in cameras.get('data', []):
                self.cameras.add(UnifiVideoCamera(self, camera))

    def refresh_recordings(self, limit=300):
        """GET recordings from the server and update ``self.recordings``.

        :param int limit: Limit the number of recording items
            to fetch (``0`` for no limit).
        """

        recordings = self.get(endpoints['recordings'](limit))
        if isinstance(recordings, dict):
            for recording in recordings.get('data', []):
                self.recordings.add(UnifiVideoRecording(self, recording))

    def get_camera(self, search_term):
        """Get a camera whose :attr:`~unifi_video.UnifiVideoCamera.name`,
        :attr:`~unifi_video.UnifiVideoCamera._id`, or
        :attr:`~unifi_video.UnifiVideoCamera.overlay_text` matches `search_term`.

        Returns:
            :class:`~unifi_video.camera.UnifiVideoCamera` or `NoneType`
            depending on whether or not `search_term` was matched to a camera.
        """

        search_term = search_term.lower()
        for camera in self.cameras:
            if camera._id == search_term or \
                    camera.name.lower() == search_term or \
                    camera.overlay_text.lower() == search_term:
                return camera

    def __str__(self):
        return '{}: {}'.format(type(self).__name__, {
            'name': self.name,
            'version': self.version,
            'supported_version': self._is_supported
        })


__all__ = ['UnifiVideoAPI']