# 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.

import base64

from barbican.common import config
from barbican.common import utils
from barbican.model import models
from barbican.model import repositories
from barbican.plugin.crypto import base
from barbican.plugin.crypto import manager
from barbican.plugin.interface import secret_store as sstore

CONF = config.new_config()
config.parse_args(CONF)


class StoreCryptoContext(object):
    """Context for crypto-adapter secret store plugins.

    This context object allows access to core Barbican resources such as
    datastore models.
    """
    def __init__(
            self,
            project_model,
            secret_model=None,
            private_secret_model=None,
            public_secret_model=None,
            passphrase_secret_model=None,
            content_type=None):
        self.secret_model = secret_model
        self.private_secret_model = private_secret_model
        self.public_secret_model = public_secret_model
        self.passphrase_secret_model = passphrase_secret_model
        self.project_model = project_model
        self.content_type = content_type


class StoreCryptoAdapterPlugin(object):
    """Secret store plugin adapting to 'crypto' devices as backend.

    HSM-style 'crypto' devices perform encryption/decryption processing but
    do not actually store the encrypted information, unlike other 'secret
    store' plugins that do provide storage. Hence, this adapter bridges
    between these two plugin styles, providing Barbican persistence services
    as needed to store information.

    Note that this class does not inherit from SecretStoreBase, as it also
    requires access to lower-level datastore entities such as KEKDatum. This
    additional information is passed in via the 'context' parameter.
    """

    def __init__(self):
        super(StoreCryptoAdapterPlugin, self).__init__()

    def store_secret(self, secret_dto, context):
        """Store a secret.

        :param secret_dto: SecretDTO for secret
        :param context: StoreCryptoContext for secret
        :returns: an optional dictionary containing metadata about the secret
        """

        # Find HSM-style 'crypto' plugin.
        encrypting_plugin = manager.get_manager().get_plugin_store_generate(
            base.PluginSupportTypes.ENCRYPT_DECRYPT,
            project_id=context.project_model.id
        )

        # Find or create a key encryption key metadata.
        kek_datum_model, kek_meta_dto = _find_or_create_kek_objects(
            encrypting_plugin, context.project_model)

        # Secrets are base64 encoded before being passed to the secret stores.
        secret_bytes = base64.b64decode(secret_dto.secret)

        encrypt_dto = base.EncryptDTO(secret_bytes)

        # Enhance the context with content_type, This is needed to build
        # datum_model to store
        if not context.content_type:
            context.content_type = secret_dto.content_type

        # Create an encrypted datum instance and add the encrypted cyphertext.
        response_dto = encrypting_plugin.encrypt(
            encrypt_dto, kek_meta_dto, context.project_model.external_id
        )

        # Convert binary data into a text-based format.
        _store_secret_and_datum(
            context, context.secret_model, kek_datum_model, response_dto)

        return None

    def get_secret(self, secret_type, metadata, context):
        """Retrieve a secret.

        :param secret_type: secret type
        :param metadata: secret metadata
        :param context: StoreCryptoContext for secret
        :returns: SecretDTO that contains secret
        """
        if (not context.secret_model or
                not context.secret_model.encrypted_data):
            raise sstore.SecretNotFoundException()

        # TODO(john-wood-w) Need to revisit 1 to many datum relationship.
        datum_model = context.secret_model.encrypted_data[0]

        # Find HSM-style 'crypto' plugin.
        decrypting_plugin = manager.get_manager().get_plugin_retrieve(
            datum_model.kek_meta_project.plugin_name)

        # wrap the KEKDatum instance in our DTO
        kek_meta_dto = base.KEKMetaDTO(datum_model.kek_meta_project)

        # Convert from text-based storage format to binary.
        encrypted = base64.b64decode(datum_model.cypher_text)
        decrypt_dto = base.DecryptDTO(encrypted)

        # Decrypt the secret.
        secret = decrypting_plugin.decrypt(decrypt_dto,
                                           kek_meta_dto,
                                           datum_model.kek_meta_extended,
                                           context.project_model.external_id)
        secret = base64.b64encode(secret)
        key_spec = sstore.KeySpec(alg=context.secret_model.algorithm,
                                  bit_length=context.secret_model.bit_length,
                                  mode=context.secret_model.mode)

        return sstore.SecretDTO(secret_type,
                                secret, key_spec,
                                datum_model.content_type)

    def delete_secret(self, secret_metadata):
        """Delete a secret."""
        pass

    def generate_symmetric_key(self, key_spec, context):
        """Generate a symmetric key.

        :param key_spec: KeySpec that contains details on the type of key to
        generate
        :param context: StoreCryptoContext for secret
        :returns: a dictionary that contains metadata about the key
        """

        # Find HSM-style 'crypto' plugin.
        plugin_type = _determine_generation_type(key_spec.alg)
        if base.PluginSupportTypes.SYMMETRIC_KEY_GENERATION != plugin_type:
            raise sstore.SecretAlgorithmNotSupportedException(key_spec.alg)
        generating_plugin = manager.get_manager().get_plugin_store_generate(
            plugin_type,
            key_spec.alg,
            key_spec.bit_length,
            key_spec.mode,
            project_id=context.project_model.id)

        # Find or create a key encryption key metadata.
        kek_datum_model, kek_meta_dto = _find_or_create_kek_objects(
            generating_plugin, context.project_model)

        # Create an encrypted datum instance and add the created cypher text.
        generate_dto = base.GenerateDTO(key_spec.alg,
                                        key_spec.bit_length,
                                        key_spec.mode, None)
        # Create the encrypted meta.
        response_dto = generating_plugin.generate_symmetric(
            generate_dto, kek_meta_dto, context.project_model.external_id)

        # Convert binary data into a text-based format.
        _store_secret_and_datum(
            context, context.secret_model, kek_datum_model, response_dto)

        return None

    def generate_asymmetric_key(self, key_spec, context):
        """Generates an asymmetric key.

        Returns a AsymmetricKeyMetadataDTO object containing
        metadata(s) for asymmetric key components. The metadata
        can be used to retrieve individual components of
        asymmetric key pair.
        """

        plugin_type = _determine_generation_type(key_spec.alg)
        if base.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION != plugin_type:
            raise sstore.SecretAlgorithmNotSupportedException(key_spec.alg)

        generating_plugin = manager.get_manager().get_plugin_store_generate(
            plugin_type, key_spec.alg, key_spec.bit_length,
            project_id=context.project_model.id)

        # Find or create a key encryption key metadata.
        kek_datum_model, kek_meta_dto = _find_or_create_kek_objects(
            generating_plugin, context.project_model)

        generate_dto = base.GenerateDTO(key_spec.alg,
                                        key_spec.bit_length,
                                        None, key_spec.passphrase)

        # Create the encrypted meta.
        private_key_dto, public_key_dto, passwd_dto = (
            generating_plugin.generate_asymmetric(
                generate_dto, kek_meta_dto, context.project_model.external_id
            )
        )

        _store_secret_and_datum(
            context,
            context.private_secret_model,
            kek_datum_model,
            private_key_dto)

        _store_secret_and_datum(
            context,
            context.public_secret_model,
            kek_datum_model,
            public_key_dto)

        if key_spec.passphrase and passwd_dto:
            _store_secret_and_datum(
                context,
                context.passphrase_secret_model,
                kek_datum_model,
                passwd_dto)

        return sstore.AsymmetricKeyMetadataDTO()

    def generate_supports(self, key_spec):
        """Key generation supported?

        Specifies whether the plugin supports key generation with the
        given key_spec.
        """
        return (key_spec and
                (key_spec.alg.lower() in
                 sstore.KeyAlgorithm.ASYMMETRIC_ALGORITHMS
                 or key_spec.alg.lower() in
                 sstore.KeyAlgorithm.SYMMETRIC_ALGORITHMS))

    def store_secret_supports(self, key_spec):
        """Key storage supported?

        Specifies whether the plugin supports storage of the secret given
        the attributes included in the KeySpec
        """
        return True


def _determine_generation_type(algorithm):
    """Determines the type based on algorithm."""
    if not algorithm:
        raise sstore.SecretAlgorithmNotSupportedException(algorithm)

    symmetric_algs = base.PluginSupportTypes.SYMMETRIC_ALGORITHMS
    asymmetric_algs = base.PluginSupportTypes.ASYMMETRIC_ALGORITHMS
    if algorithm.lower() in symmetric_algs:
        return base.PluginSupportTypes.SYMMETRIC_KEY_GENERATION
    elif algorithm.lower() in asymmetric_algs:
        return base.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION
    else:
        raise sstore.SecretAlgorithmNotSupportedException(algorithm)


def _find_or_create_kek_objects(plugin_inst, project_model):
    kek_repo = repositories.get_kek_datum_repository()

    # Find or create a key encryption key.
    full_plugin_name = utils.generate_fullname_for(plugin_inst)
    kek_datum_model = kek_repo.find_or_create_kek_datum(project_model,
                                                        full_plugin_name)

    # Bind to the plugin's key management.
    # TODO(jwood): Does this need to be in a critical section? Should the
    # bind operation just be declared idempotent in the plugin contract?
    kek_meta_dto = base.KEKMetaDTO(kek_datum_model)
    if not kek_datum_model.bind_completed:
        kek_meta_dto = plugin_inst.bind_kek_metadata(kek_meta_dto)

        # By contract, enforce that plugins return a
        # (typically modified) DTO.
        if kek_meta_dto is None:
            raise base.CryptoKEKBindingException(full_plugin_name)

        _indicate_bind_completed(kek_meta_dto, kek_datum_model)
        kek_repo.save(kek_datum_model)

    return kek_datum_model, kek_meta_dto


def _store_secret_and_datum(
        context, secret_model, kek_datum_model, generated_dto):

    # Create Secret entities in data store.
    if not secret_model.id:
        secret_model.project_id = context.project_model.id
        repositories.get_secret_repository().create_from(secret_model)

    # setup and store encrypted datum
    datum_model = models.EncryptedDatum(secret_model, kek_datum_model)
    datum_model.content_type = context.content_type
    datum_model.cypher_text = base64.b64encode(generated_dto.cypher_text)
    datum_model.kek_meta_extended = generated_dto.kek_meta_extended
    repositories.get_encrypted_datum_repository().create_from(
        datum_model)


def _indicate_bind_completed(kek_meta_dto, kek_datum):
    """Updates the supplied kek_datum instance

    Updates the kek_datum per the contents of the supplied
    kek_meta_dto instance. This function is typically used once plugins
    have had a chance to bind kek_meta_dto to their crypto systems.

    :param kek_meta_dto:
    :param kek_datum:
    :return: None

    """
    kek_datum.bind_completed = True
    kek_datum.algorithm = kek_meta_dto.algorithm
    kek_datum.bit_length = kek_meta_dto.bit_length
    kek_datum.mode = kek_meta_dto.mode
    kek_datum.plugin_meta = kek_meta_dto.plugin_meta
