# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Module which contains common Kubernetes related code.
"""

from typing import Optional

import os
import base64
import warnings

from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import b

from libcloud.common.base import JsonResponse, ConnectionUserAndKey
from libcloud.common.base import KeyCertificateConnection, ConnectionKey
from libcloud.common.types import InvalidCredsError

__all__ = [
    'KubernetesException',

    'KubernetesBasicAuthConnection',
    'KubernetesTLSAuthConnection',
    'KubernetesTokenAuthConnection',

    'KubernetesDriverMixin',

    'VALID_RESPONSE_CODES'
]

VALID_RESPONSE_CODES = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
                        httplib.NO_CONTENT]


class KubernetesException(Exception):

    def __init__(self, code, message):
        self.code = code
        self.message = message
        self.args = (code, message)

    def __str__(self):
        return "%s %s" % (self.code, self.message)

    def __repr__(self):
        return "KubernetesException %s %s" % (self.code, self.message)


class KubernetesResponse(JsonResponse):

    valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
                            httplib.NO_CONTENT]

    def parse_error(self):
        if self.status == 401:
            raise InvalidCredsError('Invalid credentials')
        return self.body

    def success(self):
        return self.status in self.valid_response_codes


class KubernetesTLSAuthConnection(KeyCertificateConnection):
    responseCls = KubernetesResponse
    timeout = 60

    def __init__(self, key, secure=True, host='localhost',
                 port='6443', key_file=None, cert_file=None,
                 **kwargs):

        super(KubernetesTLSAuthConnection, self).__init__(
            key_file=key_file,
            cert_file=cert_file,
            secure=secure, host=host,
            port=port, url=None,
            proxy_url=None,
            timeout=None,
            backoff=None,
            retry_delay=None)

        if key_file:
            keypath = os.path.expanduser(key_file)
            is_file_path = os.path.exists(keypath) and os.path.isfile(keypath)

            if not is_file_path:
                raise InvalidCredsError(
                    'You need an key PEM file to authenticate '
                    'via tls. For more info please visit:'
                    'https://kubernetes.io/docs/concepts/'
                    'cluster-administration/certificates/')

            self.key_file = key_file

            certpath = os.path.expanduser(cert_file)
            is_file_path = os.path.exists(
                certpath) and os.path.isfile(certpath)

            if not is_file_path:
                raise InvalidCredsError(
                    'You need an certificate PEM file to authenticate '
                    'via tls. For more info please visit:'
                    'https://kubernetes.io/docs/concepts/'
                    'cluster-administration/certificates/'
                )

            self.cert_file = cert_file

    def add_default_headers(self, headers):
        if 'Content-Type' not in headers:
            headers['Content-Type'] = 'application/json'
        return headers


class KubernetesTokenAuthConnection(ConnectionKey):
    responseCls = KubernetesResponse
    timeout = 60

    def add_default_headers(self, headers):
        if 'Content-Type' not in headers:
            headers['Content-Type'] = 'application/json'
        if self.key:
            headers['Authorization'] = 'Bearer ' + self.key
        else:
            raise ValueError("Please provide a valid token in the key param")
        return headers


class KubernetesBasicAuthConnection(ConnectionUserAndKey):
    responseCls = KubernetesResponse
    timeout = 60

    def add_default_headers(self, headers):
        """
        Add parameters that are necessary for every request
        If user and password are specified, include a base http auth
        header
        """
        if 'Content-Type' not in headers:
            headers['Content-Type'] = 'application/json'

        if self.user_id and self.key:
            auth_string = b('%s:%s' % (self.user_id, self.key))
            user_b64 = base64.b64encode(auth_string)
            headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8'))
        return headers


class KubernetesDriverMixin(object):
    """
    Base driver class to be used with various Kubernetes drivers.

    NOTE: This base class can be used in different APIs such as container and
    compute one.
    """

    def __init__(self, key=None, secret=None, secure=False, host='localhost',
                 port=4243, key_file=None, cert_file=None, ca_cert=None,
                 ex_token_bearer_auth=False):
        """
        :param    key: API key or username to be used (required)
        :type     key: ``str``

        :param    secret: Secret password to be used (required)
        :type     secret: ``str``

        :param    secure: Whether to use HTTPS or HTTP. Note: Some providers
                          only support HTTPS, and it is on by default.
        :type     secure: ``bool``

        :param    host: Override hostname used for connections.
        :type     host: ``str``

        :param    port: Override port used for connections.
        :type     port: ``int``

        :param    key_file: Path to the key file used to authenticate (when
                            using key file auth).
        :type     key_file: ``str``

        :param    cert_file: Path to the cert file used to authenticate (when
                             using key file auth).
        :type     cert_file: ``str``

        :param    ex_token_bearer_auth: True to use token bearer auth.
        :type     ex_token_bearer_auth: ``bool``

        :return: ``None``
        """
        if ex_token_bearer_auth:
            self.connectionCls = KubernetesTokenAuthConnection
            if not key:
                msg = 'The token must be a string provided via "key" argument'
                raise ValueError(msg)
            secure = True

        if key_file or cert_file:
            # Certificate based auth is used
            if not (key_file and cert_file):
                raise ValueError("Both key and certificate files are needed")

        if key_file:
            self.connectionCls = KubernetesTLSAuthConnection
            self.key_file = key_file
            self.cert_file = cert_file
            secure = True

        if host and host.startswith('https://'):
            secure = True

        host = self._santize_host(host=host)

        super(KubernetesDriverMixin, self).__init__(key=key, secret=secret,
                                                    secure=secure,
                                                    host=host,
                                                    port=port,
                                                    key_file=key_file,
                                                    cert_file=cert_file)

        if ca_cert:
            self.connection.connection.ca_cert = ca_cert
        else:
            # do not verify SSL certificate
            warnings.warn("Kubernetes has its own CA, since you didn't supply "
                          "a CA certificate be aware that SSL verification "
                          "will be disabled for this session.")
            self.connection.connection.ca_cert = False

        self.connection.secure = secure
        self.connection.host = host

        if port is not None:
            self.connection.port = port

    def _ex_connection_class_kwargs(self):
        kwargs = {}
        if hasattr(self, 'key_file'):
            kwargs['key_file'] = self.key_file
        if hasattr(self, 'cert_file'):
            kwargs['cert_file'] = self.cert_file
        return kwargs

    def _santize_host(self, host=None):
        # type: (Optional[str]) -> Optional[str]
        """
        Sanitize "host" argument any remove any protocol prefix (if specified).
        """
        if not host:
            return None

        prefixes = ['http://', 'https://']
        for prefix in prefixes:
            if host.startswith(prefix):
                host = host.lstrip(prefix)

        return host
