"""
Copyright (c) 2015-2019 Red Hat, Inc
All rights reserved.

This software may be modified and distributed under the terms
of the BSD license. See the LICENSE file for details.


abstraction on top of http api calls
"""

from __future__ import print_function, absolute_import, unicode_literals

import sys
import logging
import json
from six.moves import http_client


from osbs.exceptions import OsbsException, OsbsNetworkException, OsbsResponseException
from osbs.constants import (
    HTTP_MAX_RETRIES, HTTP_BACKOFF_FACTOR, HTTP_RETRIES_STATUS_FORCELIST,
    HTTP_RETRIES_METHODS_WHITELIST, HTTP_REQUEST_TIMEOUT)

import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError, RetryError, Timeout
from requests.utils import guess_json_utf
try:
    from requests_kerberos import HTTPKerberosAuth
except ImportError:
    HTTPKerberosAuth = None

from urllib3.exceptions import InsecureRequestWarning
from urllib3.util import Retry
from urllib3 import disable_warnings
disable_warnings(InsecureRequestWarning)

logger = logging.getLogger(__name__)


class HttpSession(object):
    def __init__(self, verbose=False):
        self.verbose = verbose

    def get(self, url, **kwargs):
        return self.request(url, "get", **kwargs)

    def post(self, url, **kwargs):
        return self.request(url, "post", **kwargs)

    def put(self, url, **kwargs):
        return self.request(url, "put", **kwargs)

    def delete(self, url, **kwargs):
        return self.request(url, "delete", **kwargs)

    def request(self, url, *args, **kwargs):
        try:
            stream = HttpStream(url, *args, verbose=self.verbose, **kwargs)
            if kwargs.get('stream', False):
                return stream

            with stream as s:
                content = s.req.content
                return HttpResponse(s.status_code, s.headers, content)
        # Timeout will catch both ConnectTimout and ReadTimeout
        except (RetryError, Timeout) as ex:
            raise OsbsNetworkException(url, str(ex), '',
                                       cause=ex, traceback=sys.exc_info()[2])
        except HTTPError as ex:
            raise OsbsNetworkException(url, str(ex), ex.response.status_code,
                                       cause=ex, traceback=sys.exc_info()[2])
        except Exception as ex:
            raise OsbsException(cause=ex, traceback=sys.exc_info()[2])


class HttpStream(object):
    """
    Handle on HTTP response that is mostly useful for reading the server response incrementally when
    Transfer-Encoding: chunked is used.

    Users of this class should explicitly free the curl resources associated with it. The preferred
    way is to use it as a context manager which ensures that it is closed when exception is raised
    in the middle of reading the stream. Because it doesn't fit into our current API, the class also
    tries to free the resources when it finishes reading the http stream and also when it's garbage
    collected.
    """

    def __init__(self, url, method, data=None, kerberos_auth=False,
                 allow_redirects=True, verify_ssl=True, ca=None, use_json=False,
                 headers=None, stream=False, username=None, password=None,
                 client_cert=None, client_key=None, verbose=False, retries_enabled=True):
        self.finished = False  # have we read all data?
        self.closed = False    # have we destroyed curl resources?

        self.status_code = 0
        self.headers = None

        retry = Retry(
            total=HTTP_MAX_RETRIES,
            connect=HTTP_MAX_RETRIES,
            backoff_factor=HTTP_BACKOFF_FACTOR,
            status_forcelist=HTTP_RETRIES_STATUS_FORCELIST,
            method_whitelist=HTTP_RETRIES_METHODS_WHITELIST
        )
        self.session = requests.Session()
        if retries_enabled:
            self.session.mount('http://', HTTPAdapter(max_retries=retry))
            self.session.mount('https://', HTTPAdapter(max_retries=retry))

        self.url = url
        headers = headers or {}
        method = method.lower()

        if method not in ['post', 'get', 'put', 'delete']:
            raise RuntimeError("Unsupported method '%s' for curl call!" % method)

        args = {}

        if method in ['post', 'put']:
            headers['Expect'] = ''

        if not verify_ssl:
            args['verify'] = False
        else:
            if ca:
                args['verify'] = ca
            else:
                args['verify'] = True

        if username and password:
            args['auth'] = (username, password)

        if client_cert and client_key:
            args['cert'] = (client_cert, client_key)

        if data:
            args['data'] = data

        if use_json:
            headers['Content-Type'] = 'application/json'

        args['allow_redirects'] = allow_redirects

        if kerberos_auth:
            if not HTTPKerberosAuth:
                raise RuntimeError('Kerberos auth unavailable')
            args['auth'] = HTTPKerberosAuth()

        if stream:
            args['stream'] = True

        args['headers'] = headers
        args['timeout'] = HTTP_REQUEST_TIMEOUT

        self.req = self.session.request(method, url, **args)

        self.headers = self.req.headers
        self.status_code = self.req.status_code

    def _get_received_data(self):
        return self.req.text

    def iter_chunks(self):
        return self.req.iter_content(None)

    def iter_lines(self):
        kwargs = {
            # OpenShift does not respond with any encoding value.
            # This causes requests module to guess it as ISO-8859-1.
            # Likely, the encoding is actually UTF-8, but we can't
            # guarantee it. Therefore, we take the approach of simply
            # passing through the encoded data with no effort to
            # attempt decoding it.
            'decode_unicode': False
        }
        if requests.__version__.startswith('2.6.'):
            kwargs['chunk_size'] = 1
        # if this fails for any reason other than ChunkedEncodingError
        # or IncompleteRead (either of which may happen when no bytes
        # are received), let someone else handle the exception
        try:
            for line in self.req.iter_lines(**kwargs):
                yield line
        except (requests.exceptions.ChunkedEncodingError,
                http_client.IncompleteRead):
            return

    def close(self):
        if not self.closed:
            logger.debug("cleaning up")
            if hasattr(self, 'req'):
                del self.req
        self.closed = True

    def __del__(self):
        self.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


class HttpResponse(object):
    def __init__(self, status_code, headers, content):
        self.status_code = status_code
        self.headers = headers
        self.content = content

    def json(self, check=True):
        encoding = guess_json_utf(self.content)
        text = self.content.decode(encoding)
        if check and self.status_code not in (0, requests.codes.OK, requests.codes.CREATED):
            raise OsbsResponseException(text, self.status_code)

        try:
            return json.loads(text)
        except ValueError:
            msg = '{}Headers {}\nContent {}'.format('HtttpResponse has corrupt json:\n',
                                                    self.headers, self.content)
            logger.exception(msg)
            raise OsbsResponseException(msg, self.status_code)