# Copyright (c) 2014 Rackspace, Inc.
#
# 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 functools
import logging
import six

from oslo_utils.timeutils import parse_isotime

from barbicanclient import base
from barbicanclient import formatter
from barbicanclient import secrets as secret_manager


LOG = logging.getLogger(__name__)


def _immutable_after_save(func):
    @functools.wraps(func)
    def wrapper(self, *args):
        if hasattr(self, '_container_ref') and self._container_ref:
            raise base.ImmutableException()
        return func(self, *args)
    return wrapper


class ContainerFormatter(formatter.EntityFormatter):

    columns = ("Container href",
               "Name",
               "Created",
               "Status",
               "Type",
               "Secrets",
               "Consumers",
               )

    def _get_formatted_data(self):
        formatted_secrets = None
        formatted_consumers = None
        if self.secrets:
            formatted_secrets = '\n'.join((
                '='.join((name, secret_ref))
                for name, secret_ref in six.iteritems(self.secret_refs)
            ))
        if self.consumers:
            formatted_consumers = '\n'.join((str(c) for c in self.consumers))
        data = (self.container_ref,
                self.name,
                self.created,
                self.status,
                self._type,
                formatted_secrets,
                formatted_consumers,
                )
        return data


class Container(ContainerFormatter):
    """Container is a generic grouping of Secrets"""
    _entity = 'containers'
    _type = 'generic'

    def __init__(self, api, name=None, secrets=None, consumers=None,
                 container_ref=None, created=None, updated=None, status=None,
                 secret_refs=None):
        self._api = api
        self._secret_manager = secret_manager.SecretManager(api)
        self._name = name
        self._container_ref = container_ref
        self._secret_refs = secret_refs
        self._cached_secrets = dict()
        self._initialize_secrets(secrets)
        if container_ref:
            self._consumers = consumers if consumers else list()
            self._created = parse_isotime(created) if created else None
            self._updated = parse_isotime(updated) if updated else None
            self._status = status
        else:
            self._consumers = list()
            self._created = None
            self._updated = None
            self._status = None

    def _initialize_secrets(self, secrets):
        try:
            self._fill_secrets_from_secret_refs()
        except Exception:
            raise ValueError("One or more of the provided secret_refs could "
                             "not be retrieved!")
        if secrets:
            try:
                for name, secret in six.iteritems(secrets):
                    self.add(name, secret)
            except Exception:
                raise ValueError("One or more of the provided secrets are not "
                                 "valid Secret objects!")

    def _fill_secrets_from_secret_refs(self):
        if self._secret_refs:
            self._cached_secrets = dict(
                (name.lower(), self._secret_manager.get(secret_ref=secret_ref))
                for name, secret_ref in six.iteritems(self._secret_refs)
            )

    @property
    def container_ref(self):
        return self._container_ref

    @property
    def name(self):
        if self._container_ref and not self._name:
            self._reload()
        return self._name

    @property
    def created(self):
        return self._created

    @property
    def updated(self):
        return self._updated

    @property
    def status(self):
        if self._container_ref and not self._status:
            self._reload()
        return self._status

    @property
    def secret_refs(self):
        if self._cached_secrets:
            self._secret_refs = dict(
                (name, secret.secret_ref)
                for name, secret in six.iteritems(self._cached_secrets)
            )

        return self._secret_refs

    @property
    def secrets(self, cache=True):
        """List of Secrets in Containers"""
        if not self._cached_secrets or not cache:
            self._fill_secrets_from_secret_refs()
        return self._cached_secrets

    @property
    def consumers(self):
        return self._consumers

    @name.setter
    @_immutable_after_save
    def name(self, value):
        self._name = value

    @_immutable_after_save
    def add(self, name, secret):
        if not isinstance(secret, secret_manager.Secret):
            raise ValueError("Must provide a valid Secret object")
        if name.lower() in self.secrets:
            raise KeyError("A secret with this name already exists!")
        self._cached_secrets[name.lower()] = secret

    @_immutable_after_save
    def remove(self, name):
        self._cached_secrets.pop(name.lower(), None)
        if self._secret_refs:
            self._secret_refs.pop(name.lower(), None)

    @_immutable_after_save
    def store(self):
        """Store Container in Barbican"""
        secret_refs = self._get_secrets_and_store_them_if_necessary()

        container_dict = base.filter_null_keys({
            'name': self.name,
            'type': self._type,
            'secret_refs': secret_refs
        })

        LOG.debug("Request body: {0}".format(container_dict))

        # Save, store container_ref and return
        response = self._api.post(self._entity, json=container_dict)
        if response:
            self._container_ref = response['container_ref']
        return self.container_ref

    def delete(self):
        """Delete container from Barbican"""
        if self._container_ref:
            self._api.delete(self._container_ref)
            self._container_ref = None
            self._status = None
            self._created = None
            self._updated = None
        else:
            raise LookupError("Secret is not yet stored.")

    def _get_secrets_and_store_them_if_necessary(self):
        # Save all secrets if they are not yet saved
        LOG.debug("Storing secrets: {0}".format(self.secrets))
        secret_refs = []
        for name, secret in six.iteritems(self.secrets):
            if secret and not secret.secret_ref:
                secret.store()
            secret_refs.append({'name': name, 'secret_ref': secret.secret_ref})
        return secret_refs

    def _reload(self):
        if not self._container_ref:
            raise AttributeError("container_ref not set, cannot reload data.")
        LOG.debug('Getting container - Container href: {0}'
                  .format(self._container_ref))
        base.validate_ref(self._container_ref, 'Container')
        try:
            response = self._api.get(self._container_ref)
        except AttributeError:
            raise LookupError('Container {0} could not be found.'
                              .format(self._container_ref))
        self._name = response.get('name')
        self._consumers = response.get('consumers', [])
        created = response.get('created')
        updated = response.get('updated')
        self._created = parse_isotime(created) if created else None
        self._updated = parse_isotime(updated) if updated else None
        self._status = response.get('status')

    def _get_named_secret(self, name):
        return self.secrets.get(name)

    def __repr__(self):
        return 'Container(name="{0}")'.format(self.name)


class RSAContainerFormatter(formatter.EntityFormatter):
    _get_generic_data = ContainerFormatter._get_formatted_data

    def _get_generic_columns(self):
        return ContainerFormatter.columns

    columns = ("Container href",
               "Name",
               "Created",
               "Status",
               "Type",
               "Public Key",
               "Private Key",
               "PK Passphrase",
               "Consumers",
               )

    def _get_formatted_data(self):
        formatted_public_key = None
        formatted_private_key = None
        formatted_pkp = None
        formatted_consumers = None
        if self.public_key:
            formatted_public_key = self.public_key.secret_ref
        if self.private_key:
            formatted_private_key = self.private_key.secret_ref
        if self.private_key_passphrase:
            formatted_pkp = self.private_key_passphrase.secret_ref
        if self.consumers:
            formatted_consumers = '\n'.join((str(c) for c in self.consumers))
        data = (self.container_ref,
                self.name,
                self.created,
                self.status,
                self._type,
                formatted_public_key,
                formatted_private_key,
                formatted_pkp,
                formatted_consumers,
                )
        return data


class RSAContainer(RSAContainerFormatter, Container):
    _required_secrets = ["public_key", "private_key"]
    _optional_secrets = ["private_key_passphrase"]
    _type = 'rsa'

    def __init__(self, api, name=None, public_key=None, private_key=None,
                 private_key_passphrase=None, consumers=[], container_ref=None,
                 created=None, updated=None, status=None, public_key_ref=None,
                 private_key_ref=None, private_key_passphrase_ref=None):
        secret_refs = {}
        if public_key_ref:
            secret_refs['public_key'] = public_key_ref
        if private_key_ref:
            secret_refs['private_key'] = private_key_ref
        if private_key_passphrase_ref:
            secret_refs['private_key_passphrase'] = private_key_passphrase_ref
        super(RSAContainer, self).__init__(
            api=api,
            name=name,
            consumers=consumers,
            container_ref=container_ref,
            created=created,
            updated=updated,
            status=status,
            secret_refs=secret_refs
        )
        if public_key:
            self.public_key = public_key
        if private_key:
            self.private_key = private_key
        if private_key_passphrase:
            self.private_key_passphrase = private_key_passphrase

    @property
    def public_key(self):
        """Secret containing the Public Key"""
        return self._get_named_secret("public_key")

    @property
    def private_key(self):
        """Secret containing the Private Key"""
        return self._get_named_secret("private_key")

    @property
    def private_key_passphrase(self):
        """Secret containing the Passphrase"""
        return self._get_named_secret("private_key_passphrase")

    @public_key.setter
    @_immutable_after_save
    def public_key(self, value):
        super(RSAContainer, self).remove("public_key")
        super(RSAContainer, self).add("public_key", value)

    @private_key.setter
    @_immutable_after_save
    def private_key(self, value):
        super(RSAContainer, self).remove("private_key")
        super(RSAContainer, self).add("private_key", value)

    @private_key_passphrase.setter
    @_immutable_after_save
    def private_key_passphrase(self, value):
        super(RSAContainer, self).remove("private_key_passphrase")
        super(RSAContainer, self).add("private_key_passphrase", value)

    def add(self, name, sec):
        raise NotImplementedError("`add()` is not implemented for "
                                  "Typed Containers")

    def __repr__(self):
        return 'RSAContainer(name="{0}")'.format(self.name)


class CertificateContainerFormatter(formatter.EntityFormatter):
    _get_generic_data = ContainerFormatter._get_formatted_data

    def _get_generic_columns(self):
        return ContainerFormatter.columns

    columns = ("Container href",
               "Name",
               "Created",
               "Status",
               "Type",
               "Certificate",
               "Intermediates",
               "Private Key",
               "PK Passphrase",
               "Consumers",
               )

    def _get_formatted_data(self):
        formatted_certificate = None
        formatted_private_key = None
        formatted_pkp = None
        formatted_intermediates = None
        formatted_consumers = None
        if self.certificate:
            formatted_certificate = self.certificate.secret_ref
        if self.intermediates:
            formatted_intermediates = self.intermediates.secret_ref
        if self.private_key:
            formatted_private_key = self.private_key.secret_ref
        if self.private_key_passphrase:
            formatted_pkp = self.private_key_passphrase.secret_ref
        if self.consumers:
            formatted_consumers = '\n'.join((str(c) for c in self.consumers))
        data = (self.container_ref,
                self.name,
                self.created,
                self.status,
                self._type,
                formatted_certificate,
                formatted_intermediates,
                formatted_private_key,
                formatted_pkp,
                formatted_consumers,
                )
        return data


class CertificateContainer(CertificateContainerFormatter, Container):
    _required_secrets = ["certificate", "private_key"]
    _optional_secrets = ["private_key_passphrase", "intermediates"]
    _type = 'certificate'

    def __init__(self, api, name=None, certificate=None, intermediates=None,
                 private_key=None, private_key_passphrase=None, consumers=[],
                 container_ref=None, created=None, updated=None, status=None,
                 certificate_ref=None, intermediates_ref=None,
                 private_key_ref=None, private_key_passphrase_ref=None):
        secret_refs = {}
        if certificate_ref:
            secret_refs['certificate'] = certificate_ref
        if intermediates_ref:
            secret_refs['intermediates'] = intermediates_ref
        if private_key_ref:
            secret_refs['private_key'] = private_key_ref
        if private_key_passphrase_ref:
            secret_refs['private_key_passphrase'] = private_key_passphrase_ref
        super(CertificateContainer, self).__init__(
            api=api,
            name=name,
            consumers=consumers,
            container_ref=container_ref,
            created=created,
            updated=updated,
            status=status,
            secret_refs=secret_refs
        )
        if certificate:
            self.certificate = certificate
        if intermediates:
            self.intermediates = intermediates
        if private_key:
            self.private_key = private_key
        if private_key_passphrase:
            self.private_key_passphrase = private_key_passphrase

    @property
    def certificate(self):
        """Secret containing the certificate"""
        return self._get_named_secret("certificate")

    @property
    def private_key(self):
        """Secret containing the private key"""
        return self._get_named_secret("private_key")

    @property
    def private_key_passphrase(self):
        """Secret containing the passphrase"""
        return self._get_named_secret("private_key_passphrase")

    @property
    def intermediates(self):
        """Secret containing intermediate certificates"""
        return self._get_named_secret("intermediates")

    @certificate.setter
    @_immutable_after_save
    def certificate(self, value):
        super(CertificateContainer, self).remove("certificate")
        super(CertificateContainer, self).add("certificate", value)

    @private_key.setter
    @_immutable_after_save
    def private_key(self, value):
        super(CertificateContainer, self).remove("private_key")
        super(CertificateContainer, self).add("private_key", value)

    @private_key_passphrase.setter
    @_immutable_after_save
    def private_key_passphrase(self, value):
        super(CertificateContainer, self).remove("private_key_passphrase")
        super(CertificateContainer, self).add("private_key_passphrase", value)

    @intermediates.setter
    @_immutable_after_save
    def intermediates(self, value):
        super(CertificateContainer, self).remove("intermediates")
        super(CertificateContainer, self).add("intermediates", value)

    def add(self, name, sec):
        raise NotImplementedError("`add()` is not implemented for "
                                  "Typed Containers")

    def __repr__(self):
        return 'CertificateContainer(name="{0}")'.format(self.name)


class ContainerManager(base.BaseEntityManager):
    """
    EntityManager for Container entities

    You should use the ContainerManager exposed by the Client and should not
    need to instantiate your own.
    """

    _container_map = {
        'generic': Container,
        'rsa': RSAContainer,
        'certificate': CertificateContainer
    }

    def __init__(self, api):
        super(ContainerManager, self).__init__(api, 'containers')

    def get(self, container_ref):
        """
        Retrieve an existing Container from Barbican

        :param str container_ref: Full HATEOAS reference to a Container
        :returns: Container object or a subclass of the appropriate type
        """
        LOG.debug('Getting container - Container href: {0}'
                  .format(container_ref))
        base.validate_ref(container_ref, 'Container')
        try:
            response = self._api.get(container_ref)
        except AttributeError:
            raise LookupError('Container {0} could not be found.'
                              .format(container_ref))
        return self._generate_typed_container(response)

    def _generate_typed_container(self, response):
        resp_type = response.get('type', '').lower()
        container_type = self._container_map.get(resp_type)
        if not container_type:
            raise TypeError('Unknown container type "{0}".'
                            .format(resp_type))

        name = response.get('name')
        consumers = response.get('consumers', [])
        container_ref = response.get('container_ref')
        created = response.get('created')
        updated = response.get('updated')
        status = response.get('status')
        secret_refs = self._translate_secret_refs_from_json(
            response.get('secret_refs')
        )

        if container_type is RSAContainer:
            public_key_ref = secret_refs.get('public_key')
            private_key_ref = secret_refs.get('private_key')
            private_key_pass_ref = secret_refs.get('private_key_passphrase')
            return RSAContainer(
                api=self._api,
                name=name,
                consumers=consumers,
                container_ref=container_ref,
                created=created,
                updated=updated,
                status=status,
                public_key_ref=public_key_ref,
                private_key_ref=private_key_ref,
                private_key_passphrase_ref=private_key_pass_ref,
            )
        elif container_type is CertificateContainer:
            certificate_ref = secret_refs.get('certificate')
            intermediates_ref = secret_refs.get('intermediates')
            private_key_ref = secret_refs.get('private_key')
            private_key_pass_ref = secret_refs.get('private_key_passphrase')
            return CertificateContainer(
                api=self._api,
                name=name,
                consumers=consumers,
                container_ref=container_ref,
                created=created,
                updated=updated,
                status=status,
                certificate_ref=certificate_ref,
                intermediates_ref=intermediates_ref,
                private_key_ref=private_key_ref,
                private_key_passphrase_ref=private_key_pass_ref,
            )
        return container_type(
            api=self._api,
            name=name,
            secret_refs=secret_refs,
            consumers=consumers,
            container_ref=container_ref,
            created=created,
            updated=updated,
            status=status
        )

    @staticmethod
    def _translate_secret_refs_from_json(json_refs):
        return dict(
            (ref_pack.get('name'), ref_pack.get('secret_ref'))
            for ref_pack in json_refs
        )

    def create(self, name=None, secrets=None):
        """
        Factory method for `Container` objects

        `Container` objects returned by this method have not yet been
        stored in Barbican.

        :param name: A friendly name for the Container
        :param secrets: Secrets to populate when creating a Container
        :returns: Container
        :rtype: :class:`barbicanclient.containers.Container`
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        return Container(
            api=self._api,
            name=name,
            secrets=secrets
        )

    def create_rsa(self, name=None, public_key=None, private_key=None,
                   private_key_passphrase=None):
        """
        Factory method for `RSAContainer` objects

        `RSAContainer` objects returned by this method have not yet been
        stored in Barbican.

        :param name: A friendly name for the RSAContainer
        :param public_key: Secret object containing a Public Key
        :param private_key: Secret object containing a Private Key
        :param private_key_passphrase: Secret object containing a passphrase
        :returns: RSAContainer
        :rtype: :class:`barbicanclient.containers.RSAContainer`
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        return RSAContainer(
            api=self._api,
            name=name,
            public_key=public_key,
            private_key=private_key,
            private_key_passphrase=private_key_passphrase
        )

    def create_certificate(self, name=None, certificate=None,
                           intermediates=None, private_key=None,
                           private_key_passphrase=None):
        """
        Factory method for `CertificateContainer` objects

        `CertificateContainer` objects returned by this method have not yet
        been stored in Barbican.

        :param name: A friendly name for the CertificateContainer
        :param certificate: Secret object containing a Certificate
        :param intermediates: Secret object containing Intermediate Certs
        :param private_key: Secret object containing a Private Key
        :param private_key_passphrase: Secret object containing a passphrase
        :returns: CertificateContainer
        :rtype: :class:`barbicanclient.containers.CertificateContainer`
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        return CertificateContainer(
            api=self._api,
            name=name,
            certificate=certificate,
            intermediates=intermediates,
            private_key=private_key,
            private_key_passphrase=private_key_passphrase
        )

    def delete(self, container_ref):
        """
        Delete a Container from Barbican

        :param container_ref: Full HATEOAS reference to a Container
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        if not container_ref:
            raise ValueError('container_ref is required.')
        self._api.delete(container_ref)

    def list(self, limit=10, offset=0, name=None, type=None):
        """
        List containers for the project.  This method uses the limit and offset
        parameters for paging.

        :param limit: Max number of containers returned
        :param offset: Offset containers to begin list
        :param name: Name filter for the list
        :param type: Type filter for the list
        :returns: list of Container metadata objects
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        LOG.debug('Listing containers - offset {0} limit {1} name {2} type {3}'
                  .format(offset, limit, name, type))
        params = {'limit': limit, 'offset': offset}
        if name:
            params['name'] = name
        if type:
            params['type'] = type

        response = self._api.get(self._entity, params=params)

        return [self._generate_typed_container(container)
                for container in response.get('containers', [])]

    def register_consumer(self, container_ref, name, url):
        """
        Add a consumer to the container

        :param container_ref: Full HATEOAS reference to a Container
        :param name: Name of the consuming service
        :param url: URL of the consuming resource
        :returns: A container object per the get() method
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        LOG.debug('Creating consumer registration for container '
                  '{0} as {1}: {2}'.format(container_ref, name, url))
        href = '{0}/{1}/consumers'.format(self._entity,
                                          container_ref.split('/')[-1])
        consumer_dict = dict()
        consumer_dict['name'] = name
        consumer_dict['URL'] = url

        response = self._api.post(href, json=consumer_dict)
        return self._generate_typed_container(response)

    def remove_consumer(self, container_ref, name, url):
        """
        Remove a consumer from the container

        :param container_ref: Full HATEOAS reference to a Container
        :param name: Name of the previously consuming service
        :param url: URL of the previously consuming resource
        :raises barbicanclient.exceptions.HTTPAuthError: 401 Responses
        :raises barbicanclient.exceptions.HTTPClientError: 4xx Responses
        :raises barbicanclient.exceptions.HTTPServerError: 5xx Responses
        """
        LOG.debug('Deleting consumer registration for container '
                  '{0} as {1}: {2}'.format(container_ref, name, url))
        href = '{0}/{1}/consumers'.format(self._entity,
                                          container_ref.split('/')[-1])
        consumer_dict = {
            'name': name,
            'URL': url
        }

        self._api.delete(href, json=consumer_dict)
