# 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