"""Decrypt objects in Config."""

import logging
from base64 import b64decode

import boto3
from botocore.exceptions import ClientError, NoRegionError

LOG = logging.getLogger(__name__)

GPG_IMPORTED = False
try:
    import gnupg

    GPG_IMPORTED = True
except ImportError:
    LOG.exception("Could not load gnupg. Will be unable to unpack secrets.")


def gpg_decrypt(cfg, gpg_config=None):
    """Decrypt GPG objects in configuration.

    Args:
        cfg (dict): configuration dictionary
        gpg_config (optional[dict]): gpg configuration
            dict of arguments for gpg including:
                homedir, binary, and keyring
            example:
                gpg_config = {'homedir': '~/.gnupg/',
                              'binary': 'gpg',
                              'keyring': 'pubring.kbx'}

    Returns:
        dict: decrypted configuration dictionary

    The aim is to find in the dictionary items which have been encrypted
    with gpg, then decrypt them if possible.

    We will either detect the encryption based on the PGP block text or a
    user can create a key "_gpg" in which to store the data. Either case
    will work. In the case of the "_gpg" key all data at this level will
    be replaced with the decrypted contents. For example:

        {'component': {'key': 'PGP Block ...'}}

    will transform to:

        {'component': {'key': 'decrypted value'}}

    However:

        {'component': {'key': {'_gpg': 'PGP Block ...', 'nothing': 'should go here'}}}

    will transform to:

        {'component': {'key': 'decrypted value'}}
    """

    def decrypt(obj):
        """Decrypt the object.

        It is an inner function because we must first verify that gpg
        is ready. If we did them in the same function we would end up
        calling the gpg checks several times, potentially, since we are
        calling this recursively.
        """
        if isinstance(obj, list):
            return [decrypt(item) for item in obj]
        elif isinstance(obj, dict):
            if "_gpg" in obj:
                try:
                    decrypted = gpg.decrypt(obj["_gpg"])
                    if decrypted.ok:
                        obj = decrypted.data.decode("utf-8")
                    else:
                        LOG.error("gpg error unpacking secrets %s", decrypted.stderr)
                except Exception as err:
                    LOG.error("error unpacking secrets %s", err)
            else:
                for k, v in obj.items():
                    obj[k] = decrypt(v)
        else:
            try:
                if "BEGIN PGP" in obj:
                    try:
                        decrypted = gpg.decrypt(obj)
                        if decrypted.ok:
                            obj = decrypted.data.decode("utf-8")
                        else:
                            LOG.error("gpg error unpacking secrets %s", decrypted.stderr)
                    except Exception as err:
                        LOG.error("error unpacking secrets %s", err)
            except TypeError:
                LOG.debug("Pass on decryption. Only decrypt strings")
        return obj

    gpg_config = gpg_config if gpg_config is not None else {}
    _key_map = {"homedir": "gnupghome", "binary": "gpgbinary"}
    gpg_config = {_key_map.get(k, k): v for k, v in gpg_config.items()}
    if GPG_IMPORTED:
        try:
            gpg = gnupg.GPG(**gpg_config)
        except (OSError, RuntimeError):
            LOG.exception("Failed to configure gpg. Will be unable to decrypt secrets.")
        return decrypt(cfg)
    return cfg


def kms_decrypt(cfg, aws_config=None):
    """Decrypt KMS objects in configuration.

    Args:
        cfg (dict): configuration dictionary
        aws_config (dict): aws credentials
            dict of arguments passed into boto3 session
            example:
                aws_creds = {'aws_access_key_id': aws_access_key_id,
                             'aws_secret_access_key': aws_secret_access_key,
                             'region_name': 'us-east-1'}

    Returns:
        dict: decrypted configuration dictionary

    AWS credentials follow the standard boto flow. Provided values first,
    followed by environment, and then configuration files on the machine.
    Ideally, one would set up an IAM role for this machine to authenticate.

    The aim is to find in the dictionary items which have been encrypted
    with KMS, then decrypt them if possible.

    A user can create a key "_kms" in which to store the data. All data
    at this level will be replaced with the decrypted contents. For example:

        {'component': {'key': {'_kms': 'encrypted cipher text', 'nothing': 'should go here'}}}

    will transform to:

        {'component': {'key': 'decrypted value'}}

    To get the value to be stored as a KMS encrypted string:

        from figgypy.util import kms_encrypt
        encrypted = kms_encrypt('your secret', 'your key or alias', optional_aws_config)
    """

    def decrypt(obj):
        """Decrypt the object.

        It is an inner function because we must first configure our KMS
        client. Then we call this recursively on the object.
        """
        if isinstance(obj, list):
            return [decrypt(item) for item in obj]
        elif isinstance(obj, dict):
            if "_kms" in obj:
                try:
                    res = client.decrypt(CiphertextBlob=b64decode(obj["_kms"]))
                    obj = res["Plaintext"].decode("utf-8")
                except ClientError as err:
                    if "AccessDeniedException" in err.args[0]:
                        LOG.warning(
                            "Unable to decrypt %s. Key does not exist or no access",
                            obj["_kms"],
                        )
                    else:
                        raise
            else:
                for k, v in obj.items():
                    obj[k] = decrypt(v)
        return obj

    aws_config = aws_config if aws_config is not None else {}
    try:
        aws = boto3.session.Session(**aws_config)
        client = aws.client("kms")
    except NoRegionError:
        LOG.exception(
            "Missing or invalid aws configuration. Will not be able to unpack KMS secrets."
        )
        return cfg
    return decrypt(cfg)


def ssm_decrypt(cfg, aws_config=None):
    """Decrypt/get ssm parameter store objects in configuration.
    Args:
        cfg (dict): configuration dictionary
        aws_config (dict): aws credentials
            dict of arguments passed into boto3 session
            example:
                aws_creds = {'aws_access_key_id': aws_access_key_id,
                             'aws_secret_access_key': aws_secret_access_key,
                             'region_name': 'us-east-1'}

    Returns:
        dict: decrypted configuration dictionary

    AWS credentials follow the standard boto flow. Provided values first,
    followed by environment, and then configuration files on the machine.
    Ideally, one would set up an IAM role for this machine to authenticate.

    The aim is to find in the dictionary items which have been encrypted/stored
    with the ssm parameter store.

    A user can create a key "_ssm" in which to store the data. All data
    at this level will be treated as a parameter name and retrieved from ssm
    parameter store. For example:

        {'component': {'key': {'_ssm': 'name of parameter to get', 'nothing': 'should go here'}}}

    will transform to:

        {'component': {'key': 'retrieved/decrypted value'}}


    """

    def decrypt(obj):
        """Decrypt/get the object.

        It is an inner function because we must first configure our ssm
        client. Then we call this recursively on the object.
        """
        if isinstance(obj, list):
            return [decrypt(item) for item in obj]
        elif isinstance(obj, dict):
            if "_ssm" in obj:
                try:
                    res = client.get_parameter(Name=obj["_ssm"], WithDecryption=True)
                    obj = res["Parameter"]["Value"]
                except ClientError as err:
                    if "AccessDeniedException" in err.args[0]:
                        LOG.warning(
                            "Unable to decrypt %s. Parameter does not exist or no access",
                            obj["_ssm"],
                        )
                    else:
                        raise
            else:
                for k, v in obj.items():
                    obj[k] = decrypt(v)
        return obj

    aws_config = aws_config if aws_config is not None else {}
    try:
        aws = boto3.session.Session(**aws_config)
        client = aws.client("ssm")
    except NoRegionError:
        LOG.info("Missing or invalid aws configuration. Will not be able to unpack SSM secrets.")
        return cfg
    return decrypt(cfg)
