# Copyright 2014 IBM Corp.
#
# 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 abc

import six

from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone import exception
from keystone.i18n import _, _LE, _LW
from keystone.openstack.common import log

CONF = config.CONF
LOG = log.getLogger(__name__)


@dependency.provider('endpoint_policy_api')
@dependency.requires('catalog_api', 'policy_api')
class Manager(manager.Manager):
    """Default pivot point for the Endpoint Policy backend.

    See :mod:`keystone.common.manager.Manager` for more details on how this
    dynamically calls the backend.

    """

    def __init__(self):
        super(Manager, self).__init__(CONF.endpoint_policy.driver)

    def _assert_valid_association(self, endpoint_id, service_id, region_id):
        """Assert that the association is supported.

        There are three types of association supported:

        - Endpoint (in which case service and region must be None)
        - Service and region (in which endpoint must be None)
        - Service (in which case endpoint and region must be None)

        """
        if (endpoint_id is not None and
                service_id is None and region_id is None):
            return
        if (service_id is not None and region_id is not None and
                endpoint_id is None):
            return
        if (service_id is not None and
                endpoint_id is None and region_id is None):
            return

        raise exception.InvalidPolicyAssociation(endpoint_id=endpoint_id,
                                                 service_id=service_id,
                                                 region_id=region_id)

    def create_policy_association(self, policy_id, endpoint_id=None,
                                  service_id=None, region_id=None):
        self._assert_valid_association(endpoint_id, service_id, region_id)
        self.driver.create_policy_association(policy_id, endpoint_id,
                                              service_id, region_id)

    def check_policy_association(self, policy_id, endpoint_id=None,
                                 service_id=None, region_id=None):
        self._assert_valid_association(endpoint_id, service_id, region_id)
        self.driver.check_policy_association(policy_id, endpoint_id,
                                             service_id, region_id)

    def delete_policy_association(self, policy_id, endpoint_id=None,
                                  service_id=None, region_id=None):
        self._assert_valid_association(endpoint_id, service_id, region_id)
        self.driver.delete_policy_association(policy_id, endpoint_id,
                                              service_id, region_id)

    def list_endpoints_for_policy(self, policy_id):

        def _get_endpoint(endpoint_id, policy_id):
            try:
                return self.catalog_api.get_endpoint(endpoint_id)
            except exception.EndpointNotFound:
                msg = _LW('Endpoint %(endpoint_id)s referenced in '
                          'association for policy %(policy_id)s not found.')
                LOG.warning(msg, {'policy_id': policy_id,
                                  'endpoint_id': endpoint_id})
                raise

        def _get_endpoints_for_service(service_id, endpoints):
            # TODO(henry-nash): Consider optimizing this in the future by
            # adding an explicit list_endpoints_for_service to the catalog API.
            return [ep for ep in endpoints if ep['service_id'] == service_id]

        def _get_endpoints_for_service_and_region(
                service_id, region_id, endpoints, regions):
            # TODO(henry-nash): Consider optimizing this in the future.
            # The lack of a two-way pointer in the region tree structure
            # makes this somewhat inefficient.

            def _recursively_get_endpoints_for_region(
                region_id, service_id, endpoint_list, region_list,
                    endpoints_found, regions_examined):
                """Recursively search down a region tree for endpoints.

                :param region_id: the point in the tree to examine
                :param service_id: the service we are interested in
                :param endpoint_list: list of all endpoints
                :param region_list: list of all regions
                :param endpoints_found: list of matching endpoints found so
                                        far - which will be updated if more are
                                        found in this iteration
                :param regions_examined: list of regions we have already looked
                                         at - used to spot illegal circular
                                         references in the tree to avoid never
                                         completing search
                :returns: list of endpoints that match

                """

                if region_id in regions_examined:
                    msg = _LE('Circular reference or a repeated entry found '
                              'in region tree - %(region_id)s.')
                    LOG.error(msg, {'region_id': ref.region_id})
                    return

                regions_examined.append(region_id)
                endpoints_found += (
                    [ep for ep in endpoint_list if
                     ep['service_id'] == service_id and
                     ep['region_id'] == region_id])

                for region in region_list:
                    if region['parent_region_id'] == region_id:
                        _recursively_get_endpoints_for_region(
                            region['id'], service_id, endpoints, regions,
                            endpoints_found, regions_examined)

            endpoints_found = []
            regions_examined = []

            # Now walk down the region tree
            _recursively_get_endpoints_for_region(
                region_id, service_id, endpoints, regions,
                endpoints_found, regions_examined)

            return endpoints_found

        matching_endpoints = []
        endpoints = self.catalog_api.list_endpoints()
        regions = self.catalog_api.list_regions()
        for ref in self.driver.list_associations_for_policy(policy_id):
            if ref.get('endpoint_id') is not None:
                matching_endpoints.append(
                    _get_endpoint(ref['endpoint_id'], policy_id))
                continue

            if (ref.get('service_id') is not None and
                    ref.get('region_id') is None):
                matching_endpoints += _get_endpoints_for_service(
                    ref['service_id'], endpoints)
                continue

            if (ref.get('service_id') is not None and
                    ref.get('region_id') is not None):
                matching_endpoints += (
                    _get_endpoints_for_service_and_region(
                        ref['service_id'], ref['region_id'],
                        endpoints, regions))
                continue

            msg = _LW('Unsupported policy association found - '
                      'Policy %(policy_id)s, Endpoint %(endpoint_id)s, '
                      'Service %(service_id)s, Region %(region_id)s, ')
            LOG.warning(msg, {'policy_id': policy_id,
                              'endpoint_id': ref['endpoint_id'],
                              'service_id': ref['service_id'],
                              'region_id': ref['region_id']})

        return matching_endpoints

    def get_policy_for_endpoint(self, endpoint_id):

        def _get_policy(policy_id, endpoint_id):
            try:
                return self.policy_api.get_policy(policy_id)
            except exception.PolicyNotFound:
                msg = _LW('Policy %(policy_id)s referenced in association '
                          'for endpoint %(endpoint_id)s not found.')
                LOG.warning(msg, {'policy_id': policy_id,
                                  'endpoint_id': endpoint_id})
                raise

        def _look_for_policy_for_region_and_service(endpoint):
            """Look in the region and its parents for a policy.

            Examine the region of the endpoint for a policy appropriate for
            the service of the endpoint. If there isn't a match, then chase up
            the region tree to find one.

            """
            region_id = endpoint['region_id']
            regions_examined = []
            while region_id is not None:
                try:
                    ref = self.driver.get_policy_association(
                        service_id=endpoint['service_id'],
                        region_id=region_id)
                    return ref['policy_id']
                except exception.PolicyAssociationNotFound:
                    pass

                # There wasn't one for that region & service, let's
                # chase up the region tree
                regions_examined.append(region_id)
                region = self.catalog_api.get_region(region_id)
                region_id = None
                if region.get('parent_region_id') is not None:
                    region_id = region['parent_region_id']
                    if region_id in regions_examined:
                        msg = _LE('Circular reference or a repeated entry '
                                  'found in region tree - %(region_id)s.')
                        LOG.error(msg, {'region_id': region_id})
                        break

        # First let's see if there is a policy explicitly defined for
        # this endpoint.

        try:
            ref = self.driver.get_policy_association(endpoint_id=endpoint_id)
            return _get_policy(ref['policy_id'], endpoint_id)
        except exception.PolicyAssociationNotFound:
            pass

        # There wasn't a policy explicitly defined for this endpoint, so
        # now let's see if there is one for the Region & Service.

        endpoint = self.catalog_api.get_endpoint(endpoint_id)
        policy_id = _look_for_policy_for_region_and_service(endpoint)
        if policy_id is not None:
            return _get_policy(policy_id, endpoint_id)

        # Finally, just check if there is one for the service.
        try:
            ref = self.driver.get_policy_association(
                service_id=endpoint['service_id'])
            return _get_policy(ref['policy_id'], endpoint_id)
        except exception.PolicyAssociationNotFound:
            pass

        msg = _('No policy is associated with endpoint '
                '%(endpoint_id)s.') % {'endpoint_id': endpoint_id}
        raise exception.NotFound(msg)


@six.add_metaclass(abc.ABCMeta)
class Driver(object):
    """Interface description for an Endpoint Policy driver."""

    @abc.abstractmethod
    def create_policy_association(self, policy_id, endpoint_id=None,
                                  service_id=None, region_id=None):
        """Creates a policy association.

        :param policy_id: identity of policy that is being associated
        :type policy_id: string
        :param endpoint_id: identity of endpoint to associate
        :type endpoint_id: string
        :param service_id: identity of the service to associate
        :type service_id: string
        :param region_id: identity of the region to associate
        :type region_id: string
        :returns: None

        There are three types of association permitted:

        - Endpoint (in which case service and region must be None)
        - Service and region (in which endpoint must be None)
        - Service (in which case endpoint and region must be None)

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def check_policy_association(self, policy_id, endpoint_id=None,
                                 service_id=None, region_id=None):
        """Checks existence a policy association.

        :param policy_id: identity of policy that is being associated
        :type policy_id: string
        :param endpoint_id: identity of endpoint to associate
        :type endpoint_id: string
        :param service_id: identity of the service to associate
        :type service_id: string
        :param region_id: identity of the region to associate
        :type region_id: string
        :raises: keystone.exception.PolicyAssociationNotFound if there is no
                 match for the specified association
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def delete_policy_association(self, policy_id, endpoint_id=None,
                                  service_id=None, region_id=None):
        """Deletes a policy association.

        :param policy_id: identity of policy that is being associated
        :type policy_id: string
        :param endpoint_id: identity of endpoint to associate
        :type endpoint_id: string
        :param service_id: identity of the service to associate
        :type service_id: string
        :param region_id: identity of the region to associate
        :type region_id: string
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def get_policy_association(self, endpoint_id=None,
                               service_id=None, region_id=None):
        """Gets the policy for an explicit association.

        This method is not exposed as a public API, but is used by
        get_policy_for_endpoint().

        :param endpoint_id: identity of endpoint
        :type endpoint_id: string
        :param service_id: identity of the service
        :type service_id: string
        :param region_id: identity of the region
        :type region_id: string
        :raises: keystone.exception.PolicyAssociationNotFound if there is no
                 match for the specified association
        :returns: dict containing policy_id

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def list_associations_for_policy(self, policy_id):
        """List the associations for a policy.

        This method is not exposed as a public API, but is used by
        list_endpoints_for_policy().

        :param policy_id: identity of policy
        :type policy_id: string
        :returns: List of association dicts

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def list_endpoints_for_policy(self, policy_id):
        """List all the endpoints using a given policy.

        :param policy_id: identity of policy that is being associated
        :type policy_id: string
        :returns: list of endpoints that have an effective association with
                  that policy

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def get_policy_for_endpoint(self, endpoint_id):
        """Get the appropriate policy for a given endpoint.

        :param endpoint_id: identity of endpoint
        :type endpoint_id: string
        :returns: Policy entity for the endpoint


        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def delete_association_by_endpoint(self, endpoint_id):
        """Removes all the policy associations with the specific endpoint.

        :param endpoint_id: identity of endpoint to check
        :type endpoint_id: string
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def delete_association_by_service(self, service_id):
        """Removes all the policy associations with the specific service.

        :param service_id: identity of endpoint to check
        :type service_id: string
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def delete_association_by_region(self, region_id):
        """Removes all the policy associations with the specific region.

        :param region_id: identity of endpoint to check
        :type region_id: string
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover

    @abc.abstractmethod
    def delete_association_by_policy(self, policy_id):
        """Removes all the policy associations with the specific policy.

        :param policy_id: identity of endpoint to check
        :type policy_id: string
        :returns: None

        """
        raise exception.NotImplemented()  # pragma: no cover
