# Copyright 2016-2018, Pulumi Corporation.
#
# Licensed 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.

"""
The config module contains all configuration management functionality.
"""
import json
from typing import Any, Callable, Optional, overload

from . import errors
from .metadata import get_project
from .output import Output
from .runtime.config import get_config


class Config:
    """
    Config is a bag of related configuration state.  Each bag contains any number of configuration variables, indexed by
    simple keys, and each has a name that uniquely identifies it; two bags with different names do not share values for
    variables that otherwise share the same key.  For example, a bag whose name is `pulumi:foo`, with keys `a`, `b`,
    and `c`, is entirely separate from a bag whose name is `pulumi:bar` with the same simple key names.  Each key has a
    fully qualified names, such as `pulumi:foo:a`, ..., and `pulumi:bar:a`, respectively.
    """

    name: str
    """
    The configuration bag's logical name that uniquely identifies it.  The default is the name of the current project.
    """

    def __init__(self, name: Optional[str] = None) -> None:
        """
        :param str name: The configuration bag's logical name that uniquely identifies it.  If not provided, the name
               of the current project is used.
        """
        if not name:
            name = get_project()
        if not isinstance(name, str):
            raise TypeError("Expected name to be a string")
        self.name = name

    # pylint: disable=unused-argument
    def _get(
        self,
        key: str,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Optional[str]:
        full_key = self.full_key(key)
        # TODO[pulumi/pulumi#7127]: Re-enabled the warning.
        # if use is not None and is_config_secret(full_key):
        #     assert instead_of is not None
        #     log.warn(
        #         f"Configuration '{full_key}' value is a secret; " +
        #         f"use `{use.__name__}` instead of `{instead_of.__name__}`")
        return get_config(full_key)

    @overload
    def get(self, key: str, default: str) -> str: ...
    @overload
    def get(self, key: str) -> Optional[str]: ...
    @overload
    def get(self, key: str, default: Optional[str] = None) -> Optional[str]: ...
    def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
        """
        Returns an optional configuration value by its key,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.

        :param str key: The requested configuration key.
        :param Optional[str] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[str]
        """
        config_candidate = self._get(key, self.get_secret, self.get)
        return config_candidate if config_candidate is not None else default

    @overload
    def get_secret(self, key: str, default: str) -> Output[str]: ...
    @overload
    def get_secret(self, key: str) -> Optional[Output[str]]: ...
    @overload
    def get_secret(
        self, key: str, default: Optional[str] = None
    ) -> Optional[Output[str]]: ...
    def get_secret(
        self, key: str, default: Optional[str] = None
    ) -> Optional[Output[str]]:
        """
        Returns an optional configuration value by its key, marked as a secret,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.

        :param str key: The requested configuration key.
        :param Optional[str] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[str]
        """
        config_candidate = self._get(key)
        v = config_candidate if config_candidate is not None else default
        if v is None:
            return None
        return Output.secret(v)

    def _get_bool(
        self,
        key: str,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Optional[bool]:
        v = self._get(key, use, instead_of)
        if v is None:
            return None
        if v in ["true", "True"]:
            return True
        if v in ["false", "False"]:
            return False
        raise ConfigTypeError(self.full_key(key), v, "bool")

    @overload
    def get_bool(self, key: str, default: bool) -> bool: ...
    @overload
    def get_bool(self, key: str) -> Optional[bool]: ...
    @overload
    def get_bool(self, key: str, default: Optional[bool] = None) -> Optional[bool]: ...
    def get_bool(self, key: str, default: Optional[bool] = None) -> Optional[bool]:
        """
        Returns an optional configuration value, as a bool, by its key,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal boolean, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[bool] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[bool]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to bool.
        """
        config_candidate = self._get_bool(key, self.get_secret_bool, self.get_bool)
        return config_candidate if config_candidate is not None else default

    @overload
    def get_secret_bool(self, key: str, default: bool) -> Output[bool]: ...
    @overload
    def get_secret_bool(self, key: str) -> Optional[Output[bool]]: ...
    @overload
    def get_secret_bool(
        self, key: str, default: Optional[bool] = None
    ) -> Optional[Output[bool]]: ...
    def get_secret_bool(
        self, key: str, default: Optional[bool] = None
    ) -> Optional[Output[bool]]:
        """
        Returns an optional configuration value, as a bool, by its key, marked as a secret,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal boolean, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[bool] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[bool]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to bool.
        """
        config_candidate = self._get_bool(key)
        v = config_candidate if config_candidate is not None else default
        if v is None:
            return None
        return Output.secret(v)

    def _get_int(
        self,
        key: str,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Optional[int]:
        v = self._get(key, use, instead_of)
        if v is None:
            return None
        try:
            return int(v)
        except Exception as e:
            raise ConfigTypeError(self.full_key(key), v, "int") from e

    @overload
    def get_int(self, key: str, default: int) -> int: ...
    @overload
    def get_int(self, key: str) -> Optional[int]: ...
    @overload
    def get_int(self, key: str, default: Optional[int] = None) -> Optional[int]: ...
    def get_int(self, key: str, default: Optional[int] = None) -> Optional[int]:
        """
        Returns an optional configuration value, as an int, by its key,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal int, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[int] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[int]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to int.
        """
        config_candidate = self._get_int(key, self.get_secret_int, self.get_int)
        return config_candidate if config_candidate is not None else default

    @overload
    def get_secret_int(self, key: str, default: int) -> Output[int]: ...
    @overload
    def get_secret_int(self, key: str) -> Optional[Output[int]]: ...
    @overload
    def get_secret_int(
        self, key: str, default: Optional[int] = None
    ) -> Optional[Output[int]]: ...
    def get_secret_int(
        self, key: str, default: Optional[int] = None
    ) -> Optional[Output[int]]:
        """
        Returns an optional configuration value, as an int, by its key, marked as a secret,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal int, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[int] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[int]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to int.
        """
        config_candidate = self._get_int(key)
        v = config_candidate if config_candidate is not None else default
        if v is None:
            return None
        return Output.secret(v)

    def _get_float(
        self,
        key: str,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Optional[float]:
        v = self._get(key, use, instead_of)
        if v is None:
            return None
        try:
            return float(v)
        except Exception as e:
            raise ConfigTypeError(self.full_key(key), v, "float") from e

    @overload
    def get_float(self, key: str, default: float) -> float: ...
    @overload
    def get_float(self, key: str) -> Optional[float]: ...
    @overload
    def get_float(
        self, key: str, default: Optional[float] = None
    ) -> Optional[float]: ...
    def get_float(self, key: str, default: Optional[float] = None) -> Optional[float]:
        """
        Returns an optional configuration value, as a float, by its key, marked as a secret,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal float, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[float] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[float]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        config_candidate = self._get_float(key, self.get_secret_float, self.get_float)
        return config_candidate if config_candidate is not None else default

    @overload
    def get_secret_float(self, key: str, default: float) -> Output[float]: ...
    @overload
    def get_secret_float(self, key: str) -> Optional[Output[float]]: ...
    @overload
    def get_secret_float(
        self, key: str, default: Optional[float] = None
    ) -> Optional[Output[float]]: ...
    def get_secret_float(
        self, key: str, default: Optional[float] = None
    ) -> Optional[Output[float]]:
        """
        Returns an optional configuration value, as a float, by its key, marked as a secret,
        a default value if that key is unset and a default is provided,
        or None if it doesn't exist.
        If the configuration value isn't a legal float, this function will throw an error.

        :param str key: The requested configuration key.
        :param Optional[float] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[float]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        config_candidate = self._get_float(key)
        v = config_candidate if config_candidate is not None else default
        if v is None:
            return None
        return Output.secret(v)

    def _get_object(
        self,
        key: str,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Optional[Any]:
        v = self._get(key, use, instead_of)
        if v is None:
            return None
        try:
            return json.loads(v)
        except Exception as e:
            raise ConfigTypeError(self.full_key(key), v, "JSON object") from e

    @overload
    def get_object(self, key: str, default: Any) -> Any: ...
    @overload
    def get_object(self, key: str) -> Optional[Any]: ...
    @overload
    def get_object(self, key: str, default: Optional[Any] = None) -> Optional[Any]: ...
    def get_object(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
        """
        Returns an optional configuration value, as an object, by its key,
        a default value if that key is unset and a default is provided, or undefined if it
        doesn't exist. This routine simply JSON parses and doesn't validate the shape of the
        contents.

        :param str key: The requested configuration key.
        :param Optional[Any] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[Any]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        config_candidate = self._get_object(
            key, self.get_secret_object, self.get_object
        )
        return config_candidate if config_candidate is not None else default

    @overload
    def get_secret_object(self, key: str, default: Any) -> Output[Any]: ...
    @overload
    def get_secret_object(self, key: str) -> Optional[Output[Any]]: ...
    @overload
    def get_secret_object(
        self, key: str, default: Optional[Any] = None
    ) -> Optional[Output[Any]]: ...
    def get_secret_object(
        self, key: str, default: Optional[Any] = None
    ) -> Optional[Output[Any]]:
        """
        Returns an optional configuration value, as an object, by its key, marking it as a secret,
        a default value if that key is unset and a default is provided,
        or undefined if it doesn't exist. This routine simply JSON parses and doesn't validate the
        shape of the contents.

        :param str key: The requested configuration key.
        :param Optional[Any] default: An optional fallback value to use if the given configuration key is not set.
        :return: The configuration key's value, or None if one does not exist.
        :rtype: Optional[Any]
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        config_candidate = self._get_object(key)
        v = config_candidate if config_candidate is not None else default
        if v is None:
            return None
        return Output.secret(v)

    def _require(
        self,
        key: str,
        secret: bool,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> str:
        v = self._get(key, use, instead_of)
        if v is None:
            raise ConfigMissingError(self.full_key(key), secret)
        return v

    def require(self, key: str) -> str:
        """
        Returns a configuration value by its given key.  If it doesn't exist, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: str
        :raises ConfigMissingError: The configuration value did not exist.
        """
        return self._require(key, False, self.require_secret, self.require)

    def require_secret(self, key: str) -> Output[str]:
        """
        Returns a configuration value, marked as a secret by its given key.  If it doesn't exist, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: str
        :raises ConfigMissingError: The configuration value did not exist.
        """
        return Output.secret(self._require(key, True))

    def _require_bool(
        self,
        key: str,
        secret: bool,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> bool:
        v = self._get_bool(key, use, instead_of)
        if v is None:
            raise ConfigMissingError(self.full_key(key), secret)
        return v

    def require_bool(self, key: str) -> bool:
        """
        Returns a configuration value, as a bool, by its given key.  If it doesn't exist, or the
        configuration value is not a legal bool, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: bool
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to bool.
        """
        return self._require_bool(
            key, False, self.require_secret_bool, self.require_bool
        )

    def require_secret_bool(self, key: str) -> Output[bool]:
        """
        Returns a configuration value, as a bool, marked as a secret by its given key.  If it doesn't exist, or the
        configuration value is not a legal bool, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: bool
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to bool.
        """
        return Output.secret(self._require_bool(key, True))

    def _require_int(
        self,
        key: str,
        secret: bool,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> int:
        v = self._get_int(key, use, instead_of)
        if v is None:
            raise ConfigMissingError(self.full_key(key), secret)
        return v

    def require_int(self, key: str) -> int:
        """
        Returns a configuration value, as an int, by its given key.  If it doesn't exist, or the
        configuration value is not a legal int, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: int
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to int.
        """
        return self._require_int(key, False, self.require_secret_int, self.require_int)

    def require_secret_int(self, key: str) -> Output[int]:
        """
        Returns a configuration value, as an int, marked as a secret by its given key.  If it doesn't exist, or the
        configuration value is not a legal int, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: int
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to int.
        """
        return Output.secret(self._require_int(key, True))

    def _require_float(
        self,
        key: str,
        secret: bool,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> float:
        v = self._get_float(key, use, instead_of)
        if v is None:
            raise ConfigMissingError(self.full_key(key), secret)
        return v

    def require_float(self, key: str) -> float:
        """
        Returns a configuration value, as a float, by its given key.  If it doesn't exist, or the
        configuration value is not a legal number, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: float
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        return self._require_float(
            key, False, self.require_secret_float, self.require_float
        )

    def require_secret_float(self, key: str) -> Output[float]:
        """
        Returns a configuration value, as a float, marked as a secret by its given key.  If it doesn't exist, or the
        configuration value is not a legal number, an error is thrown.

        :param str key: The requested configuration key.
        :return: The configuration key's value.
        :rtype: float
        :raises ConfigMissingError: The configuration value did not exist.
        :raises ConfigTypeError: The configuration value existed but couldn't be coerced to float.
        """
        return Output.secret(self._require_float(key, True))

    def _require_object(
        self,
        key: str,
        secret: bool,
        use: Optional[Callable] = None,
        instead_of: Optional[Callable] = None,
    ) -> Any:
        v = self._get_object(key, use, instead_of)
        if v is None:
            raise ConfigMissingError(self.full_key(key), secret)
        return v

    def require_object(self, key: str) -> Any:
        """
        Returns a configuration value as a JSON string and deserializes the JSON into a Python
        object. If it doesn't exist, or the configuration value is not a legal JSON string, an error
        is thrown.
        """
        return self._require_object(
            key, False, self.require_secret_object, self.require_object
        )

    def require_secret_object(self, key: str) -> Output[Any]:
        """
        Returns a configuration value as a JSON string and deserializes the JSON into a Python
        object, marking it as a secret. If it doesn't exist, or the configuration value is not a
        legal JSON string, an error is thrown.
        """
        return Output.secret(self._require_object(key, True))

    def full_key(self, key: str) -> str:
        """
        Turns a simple configuration key into a fully resolved one, by prepending the bag's name.

        :param str key: The name of the configuration key.
        :return: The name of the configuration key, prefixed with the bag's name.
        :rtype: str
        """
        return f"{self.name}:{key}"


class ConfigTypeError(errors.RunError):
    """
    Indicates a configuration value is of the wrong type.
    """

    key: str
    """
    The name of the key whose value was ill-typed.
    """

    value: str
    """
    The ill-typed value.
    """

    expect_type: str
    """
    The expected type of this value.
    """

    def __init__(self, key: str, value: str, expect_type: str) -> None:
        self.key = key
        self.value = value
        self.expect_type = expect_type
        super().__init__(
            f"Configuration '{key}' value '{value}' is not a valid '{expect_type}'"
        )


class ConfigMissingError(errors.RunError):
    """
    Indicates a configuration value is missing.
    """

    key: str
    """
    The name of the missing configuration key.
    """

    secret: bool
    """
    If this is a secret configuration key.
    """

    def __init__(self, key: str, secret: bool) -> None:
        self.key = key
        self.secret = secret
        super().__init__(
            f"Missing required configuration variable '{key}'\n"
            + f"\tplease set a value using the command `pulumi config set{' --secret ' if secret else ' '}{key} <value>`"
        )
