"""
Harbor Client interface. Handles the REST calls and responses.
"""

import copy
import hashlib
import logging
from urlparse import urlparse

from oslo_utils import importutils
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

try:
    import json
except ImportError:
    import simplejson as json

from harborclient import api_versions
from harborclient import exceptions
from harborclient import utils

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


class HTTPClient(object):
    USER_AGENT = 'python-harborclient'

    def __init__(self,
                 username,
                 password,
                 project,
                 baseurl,
                 timeout=None,
                 timings=False,
                 http_log_debug=False,
                 cacert=None,
                 insecure=False,
                 api_version=None):
        self.username = username
        self.password = password
        self.project = project
        self.baseurl = baseurl
        self.api_version = api_version or api_versions.APIVersion()
        self.timings = timings
        self.http_log_debug = http_log_debug
        # Has no protocol, use http
        if not urlparse(baseurl).scheme:
            self.baseurl = 'http://' + baseurl
        parsed_url = urlparse(self.baseurl)
        self.protocol = parsed_url.scheme
        self.host = parsed_url.hostname
        self.port = parsed_url.port
        if timeout is not None:
            self.timeout = float(timeout)
        else:
            self.timeout = None
        # https
        if insecure:
            self.verify_cert = False
        else:
            if cacert:
                self.verify_cert = cacert
            else:
                self.verify_cert = True
        self.times = []  # [("item", starttime, endtime), ...]

        self._logger = logging.getLogger(__name__)
        self.session_id = None

        if self.http_log_debug and not self._logger.handlers:
            # Logging level is already set on the root logger
            ch = logging.StreamHandler()
            self._logger.addHandler(ch)
            self._logger.propagate = False
            if hasattr(requests, 'logging'):
                rql = requests.logging.getLogger(requests.__name__)
                rql.addHandler(ch)
                # Since we have already setup the root logger on debug, we
                # have to set it up here on WARNING (its original level)
                # otherwise we will get all the requests logging messages
                rql.setLevel(logging.WARNING)

    def unauthenticate(self):
        """Forget all of our authentication information."""
        requests.get(
            '%s://%s/logout' % (self.protocol, self.host),
            cookies={'beegosessionID': self.session_id},
            verify=self.verify_cert)
        logging.debug("Successfully logout")

    def get_timings(self):
        return self.times

    def reset_timings(self):
        self.times = []

    def _redact(self, target, path, text=None):
        """Replace the value of a key in `target`.

        The key can be at the top level by specifying a list with a single
        key as the path. Nested dictionaries are also supported by passing a
        list of keys to be navigated to find the one that should be replaced.
        In this case the last one is the one that will be replaced.

        :param dict target: the dictionary that may have a key to be redacted;
                            modified in place
        :param list path: a list representing the nested structure in `target`
                          that should be redacted; modified in place
        :param string text: optional text to use as a replacement for the
                            redacted key. if text is not specified, the
                            default text will be sha1 hash of the value being
                            redacted
        """

        key = path.pop()

        # move to the most nested dict
        for p in path:
            try:
                target = target[p]
            except KeyError:
                return

        if key in target:
            if text:
                target[key] = text
            elif target[key] is not None:
                # because in python3 byte string handling is ... ug
                value = target[key].encode('utf-8')
                sha1sum = hashlib.sha1(value)
                target[key] = "{SHA1}%s" % sha1sum.hexdigest()

    def http_log_req(self, method, url, kwargs):
        if not self.http_log_debug:
            return

        string_parts = ['curl -g -i']

        if self.verify_cert is not None:
            if not self.verify_cert:
                string_parts.append(' --insecure')

        string_parts.append(" '%s'" % url)
        string_parts.append(' -X %s' % method)

        headers = copy.deepcopy(kwargs['headers'])
        # because dict ordering changes from 2 to 3
        keys = sorted(headers.keys())
        for name in keys:
            value = headers[name]
            header = ' -H "%s: %s"' % (name, value)
            string_parts.append(header)
        cookies = kwargs['cookies']
        for name in sorted(cookies.keys()):
            value = cookies[name]
            cookie = header = ' -b "%s: %s"' % (name, value)
            string_parts.append(cookie)
        if 'data' in kwargs:
            data = json.loads(kwargs['data'])
            string_parts.append(" -d '%s'" % json.dumps(data))
        self._logger.debug("REQ: %s" % "".join(string_parts))

    def http_log_resp(self, resp):
        if not self.http_log_debug:
            return

        if resp.text and resp.status_code != 400:
            try:
                body = json.loads(resp.text)
            except ValueError:
                body = None
        else:
            body = None

        self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: "
                           "%(text)s\n", {
                               'status': resp.status_code,
                               'headers': resp.headers,
                               'text': json.dumps(body)
                           })

    def request(self, url, method, **kwargs):
        url = self.baseurl + "/api" + url
        kwargs.setdefault('headers', kwargs.get('headers', {}))
        kwargs['headers']['User-Agent'] = self.USER_AGENT
        kwargs['headers']['Accept'] = 'application/json'
        if 'body' in kwargs:
            kwargs['headers']['Content-Type'] = 'application/json'
            kwargs['data'] = json.dumps(kwargs['body'])
            del kwargs['body']
        kwargs["headers"]['Harbor-API-Version'] = "v2"
        if self.timeout is not None:
            kwargs.setdefault('timeout', self.timeout)

        self.http_log_req(method, url, kwargs)

        resp = requests.request(method, url, verify=self.verify_cert, **kwargs)
        self.http_log_resp(resp)
        if resp.status_code >= 400:
            raise exceptions.from_response(resp, resp.text, url, method)
        try:
            body = json.loads(resp.text)
        except ValueError:
            body = resp.text
        return body

    def _time_request(self, url, method, **kwargs):
        with utils.record_time(self.times, self.timings, method, url):
            body = self.request(url, method, **kwargs)
        return body

    def _cs_request(self, url, method, **kwargs):
        if not self.session_id:
            self.authenticate()
        # Perform the request once. If we get a 401 back then it
        # might be because the auth token expired, so try to
        # re-authenticate and try again. If it still fails, bail.
        try:
            body = self._time_request(
                url,
                method,
                cookies={'beegosessionID': self.session_id},
                **kwargs)
            return body
        except exceptions.Unauthorized as e:
            try:
                # first discard auth token, to avoid the possibly expired
                # token being re-used in the re-authentication attempt
                self.unauthenticate()
                # overwrite bad token
                self.authenticate()
                body = self._time_request(url, method, **kwargs)
                return body
            except exceptions.Unauthorized:
                raise e

    def get(self, url, **kwargs):
        return self._cs_request(url, 'GET', **kwargs)

    def post(self, url, **kwargs):
        return self._cs_request(url, 'POST', **kwargs)

    def put(self, url, **kwargs):
        return self._cs_request(url, 'PUT', **kwargs)

    def delete(self, url, **kwargs):
        return self._cs_request(url, 'DELETE', **kwargs)

    def authenticate(self):
        if not self.baseurl:
            msg = ("Authentication requires 'baseurl', which should be "
                   "specified in '%s'") % self.__class__.__name__
            raise exceptions.AuthorizationFailure(msg)

        if not self.username:
            msg = ("Authentication requires 'username', which should be "
                   "specified in '%s'") % self.__class__.__name__
            raise exceptions.AuthorizationFailure(msg)

        if not self.password:
            msg = ("Authentication requires 'password', which should be "
                   "specified in '%s'") % self.__class__.__name__
            raise exceptions.AuthorizationFailure(msg)

        try:
            resp = requests.post(
                self.baseurl + "/login",
                data={'principal': self.username,
                      'password': self.password},
                verify=self.verify_cert)
        except requests.exceptions.SSLError:
            msg = ("Certificate verify failed, please use '--os-cacert' option"
                   " to specify a CA bundle file to use in verifying a TLS"
                   " (https) server certificate or use '--insecure' option"
                   " to explicitly allow client to perform insecure"
                   " TLS (https) requests.")
            raise exceptions.AuthorizationFailure(msg)
        if resp.status_code == 200:
            self.session_id = resp.cookies.get('beegosessionID')
            logging.debug(
                "Successfully login, session id: %s" % self.session_id)
        if resp.status_code >= 400:
            msg = resp.text or ("The request you have made requires "
                                "authentication. (HTTP 401)")
            reason = '{"reason": "%s", "message": "%s"}' % (resp.reason, msg)
            raise exceptions.AuthorizationFailure(reason)


def _construct_http_client(username=None,
                           password=None,
                           project=None,
                           baseurl=None,
                           timeout=None,
                           extensions=None,
                           timings=False,
                           http_log_debug=False,
                           user_agent='python-harborclient',
                           api_version=None,
                           insecure=False,
                           cacert=None,
                           **kwargs):
    return HTTPClient(
        username,
        password,
        project,
        baseurl,
        timeout=timeout,
        timings=timings,
        http_log_debug=http_log_debug,
        insecure=insecure,
        cacert=cacert,
        api_version=api_version)


def _get_client_class_and_version(version):
    if not isinstance(version, api_versions.APIVersion):
        version = api_versions.get_api_version(version)
    else:
        api_versions.check_major_version(version)
    if version.is_latest():
        raise exceptions.UnsupportedVersion(("The version should be explicit, "
                                             "not latest."))
    return version, importutils.import_class(
        "harborclient.v%s.client.Client" % version.ver_major)


def get_client_class(version):
    """Returns Client class based on given version."""
    _api_version, client_class = _get_client_class_and_version(version)
    return client_class


def Client(version,
           username=None,
           password=None,
           project=None,
           baseurl=None,
           insecure=False,
           cacert=None,
           *args,
           **kwargs):
    """Initialize client object based on given version.

    HOW-TO:
    The simplest way to create a client instance is initialization with your
    credentials::

        >>> from harborclient import client
        >>> harbor = client.Client(VERSION, USERNAME, PASSWORD,
        ...                        PROJECT, HARBOR_URL)

    Here ``VERSION`` can be a string or
    ``harborclient.api_versions.APIVersion`` obj. If you prefer string value,
    you can use ``1.1`` (deprecated now), ``2`` or ``2.X``
    (where X is a microversion).


    Alternatively, you can create a client instance using the keystoneauth
    session API. See "The harborclient Python API" page at
    python-harborclient's doc.
    """
    api_version, client_class = _get_client_class_and_version(version)
    kwargs.pop("direct_use", None)
    return client_class(
        username=username,
        password=password,
        project=project,
        baseurl=baseurl,
        api_version=api_version,
        insecure=insecure,
        cacert=cacert,
        *args,
        **kwargs)