# Copyright: (c) 2021, Red Hat | Ansible
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

import hashlib
import os
from typing import Any, Dict, List, Optional

from ansible.module_utils.six import iteritems, string_types
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
    AUTH_ARG_MAP,
    AUTH_ARG_SPEC,
    AUTH_PROXY_HEADERS_SPEC,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
    requires as _requires,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
    CoreException,
)

try:
    from ansible_collections.kubernetes.core.plugins.module_utils import (
        k8sdynamicclient,
    )
    from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import (
        LazyDiscoverer,
    )
except ImportError:
    # Handled in module setup
    pass

try:
    import kubernetes
    from kubernetes.dynamic.exceptions import (
        ResourceNotFoundError,
        ResourceNotUniqueError,
    )
    from kubernetes.dynamic.resource import Resource
except ImportError:
    # kubernetes import error is handled in module setup
    # This is defined only for the sake of Ansible's checked import requirement
    Resource = Any  # type: ignore

try:
    import urllib3

    urllib3.disable_warnings()
except ImportError:
    # Handled in module setup
    pass


_pool = {}


class unique_string(str):
    _low = None

    def __hash__(self):
        return id(self)

    def __eq__(self, other):
        return self is other

    def lower(self):
        if self._low is None:
            lower = str.lower(self)
            if str.__eq__(lower, self):
                self._low = self
            else:
                self._low = unique_string(lower)
        return self._low


def _create_auth_spec(module=None, **kwargs) -> Dict:
    auth: Dict = {}
    # If authorization variables aren't defined, look for them in environment variables
    for true_name, arg_name in AUTH_ARG_MAP.items():
        if module and module.params.get(arg_name) is not None:
            auth[true_name] = module.params.get(arg_name)
        elif arg_name in kwargs and kwargs.get(arg_name) is not None:
            auth[true_name] = kwargs.get(arg_name)
        elif true_name in kwargs and kwargs.get(true_name) is not None:
            # Aliases in kwargs
            auth[true_name] = kwargs.get(true_name)
        elif arg_name == "proxy_headers":
            # specific case for 'proxy_headers' which is a dictionary
            proxy_headers = {}
            for key in AUTH_PROXY_HEADERS_SPEC.keys():
                env_value = os.getenv(
                    "K8S_AUTH_PROXY_HEADERS_{0}".format(key.upper()), None
                )
                if env_value is not None:
                    if AUTH_PROXY_HEADERS_SPEC[key].get("type") == "bool":
                        env_value = env_value.lower() not in ["0", "false", "no"]
                    proxy_headers[key] = env_value
            if proxy_headers is not {}:
                auth[true_name] = proxy_headers
        else:
            env_value = os.getenv(
                "K8S_AUTH_{0}".format(arg_name.upper()), None
            ) or os.getenv("K8S_AUTH_{0}".format(true_name.upper()), None)
            if env_value is not None:
                if AUTH_ARG_SPEC[arg_name].get("type") == "bool":
                    env_value = env_value.lower() not in ["0", "false", "no"]
                auth[true_name] = env_value

    return auth


def _load_config(auth: Dict) -> None:
    kubeconfig = auth.get("kubeconfig")
    optional_arg = {
        "context": auth.get("context"),
        "persist_config": auth.get("persist_config"),
    }
    if kubeconfig:
        if isinstance(kubeconfig, string_types):
            kubernetes.config.load_kube_config(config_file=kubeconfig, **optional_arg)
        elif isinstance(kubeconfig, dict):
            kubernetes.config.load_kube_config_from_dict(
                config_dict=kubeconfig, **optional_arg
            )
    else:
        kubernetes.config.load_kube_config(config_file=None, **optional_arg)


def _create_configuration(auth: Dict):
    def auth_set(*names: list) -> bool:
        return all(auth.get(name) for name in names)

    if auth_set("host"):
        # Removing trailing slashes if any from hostname
        auth["host"] = auth.get("host").rstrip("/")

    if (
        auth_set("username", "password", "host")
        or auth_set("api_key", "host")
        or auth_set("cert_file", "key_file", "host")
    ):
        # We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
        pass
    elif auth_set("kubeconfig") or auth_set("context"):
        try:
            _load_config(auth)
        except Exception as err:
            raise err

    else:
        # First try to do incluster config, then kubeconfig
        try:
            kubernetes.config.load_incluster_config()
        except kubernetes.config.ConfigException:
            try:
                _load_config(auth)
            except Exception as err:
                raise err

    # Override any values in the default configuration with Ansible parameters
    # As of kubernetes-client v12.0.0, get_default_copy() is required here
    try:
        configuration = kubernetes.client.Configuration().get_default_copy()
    except AttributeError:
        configuration = kubernetes.client.Configuration()

    for key, value in iteritems(auth):
        if key in AUTH_ARG_MAP.keys() and value is not None:
            if key == "api_key":
                setattr(
                    configuration, key, {"authorization": "Bearer {0}".format(value)}
                )
            elif key == "proxy_headers":
                headers = urllib3.util.make_headers(**value)
                setattr(configuration, key, headers)
            else:
                setattr(configuration, key, value)

    return configuration


def _create_headers(module=None, **kwargs):
    header_map = {
        "impersonate_user": "Impersonate-User",
        "impersonate_groups": "Impersonate-Group",
    }

    headers = {}
    for arg_name, header_name in header_map.items():
        value = None
        if module and module.params.get(arg_name) is not None:
            value = module.params.get(arg_name)
        elif arg_name in kwargs and kwargs.get(arg_name) is not None:
            value = kwargs.get(arg_name)
        else:
            value = os.getenv("K8S_AUTH_{0}".format(arg_name.upper()), None)
            if value is not None:
                if AUTH_ARG_SPEC[arg_name].get("type") == "list":
                    value = [x for x in value.split(",") if x != ""]
        if value:
            headers[header_name] = value
    return headers


def _configuration_digest(configuration, **kwargs) -> str:
    m = hashlib.sha256()
    for k in AUTH_ARG_MAP:
        if not hasattr(configuration, k):
            v = None
        else:
            v = getattr(configuration, k)
        if v and k in ["ssl_ca_cert", "cert_file", "key_file"]:
            with open(str(v), "r") as fd:
                content = fd.read()
                m.update(content.encode())
        else:
            m.update(str(v).encode())
    for k, v in kwargs.items():
        content = "{0}: {1}".format(k, v)
        m.update(content.encode())
    digest = m.hexdigest()

    return digest


def _set_header(client, header, value):
    if isinstance(value, list):
        for v in value:
            client.set_default_header(header_name=unique_string(header), header_value=v)
    else:
        client.set_default_header(header_name=header, header_value=value)


def cache(func):
    def wrapper(*args, **kwargs):
        client = None
        hashable_kwargs = {}
        for k, v in kwargs.items():
            if isinstance(v, list):
                hashable_kwargs[k] = ",".join(sorted(v))
            else:
                hashable_kwargs[k] = v
        digest = _configuration_digest(*args, **hashable_kwargs)
        if digest in _pool:
            client = _pool[digest]
        else:
            client = func(*args, **kwargs)
            _pool[digest] = client

        return client

    return wrapper


@cache
def create_api_client(configuration, **headers):
    client = kubernetes.client.ApiClient(configuration)
    for header, value in headers.items():
        _set_header(client, header, value)
    return k8sdynamicclient.K8SDynamicClient(client, discoverer=LazyDiscoverer)


class K8SClient:
    """A Client class for K8S modules.

    This class has the primary purpose to proxy the kubernetes client and resource objects.
    If there is a need for other methods or attributes to be proxied, they can be added here.
    """

    K8S_SERVER_DRY_RUN = "All"

    def __init__(self, configuration, client, dry_run: bool = False) -> None:
        self.configuration = configuration
        self.client = client
        self.dry_run = dry_run

    @property
    def resources(self) -> List[Any]:
        return self.client.resources

    def _find_resource_with_prefix(
        self, prefix: str, kind: str, api_version: str
    ) -> Resource:
        for attribute in ["kind", "name", "singular_name"]:
            try:
                return self.client.resources.get(
                    **{"prefix": prefix, "api_version": api_version, attribute: kind}
                )
            except (ResourceNotFoundError, ResourceNotUniqueError):
                pass
        return self.client.resources.get(
            prefix=prefix, api_version=api_version, short_names=[kind]
        )

    def resource(self, kind: str, api_version: str) -> Resource:
        """Fetch a kubernetes client resource.

        This will attempt to find a kubernetes resource trying, in order, kind,
        name, singular_name and short_names.
        """
        try:
            if api_version == "v1":
                return self._find_resource_with_prefix("api", kind, api_version)
        except ResourceNotFoundError:
            pass
        return self._find_resource_with_prefix(None, kind, api_version)

    def _ensure_dry_run(self, params: Dict) -> Dict:
        if self.dry_run:
            params["dry_run"] = self.K8S_SERVER_DRY_RUN
        return params

    def validate(
        self, resource, version: Optional[str] = None, strict: Optional[bool] = False
    ):
        return self.client.validate(resource, version, strict)

    def get(self, resource, **params):
        return resource.get(**params)

    def delete(self, resource, **params):
        return resource.delete(**self._ensure_dry_run(params))

    def apply(self, resource, definition, namespace, **params):
        return resource.apply(
            definition, namespace=namespace, **self._ensure_dry_run(params)
        )

    def create(self, resource, definition, **params):
        return resource.create(definition, **self._ensure_dry_run(params))

    def replace(self, resource, definition, **params):
        return resource.replace(definition, **self._ensure_dry_run(params))

    def patch(self, resource, definition, **params):
        return resource.patch(definition, **self._ensure_dry_run(params))


def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient:
    auth_spec = _create_auth_spec(module, **kwargs)
    if module:
        requires = module.requires
    else:
        requires = _requires
    if isinstance(auth_spec.get("kubeconfig"), dict):
        requires("kubernetes", "17.17.0", "to use in-memory config")
    if auth_spec.get("no_proxy"):
        requires("kubernetes", "19.15.0", "to use the no_proxy feature")

    try:
        configuration = _create_configuration(auth_spec)
        headers = _create_headers(module, **kwargs)
        client = create_api_client(configuration, **headers)
    except kubernetes.config.ConfigException as e:
        msg = "Could not create API client: {0}".format(e)
        raise CoreException(msg) from e

    dry_run = False
    if module and module.server_side_dry_run:
        dry_run = True

    k8s_client = K8SClient(
        configuration=configuration,
        client=client,
        dry_run=dry_run,
    )

    return k8s_client
