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

from keystoneclient import exceptions as k_exceptions
from keystoneclient.v2_0 import client as k_client
from neutron_lib.db import model_base
from oslo_config import cfg
from oslo_log import helpers as log
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import excutils
import sqlalchemy as sa

from gbpservice._i18n import _
from gbpservice.common import utils
from gbpservice.network.neutronv2 import local_api
from gbpservice.neutron.db.grouppolicy import group_policy_mapping_db as gpdb
from gbpservice.neutron.services.grouppolicy import (
    group_policy_driver_api as api)
from gbpservice.neutron.services.grouppolicy.common import constants as gconst
from gbpservice.neutron.services.grouppolicy.common import exceptions as exc
from gbpservice.neutron.services.grouppolicy.common import utils as gutils
from gbpservice.neutron.services.grouppolicy.drivers import nsp_manager
from gbpservice.neutron.services.grouppolicy import sc_notifications


LOG = logging.getLogger(__name__)
SCI_CONSUMER_NOT_AVAILABLE = 'N/A'

chain_mapping_opts = [
    cfg.StrOpt('chain_owner_user',
               help=_("Chain owner username. If set, will be used in "
                      "place of the Neutron service admin for retrieving "
                      "tenant owner information through Keystone."),
               default=''),
    cfg.StrOpt('chain_owner_password',
               help=_("Chain owner password."), default='',
               secret=True),
    cfg.StrOpt('chain_owner_tenant_name',
               help=_("Name of the Tenant that will own the service chain "
                      "instances for this driver. Leave empty for provider "
                      "owned chains."), default=''),

]

cfg.CONF.register_opts(chain_mapping_opts, "chain_mapping")
cfg.CONF.import_opt('policy_drivers',
                    'gbpservice.neutron.services.grouppolicy.config',
                    group='group_policy')


class PtgServiceChainInstanceMapping(model_base.BASEV2, model_base.HasProject):
    """Policy Target Group to ServiceChainInstance mapping DB."""

    __tablename__ = 'gpm_ptgs_servicechain_mapping'
    provider_ptg_id = sa.Column(sa.String(36),
                                sa.ForeignKey('gp_policy_target_groups.id',
                                              ondelete='CASCADE'),
                                nullable=False)
    # Consumer PTG could be an External Policy
    consumer_ptg_id = sa.Column(sa.String(36), nullable=False)
    servicechain_instance_id = sa.Column(sa.String(36),
                                         sa.ForeignKey('sc_instances.id',
                                                       ondelete='CASCADE'),
                                         primary_key=True)


class ChainMappingDriver(api.PolicyDriver, local_api.LocalAPI,
                         nsp_manager.NetworkServicePolicyMappingMixin,
                         sc_notifications.ServiceChainNotificationsMixin):
    """Resource Mapping driver for Group Policy plugin.

    This driver implements service chain semantics by mapping group
    policy resources to various service chain constructs.
    """

    @log.log_method_call
    def initialize(self):
        self._cached_agent_notifier = None
        self.chain_owner = ChainMappingDriver.chain_tenant_id(reraise=True)

    @staticmethod
    def chain_tenant_id(reraise=False):
        try:
            keystone = ChainMappingDriver.chain_tenant_keystone_client()
        except cfg.NoSuchOptError:
            return None
        if keystone:
            tenant = cfg.CONF.chain_mapping.chain_owner_tenant_name
            try:
                # Can it be retrieved directly, without a further keystone
                # call?
                tenant = keystone.tenants.find(name=tenant)
                return tenant.id
            except k_exceptions.NotFound:
                with excutils.save_and_reraise_exception(reraise=reraise):
                    LOG.error('No tenant with name %s exists.', tenant)
            except k_exceptions.NoUniqueMatch:
                with excutils.save_and_reraise_exception(reraise=reraise):
                    LOG.error('Multiple tenants matches found for %s',
                              tenant)

    @staticmethod
    def chain_tenant_keystone_client():
        chain_user = cfg.CONF.chain_mapping.chain_owner_user
        user, pwd, tenant, auth_url = utils.get_keystone_creds()
        user = (chain_user or user)
        pwd = (cfg.CONF.chain_mapping.chain_owner_password or
               (pwd if not chain_user else ''))

        # Tenant must be configured in the resource_mapping section, provider
        # owner will be used otherwise.
        tenant = cfg.CONF.chain_mapping.chain_owner_tenant_name

        if tenant:
            return k_client.Client(username=user, password=pwd,
                                   auth_url=auth_url)

    @log.log_method_call
    def precommit_wrapper_for_chain_mapping(self, caller_method, context):
        # This wrapper is called from both precommit and postcommit for
        # deciding ordering for execution of precommit and postcommit
        if 'precommit' in caller_method:
            context._plugin_context.commit_phase = gconst.PRE_COMMIT
            # In case of aim_mapping driver (or a similar precommit driver),
            # postcommit functionality will be executed as a part of precommit
            if gutils.is_precommit_policy_driver_configured():
                if 'delete_policy_target_group' in caller_method:
                    context.ptg_chain_map = self._get_ptg_servicechain_mapping(
                        context._plugin_context.session,
                        context.current['id'])
                    self._cleanup_redirect_action(context)
                else:
                    method = getattr(self, '_' + caller_method.replace(
                                         'precommit', 'postcommit'))
                    method(context)
        else:
            context._plugin_context.commit_phase = gconst.POST_COMMIT
            # If driver is not aim_mapping (or a similar precommit driver),
            # then postcommit functionality will  be executed by the call
            # from gbp plugin
            if not gutils.is_precommit_policy_driver_configured():
                method = getattr(self, '_' + caller_method)
                method(context)
            elif 'create_policy_target_group' in caller_method or (
                'create_external_policy' in caller_method):
                if hasattr(context, 'provider_context') and (
                    hasattr(context, 'servicechain_attrs')):
                    context.provider_context.commit_phase = (
                        context._plugin_context.commit_phase)
                    context.provider_context.servicechain_instance = (
                        context._plugin_context.servicechain_instance)
                    super(ChainMappingDriver,
                        self)._create_servicechain_instance(
                            context.provider_context,
                            context.servicechain_attrs)

    @log.log_method_call
    def _create_policy_target_postcommit(self, context):
        if not context._plugin._is_service_target(context._plugin_context,
                                                  context.current['id']):
            mappings = self._get_ptg_servicechain_mapping(
                context._plugin_context.session,
                provider_ptg_id=context.current['policy_target_group_id'])
            for mapping in mappings:
                chain_context = self._get_chain_admin_context(
                    context._plugin_context,
                    instance_id=mapping.servicechain_instance_id)
                self._notify_sc_plugin_pt_added(
                    chain_context, context.current,
                    mapping.servicechain_instance_id)

    @log.log_method_call
    def create_policy_target_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_target_precommit', context)

    @log.log_method_call
    def create_policy_target_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_target_postcommit', context)

    @log.log_method_call
    def delete_policy_target_precommit(self, context):
        context._is_service_target = context._plugin._is_service_target(
            context._plugin_context, context.current['id'])

    @log.log_method_call
    def delete_policy_target_postcommit(self, context):
        if not context._is_service_target:
            mappings = self._get_ptg_servicechain_mapping(
                context._plugin_context.session,
                provider_ptg_id=context.current['policy_target_group_id'])
            for mapping in mappings:
                chain_context = self._get_chain_admin_context(
                    context._plugin_context,
                    instance_id=mapping.servicechain_instance_id)
                self._notify_sc_plugin_pt_removed(
                    chain_context, context.current,
                    mapping.servicechain_instance_id)

    @log.log_method_call
    def _create_policy_target_group_postcommit(self, context):
        if (context.current['provided_policy_rule_sets'] and
                self._is_group_chainable(context, context.current)):
            self._handle_redirect_action(
                context, context.current['provided_policy_rule_sets'],
                providing_ptg=context.current)
        self._handle_prs_added(context)
        self._handle_provider_updated(context)

    @log.log_method_call
    def create_policy_target_group_precommit(self, context):
        self._validate_ptg_prss(context, context.current)
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_target_group_precommit', context)

    @log.log_method_call
    def create_policy_target_group_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_target_group_postcommit', context)

    @log.log_method_call
    def _update_policy_target_group_postcommit(self, context):
        # Update service chain instance when any ruleset is changed
        orig = context.original
        curr = context.current

        new_provided_policy_rule_sets = list(
            set(curr['provided_policy_rule_sets']) - set(
                orig['provided_policy_rule_sets']))

        # Only the ones set in context in precommit operation will be deleted
        self._cleanup_redirect_action(context)
        # If the spec is changed, then update the chain with new spec
        # If redirect is newly added, create the chain
        if (self._is_redirect_in_policy_rule_sets(
                context, new_provided_policy_rule_sets) and
                self._is_group_chainable(context, context.current)):
            self._handle_redirect_action(
                context, curr['provided_policy_rule_sets'],
                providing_ptg=context.current)
        self._handle_prs_updated(context)
        self._handle_provider_updated(context)

    @log.log_method_call
    def update_policy_target_group_precommit(self, context):
        self._validate_ptg_prss(context, context.current)
        self._stash_ptg_modified_chains(context)
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_target_group_precommit', context)

    @log.log_method_call
    def update_policy_target_group_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_target_group_postcommit', context)

    @log.log_method_call
    def get_policy_target_group_status(self, context):
        try:
            if (context.current['provided_policy_rule_sets'] and not
                context.current.get('proxied_group_id')):
                ptg_status = []
                for sci in self._get_chains_by_prs(
                            context,
                            context.current['provided_policy_rule_sets']):
                    servicechain_instance = self._get_servicechain_instance(
                                                    context._plugin_context,
                                                    sci)
                    if (servicechain_instance['provider_ptg_id'] ==
                            context.current['id']):
                        ptg_status.append(
                                {'status': servicechain_instance['status'],
                                 'status_details': servicechain_instance[
                                    'status_details']})
                # REVISIT: For now assuming there will be only
                #          one sci associated with this ptg
                if ptg_status:
                    context.current['status'] = ptg_status[0]['status']
                    context.current['status_details'] = ptg_status[0][
                            'status_details']
        except Exception:
            LOG.error('Failed to update ptg status')

    @log.log_method_call
    def _delete_policy_target_group_postcommit(self, context):
        self._handle_prs_removed(context)

    @log.log_method_call
    def delete_policy_target_group_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_policy_target_group_precommit', context)

    @log.log_method_call
    def delete_policy_target_group_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_policy_target_group_postcommit', context)

    @log.log_method_call
    def _update_policy_classifier_postcommit(self, context):
        self._handle_classifier_update_notification(context)

    @log.log_method_call
    def update_policy_classifier_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_classifier_precommit', context)

    @log.log_method_call
    def update_policy_classifier_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_classifier_postcommit', context)

    @log.log_method_call
    def create_policy_action_precommit(self, context):
        spec_id = context.current['action_value']
        if spec_id:
            specs = self._get_servicechain_specs(
                context._plugin_context, filters={'id': [spec_id]})
            for spec in specs:
                if not spec.get('shared', False):
                    self._reject_shared(context.current, 'policy_action')

    @log.log_method_call
    def _update_policy_action_postcommit(self, context):
        self._handle_redirect_spec_id_update(context)

    @log.log_method_call
    def update_policy_action_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_action_precommit', context)

    @log.log_method_call
    def update_policy_action_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_action_postcommit', context)

    @log.log_method_call
    def create_policy_rule_precommit(self, context):
        self._reject_multiple_redirects_in_rule(context)

    @log.log_method_call
    def _update_policy_rule_postcommit(self, context):
        old_classifier_id = context.original['policy_classifier_id']
        new_classifier_id = context.current['policy_classifier_id']
        old_action_set = set(context.current['policy_actions'])
        new_action_set = set(context.original['policy_actions'])
        if (old_classifier_id != new_classifier_id or
                old_action_set != new_action_set):
            policy_rule_sets = (
                context._plugin._get_policy_rule_policy_rule_sets(
                    context._plugin_context, context.current['id']))

            old_redirect_policy_actions = context._plugin.get_policy_actions(
                        context._plugin_context,
                        filters={'id': context.original['policy_actions'],
                                 'action_type': [gconst.GP_ACTION_REDIRECT]})
            new_redirect_policy_actions = context._plugin.get_policy_actions(
                        context._plugin_context,
                        filters={'id': context.current['policy_actions'],
                                 'action_type': [gconst.GP_ACTION_REDIRECT]})
            if old_redirect_policy_actions or new_redirect_policy_actions:
                self._handle_redirect_action(context, policy_rule_sets)

    @log.log_method_call
    def update_policy_rule_precommit(self, context):
        self._reject_multiple_redirects_in_rule(context)
        old_redirect = self._get_redirect_action(context, context.original)
        new_redirect = self._get_redirect_action(context, context.current)
        if not old_redirect and new_redirect:
            # If redirect action is added, check that there's no contract that
            # already has a redirect action
            for prs in context._plugin.get_policy_rule_sets(
                    context._plugin_context,
                    {'id': context.current['policy_rule_sets']}):
                # Make sure the PRS can have a new redirect action
                self._validate_new_prs_redirect(context, prs)
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_rule_precommit', context)

    @log.log_method_call
    def update_policy_rule_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_rule_postcommit', context)

    @log.log_method_call
    def _create_policy_rule_set_postcommit(self, context):
        if context.current['child_policy_rule_sets']:
            self._handle_redirect_action(
                context, context.current['child_policy_rule_sets'])

    @log.log_method_call
    def create_policy_rule_set_precommit(self, context):
        self._reject_multiple_redirects_in_prs(context)
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_rule_set_precommit', context)

    @log.log_method_call
    def create_policy_rule_set_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_policy_rule_set_postcommit', context)

    @log.log_method_call
    def _update_policy_rule_set_postcommit(self, context):
        if self._is_redirect_rule_updated(context):
            # Handle any Redirects from the current Policy Rule Set
            self._handle_redirect_action(context, [context.current['id']])

        # Handle Update/Delete of Redirects for any child Rule Sets
        if (set(context.original['child_policy_rule_sets']) !=
                set(context.current['child_policy_rule_sets'])):
            if context.original['child_policy_rule_sets']:
                self._handle_redirect_action(
                    context, context.original['child_policy_rule_sets'])
            if context.current['child_policy_rule_sets']:
                self._handle_redirect_action(
                    context, context.current['child_policy_rule_sets'])

    @log.log_method_call
    def update_policy_rule_set_precommit(self, context):
        self._reject_multiple_redirects_in_prs(context)
        # If a redirect action is added (from 0 to one) we have to validate
        # the providing and consuming PTGs. Not needed at creation time since
        # no PTG could be possibly providing or consuming it
        old_red_count = self._multiple_pr_redirect_action_number(
            context._plugin_context.session, context.original['policy_rules'])
        new_red_count = self._multiple_pr_redirect_action_number(
            context._plugin_context.session, context.current['policy_rules'])
        if new_red_count > old_red_count:
            self._validate_new_prs_redirect(context, context.current)
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_rule_set_precommit', context)

    @log.log_method_call
    def update_policy_rule_set_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_policy_rule_set_postcommit', context)

    @log.log_method_call
    def _delete_policy_rule_set_postcommit(self, context):
        if context.current['child_policy_rule_sets']:
            self._handle_redirect_action(
                context, context.current['child_policy_rule_sets'])

    @log.log_method_call
    def delete_policy_rule_set_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_policy_rule_set_precommit', context)

    @log.log_method_call
    def delete_policy_rule_set_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_policy_rule_set_postcommit', context)

    @log.log_method_call
    def _create_external_policy_postcommit(self, context):
        self._handle_prs_added(context)

    @log.log_method_call
    def create_external_policy_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_external_policy_precommit', context)

    @log.log_method_call
    def create_external_policy_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'create_external_policy_postcommit', context)

    @log.log_method_call
    def _update_external_policy_postcommit(self, context):
        self._handle_prs_updated(context)

    @log.log_method_call
    def update_external_policy_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_external_policy_precommit', context)

    @log.log_method_call
    def update_external_policy_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'update_external_policy_postcommit', context)

    @log.log_method_call
    def _delete_external_policy_postcommit(self, context):
        self._handle_prs_removed(context)

    @log.log_method_call
    def delete_external_policy_precommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_external_policy_precommit', context)

    @log.log_method_call
    def delete_external_policy_postcommit(self, context):
        self.precommit_wrapper_for_chain_mapping(
            'delete_external_policy_postcommit', context)

    def _is_redirect_rule_updated(self, context):
        if (not context.original['child_policy_rule_sets']) and (
            not context.current['child_policy_rule_sets']):
            '''
            This method checks if a rule with a REDIRECT action is added to or
            removed from the PRS. If no REDIRECT action has been added or
            removed, then the processing for REDIRECT does not have to be
            performed and existing service chain instances should be
            left untouched.
            '''
            old_redirect_count = self._multiple_pr_redirect_action_number(
                context._plugin_context.session,
                context.original['policy_rules'])
            new_redirect_count = self._multiple_pr_redirect_action_number(
                context._plugin_context.session,
                context.current['policy_rules'])
            if (new_redirect_count == 0) and (old_redirect_count == 0):
                return False
            elif (new_redirect_count != old_redirect_count):
                return True
            else:
                original_policy_rules = context.original['policy_rules']
                current_policy_rules = context.current['policy_rules']
                policy_rules = context._plugin.get_policy_rules(
                    context._plugin_context,
                    filters={'id': original_policy_rules})
                for policy_rule in policy_rules:
                    original_redirect_policy_action = (
                        self._get_redirect_action(
                             context, policy_rule))
                    if (original_redirect_policy_action) and (
                        policy_rule['id'] not in current_policy_rules):
                        return True
                policy_rules = context._plugin.get_policy_rules(
                    context._plugin_context,
                    filters={'id': current_policy_rules})
                for policy_rule in policy_rules:
                    current_redirect_policy_action = self._get_redirect_action(
                        context, policy_rule)
                    if (current_redirect_policy_action) and (
                        policy_rule['id'] not in original_policy_rules):
                        return True
                return False
        return True

    def _handle_prs_added(self, context):
        # Expecting either a PTG or EP context
        if context.current['consumed_policy_rule_sets']:
            for sci in self._get_chains_by_prs(
                    context, context.current['consumed_policy_rule_sets']):
                chain_context = self._get_chain_admin_context(
                    context._plugin_context, instance_id=sci)
                self._notify_sc_consumer_added(
                    chain_context, context.current, sci)

    def _handle_prs_removed(self, context):
        # Expecting either a PTG or EP context
        if context.current['consumed_policy_rule_sets']:
            for sci in self._get_chains_by_prs(
                    context, context.current['consumed_policy_rule_sets']):
                chain_context = self._get_chain_admin_context(
                    context._plugin_context, instance_id=sci)
                self._notify_sc_consumer_removed(
                    chain_context, context.current, sci)

    def _handle_prs_updated(self, context):
        # Expecting either a PTG or EP context
        if (context.current['consumed_policy_rule_sets'] !=
                context.original['consumed_policy_rule_sets']):
            added, removed = utils.set_difference(
                context.current['consumed_policy_rule_sets'],
                context.original['consumed_policy_rule_sets'])
            if removed:
                for sci in self._get_chains_by_prs(context, removed):
                    chain_context = self._get_chain_admin_context(
                        context._plugin_context, instance_id=sci)
                    self._notify_sc_consumer_removed(
                        chain_context, context.current, sci)
            if added:
                for sci in self._get_chains_by_prs(context, added):
                    chain_context = self._get_chain_admin_context(
                        context._plugin_context, instance_id=sci)
                    self._notify_sc_consumer_removed(
                        chain_context, context.current, sci)

    def _handle_redirect_spec_id_update(self, context):
        if (context.current['action_type'] != gconst.GP_ACTION_REDIRECT or
            context.current['action_value'] ==
            context.original['action_value']):
            return

        spec = self._servicechain_plugin._get_servicechain_spec(
                    context._plugin_context, context.original['action_value'])
        for servicechain_instance in spec.instances:
            sc_instance_id = servicechain_instance.servicechain_instance_id
            sc_instance = self._servicechain_plugin.get_servicechain_instance(
                    context._plugin_context, sc_instance_id)
            old_specs = sc_instance['servicechain_specs']
            # Use the parent/child redirect spec as it is. Only replace the
            # current one
            new_specs = [context.current['action_value'] if
                         x == context.original['action_value'] else
                         x for x in old_specs]
            self._update_servicechain_instance(
                context._plugin_context,
                servicechain_instance.servicechain_instance_id,
                sc_specs=new_specs)

    def _update_servicechain_instance(self, plugin_context, sc_instance_id,
                                      classifier_id=None, sc_specs=None):
        sc_instance_update_data = {}
        if sc_specs:
            sc_instance_update_data.update({'servicechain_specs': sc_specs})
        if classifier_id:
            sc_instance_update_data.update({'classifier_id': classifier_id})
        super(ChainMappingDriver, self)._update_servicechain_instance(
            self._get_chain_admin_context(
                plugin_context, instance_id=sc_instance_id),
            sc_instance_id, sc_instance_update_data)

    # This method would either update an existing chain instance, or creates a
    # new chain instance or delete the existing instance. In case of updates,
    # the parameters that can be updated are service chain spec and
    # classifier ID.
    def _handle_redirect_action(self, context, policy_rule_set_ids,
                                providing_ptg=None):
        policy_rule_sets = context._plugin.get_policy_rule_sets(
            context._plugin_context, filters={'id': policy_rule_set_ids})
        for policy_rule_set in policy_rule_sets:
            if providing_ptg:
                ptgs_providing_prs = [providing_ptg]
            else:
                if not policy_rule_set['providing_policy_target_groups']:
                    continue
                ptgs_providing_prs = context._plugin.get_policy_target_groups(
                    context._plugin_context.elevated(),
                    {'id': policy_rule_set['providing_policy_target_groups']})
            parent_classifier_id = None
            parent_spec_id = None
            if policy_rule_set['parent_id']:
                parent = context._plugin.get_policy_rule_set(
                    context._plugin_context, policy_rule_set['parent_id'])
                policy_rules = context._plugin.get_policy_rules(
                    context._plugin_context,
                    filters={'id': parent['policy_rules']})
                for policy_rule in policy_rules:
                    policy_actions = context._plugin.get_policy_actions(
                        context._plugin_context,
                        filters={'id': policy_rule["policy_actions"],
                                 'action_type': [gconst.GP_ACTION_REDIRECT]})
                    if policy_actions:
                        parent_spec_id = policy_actions[0].get("action_value")
                        parent_classifier_id = policy_rule.get(
                            "policy_classifier_id")
                        break  # only one redirect action is supported
            policy_rules = context._plugin.get_policy_rules(
                    context._plugin_context,
                    filters={'id': policy_rule_set['policy_rules']})
            for policy_rule in policy_rules:
                hierarchial_classifier_mismatch = False
                classifier_id = policy_rule.get("policy_classifier_id")
                if parent_classifier_id and (parent_classifier_id !=
                                             classifier_id):
                    hierarchial_classifier_mismatch = True
                policy_actions = context._plugin.get_policy_actions(
                        context._plugin_context,
                        filters={'id': policy_rule.get("policy_actions"),
                                 'action_type': [gconst.GP_ACTION_REDIRECT]})
                # Only one Redirect action per PRS. The chain may belong to
                # another PRS in which case the chain should not be deleted
                if (self._is_redirect_in_policy_rule_sets(
                    context, policy_rule_set_ids) and not policy_actions):
                    continue
                spec_id = (policy_actions and
                           policy_actions[0]['action_value'] or None)
                for ptg_providing_prs in ptgs_providing_prs:
                    # REVISIT(Magesh): There are concurrency issues here with
                    # concurrent updates to the same PRS, Policy Rule or Action
                    # value
                    if self._is_group_chainable(context, ptg_providing_prs):
                        self._create_or_update_chain(
                            context, ptg_providing_prs['id'],
                            SCI_CONSUMER_NOT_AVAILABLE, spec_id,
                            parent_spec_id, classifier_id,
                            hierarchial_classifier_mismatch,
                            policy_rule_set)

    def _create_or_update_chain(self, context, provider, consumer, spec_id,
                                parent_spec_id, classifier_id,
                                hierarchial_classifier_mismatch, prs_id):
        ptg_chain_map = self._get_ptg_servicechain_mapping(
            context._plugin_context.session, provider)
        if ptg_chain_map:
            if hierarchial_classifier_mismatch or not spec_id:
                ctx = self._get_chain_admin_context(
                    context._plugin_context,
                    tenant_id=ptg_chain_map[0].tenant_id)
                self._delete_servicechain_instance(
                    ctx, ptg_chain_map[0].servicechain_instance_id)
            else:
                sc_specs = [spec_id]
                if parent_spec_id:
                    sc_specs.insert(0, parent_spec_id)
                # One chain per providing PTG
                self._update_servicechain_instance(
                    context._plugin_context,
                    ptg_chain_map[0].servicechain_instance_id,
                    classifier_id=classifier_id,
                    sc_specs=sc_specs)
        elif spec_id and not hierarchial_classifier_mismatch:
            self._create_servicechain_instance(
                context, spec_id, parent_spec_id, provider,
                SCI_CONSUMER_NOT_AVAILABLE, classifier_id, prs_id)

    def _cleanup_redirect_action(self, context):
        for ptg_chain in context.ptg_chain_map:
            ctx = self._get_chain_admin_context(context._plugin_context,
                                                tenant_id=ptg_chain.tenant_id)
            self._delete_servicechain_instance(
                ctx, ptg_chain.servicechain_instance_id)

    def _create_servicechain_instance(self, context, servicechain_spec,
                                      parent_servicechain_spec,
                                      provider_ptg_id, consumer_ptg_id,
                                      classifier_id, policy_rule_set):
        sc_spec = [servicechain_spec]
        if parent_servicechain_spec:
            sc_spec.insert(0, parent_servicechain_spec)
        config_param_values = {}
        provider_ptg = context._plugin.get_policy_target_group(
            utils.admin_context(context._plugin_context), provider_ptg_id)
        p_ctx = self._get_chain_admin_context(
            context._plugin_context,
            provider_tenant_id=provider_ptg['tenant_id'])
        session = context._plugin_context.session
        network_service_policy_id = provider_ptg.get(
            "network_service_policy_id")
        if network_service_policy_id:
            nsp = context._plugin.get_network_service_policy(
                p_ctx, network_service_policy_id)
            service_params = nsp.get("network_service_params")
            for service_parameter in service_params:
                param_type = service_parameter.get("type")
                param_value = service_parameter.get("value")
                if param_type == "ip_single" and param_value == "self_subnet":
                    key = service_parameter.get("name")
                    servicepolicy_ptg_ip_map = (
                        self._get_ptg_policy_ipaddress_mapping(
                            context._plugin_context, provider_ptg_id))
                    servicepolicy_ip = servicepolicy_ptg_ip_map.get(
                                                        "ipaddress")
                    config_param_values[key] = servicepolicy_ip
                elif param_type == "ip_single" and param_value == "nat_pool":
                    key = service_parameter.get("name")
                    fip_maps = (
                        self._get_ptg_policy_fip_mapping(
                            context._plugin_context,
                            provider_ptg_id))
                    servicepolicy_fip_ids = []
                    for fip_map in fip_maps:
                        servicepolicy_fip_ids.append(fip_map.floatingip_id)
                    config_param_values[key] = servicepolicy_fip_ids
        name = 'gbp_%s_%s' % (policy_rule_set['name'], provider_ptg['name'])

        attrs = {'tenant_id': p_ctx.project_id,
                 'name': name,
                 'description': "",
                 'servicechain_specs': sc_spec,
                 'provider_ptg_id': provider_ptg_id,
                 'consumer_ptg_id': SCI_CONSUMER_NOT_AVAILABLE,
                 'management_ptg_id': None,
                 'classifier_id': classifier_id,
                 'config_param_values': jsonutils.dumps(config_param_values)}
        context.provider_context = p_ctx
        context.servicechain_attrs = attrs
        sc_instance = super(
            ChainMappingDriver, self)._create_servicechain_instance(
                p_ctx, attrs)
        context._plugin_context.servicechain_instance = sc_instance
        self._set_ptg_servicechain_instance_mapping(
            session, provider_ptg_id, SCI_CONSUMER_NOT_AVAILABLE,
            sc_instance['id'], p_ctx.project_id)
        return sc_instance

    def _set_ptg_servicechain_instance_mapping(self, session, provider_ptg_id,
                                               consumer_ptg_id,
                                               servicechain_instance_id,
                                               provider_tenant_id):
        with session.begin(subtransactions=True):
            mapping = PtgServiceChainInstanceMapping(
                provider_ptg_id=provider_ptg_id,
                consumer_ptg_id=consumer_ptg_id,
                servicechain_instance_id=servicechain_instance_id,
                tenant_id=provider_tenant_id)
            session.add(mapping)

    def _get_ptg_servicechain_mapping(self, session, provider_ptg_id=None,
                                      consumer_ptg_id=None, tenant_id=None,
                                      servicechain_instance_id=None,
                                      provider_ptg_ids=None):
        with session.begin(subtransactions=True):
            query = session.query(PtgServiceChainInstanceMapping)
            if provider_ptg_id:
                query = query.filter_by(provider_ptg_id=provider_ptg_id)
            elif provider_ptg_ids:
                query = query.filter(
                    PtgServiceChainInstanceMapping.provider_ptg_id.in_(
                        list(provider_ptg_ids)))
            if consumer_ptg_id:
                query = query.filter_by(consumer_ptg_id=consumer_ptg_id)
            if servicechain_instance_id:
                query = query.filter_by(
                    servicechain_instance_id=servicechain_instance_id)
            if tenant_id:
                query = query.filter_by(consumer_ptg_id=tenant_id)
            all = query.all()
            return [utils.DictClass([('provider_ptg_id', x.provider_ptg_id),
                                     ('consumer_ptg_id', x.consumer_ptg_id),
                                     ('servicechain_instance_id',
                                      x.servicechain_instance_id),
                                     ('tenant_id', x.tenant_id)])
                    for x in all]

    def _get_chain_admin_context(self, plugin_context, tenant_id=None,
                                 provider_tenant_id=None, instance_id=None):
        ctx = plugin_context.elevated()
        # REVISIT(Ivar): Any particular implication when a provider owned PT
        # exist in the consumer PTG? Especially when the consumer PTG belongs
        # to another tenant? We may want to consider a strong convention
        # for reference plumbers to absolutely avoid this kind of inter tenant
        # object creation when the owner is the provider (in which case, the
        # context can as well be a normal context without admin capabilities).
        ctx.tenant_id = None
        if instance_id:
            cmap = self._get_ptg_servicechain_mapping(
                ctx.session, servicechain_instance_id=instance_id)
            if cmap:
                ctx.tenant_id = cmap[0].tenant_id
        if not self.chain_owner:
            self.chain_owner = ChainMappingDriver.chain_tenant_id(reraise=True)
        if not ctx.tenant_id:
            ctx.tenant_id = tenant_id or self.chain_owner or provider_tenant_id
        if self.chain_owner == ctx.tenant_id:
            ctx.auth_token = self.chain_tenant_keystone_client().get_token(
                self.chain_owner)
        return ctx

    def _is_redirect_in_policy_rule_sets(self, context, policy_rule_sets):
        policy_rule_ids = []
        for prs in context._plugin.get_policy_rule_sets(
                context._plugin_context, filters={'id': policy_rule_sets}):
            policy_rule_ids.extend(prs['policy_rules'])
        for rule in context._plugin.get_policy_rules(
                context._plugin_context, filters={'id': policy_rule_ids}):
            redirect_actions = context._plugin.get_policy_actions(
                        context._plugin_context,
                        filters={'id': rule["policy_actions"],
                                 'action_type': [gconst.GP_ACTION_REDIRECT]})
            if redirect_actions:
                return True
        return False

    def _get_redirect_action(self, context, policy_rule):
        for action in context._plugin.get_policy_actions(
                context._plugin_context,
                filters={'id': policy_rule['policy_actions']}):
            if action['action_type'] == gconst.GP_ACTION_REDIRECT:
                return action

    def _validate_new_prs_redirect(self, context, prs):
        if self._prss_redirect_rules(context._plugin_context.session,
                                     [prs['id']]) > 1:
            raise exc.MultipleRedirectActionsNotSupportedForPRS()
        for ptg in context._plugin.get_policy_target_groups(
                context._plugin_context,
                {'id': prs['providing_policy_target_groups']}):
            self._validate_ptg_prss(context, ptg)

    def _prss_redirect_rules(self, session, prs_ids):
        if len(prs_ids) == 0:
            # No result will be found in this case
            return 0
        query = (session.query(gpdb.gpdb.PolicyAction).
                 join(gpdb.gpdb.PolicyRuleActionAssociation).
                 join(gpdb.gpdb.PolicyRule).
                 join(gpdb.gpdb.PRSToPRAssociation).
                 filter(
                 gpdb.gpdb.PRSToPRAssociation.policy_rule_set_id.in_(prs_ids)).
                 filter(gpdb.gpdb.PolicyAction.action_type ==
                        gconst.GP_ACTION_REDIRECT))
        return query.count()

    def _multiple_pr_redirect_action_number(self, session, pr_ids):
        # Given a set of rules, gives the total number of redirect actions
        # found
        if len(pr_ids) == 0:
            # No result will be found in this case
            return 0
        return (session.query(gpdb.gpdb.PolicyAction).
                join(gpdb.gpdb.PolicyRuleActionAssociation).
                filter(
                gpdb.gpdb.PolicyRuleActionAssociation.policy_rule_id.in_(
                    pr_ids)).
                filter(gpdb.gpdb.PolicyAction.action_type ==
                       gconst.GP_ACTION_REDIRECT)).count()

    def _reject_shared(self, object, type):
        if object.get('shared'):
            raise exc.InvalidSharedResource(type=type,
                                            driver='chain_mapping')

    def _reject_multiple_redirects_in_rule(self, context):
        policy_actions = context._plugin.get_policy_actions(
                context._plugin_context,
                filters={'id': context.current['policy_actions'],
                         'action_type': [gconst.GP_ACTION_REDIRECT]})
        if len(policy_actions) > 1:
            raise exc.MultipleRedirectActionsNotSupportedForRule()

    def _reject_multiple_redirects_in_prs(self, context):
        policy_rules = context._plugin.get_policy_rules(
                context._plugin_context,
                filters={'id': context.current['policy_rules']})
        redirect_actions_list = []
        for policy_rule in policy_rules:
            policy_actions = context._plugin.get_policy_actions(
                    context._plugin_context,
                    filters={'id': policy_rule['policy_actions'],
                             'action_type': [gconst.GP_ACTION_REDIRECT]})
            redirect_actions_list.extend(policy_actions)
        if len(redirect_actions_list) > 1:
            raise exc.MultipleRedirectActionsNotSupportedForPRS()

    def _validate_ptg_prss(self, context, ptg):
        # If the PTG is providing a redirect PRS, it can't provide any more
        # redirect rules
        if self._is_group_chainable(context, ptg):
            if self._prss_redirect_rules(context._plugin_context.session,
                                         ptg['provided_policy_rule_sets']) > 1:
                raise exc.PTGAlreadyProvidingRedirectPRS(ptg_id=ptg['id'])

    def _handle_classifier_update_notification(self, context):
        # Invoke Service chain update notify hook if protocol or port or
        # direction is updated. The SC side will have to reclassify the chain
        # and update the traffic steering programming
        if (context.original['port_range'] != context.current['port_range'] or
            context.original['protocol'] != context.current['protocol'] or
            context.original['direction'] != context.current['direction']):
            sc_instances = (
                self._servicechain_plugin.get_servicechain_instances(
                    context._plugin_context.elevated(),
                    filters={'classifier_id': [context.current['id']]}))
            for sc_instance in sc_instances:
                cmap = self._get_ptg_servicechain_mapping(
                    context._plugin_context.session,
                    servicechain_instance_id=sc_instance['id'])
                ctx = self._get_chain_admin_context(context._plugin_context,
                                                    cmap[0].tenant_id)
                self._servicechain_plugin.notify_chain_parameters_updated(
                    ctx, sc_instance['id'])

    def _stash_ptg_modified_chains(self, context):
        # Update service chain instance when any ruleset is changed
        orig_provided_policy_rule_sets = context.original[
            'provided_policy_rule_sets']
        curr_provided_policy_rule_sets = context.current[
            'provided_policy_rule_sets']

        removed_provided_prs = (set(orig_provided_policy_rule_sets) -
                                set(curr_provided_policy_rule_sets))
        added_provided_prs = (set(curr_provided_policy_rule_sets) -
                              set(orig_provided_policy_rule_sets))
        context.ptg_chain_map = []
        # If the Redirect is removed, delete the chain. If the spec is
        # changed, then update the existing instance with new spec
        if (self._is_redirect_in_policy_rule_sets(
                context, removed_provided_prs) and not
            self._is_redirect_in_policy_rule_sets(
                context, added_provided_prs)):
            context.ptg_chain_map += self._get_ptg_servicechain_mapping(
                context._plugin_context.session, context.current['id'])

    def _get_chains_by_prs(self, context, prs_ids):
        # REVISIT(ivar): only works under the assumption that only -one- chain
        # can be provided by a given group. A more direct way of retrieving
        # this info must be implemented before we drop this limitation
        result = set()
        for prs in self._get_policy_rule_sets(
                context._plugin_context.elevated(), {'id': prs_ids}):
            if prs['providing_policy_target_groups']:
                result |= set(
                    [x.servicechain_instance_id for x in
                     self._get_ptg_servicechain_mapping(
                         context._plugin_context.session,
                         provider_ptg_ids=prs[
                             'providing_policy_target_groups'])])
        return result

    def _is_group_chainable(self, context, group):
        """Determines whether a group should trigger a chain.

        Non chainable groups:
        - Proxy groups;

        :param context:
        :param group:
        :return: boolean
        """
        return (not group.get('proxied_group_id') and
                group.get('enforce_service_chains', True) is True)

    def _get_ptg_provided_chain_id(self, context, ptg):
        try:
            return [x.servicechain_instance_id for x in
                    self._get_ptg_servicechain_mapping(
                        context._plugin_context.session,
                        provider_ptg_ids=[ptg['id']])][0]
        except IndexError:
            return None

    def _handle_provider_updated(self, context):
        sci = self._get_ptg_provided_chain_id(context, context.current)
        if sci:
            chain_context = self._get_chain_admin_context(
                context._plugin_context, instance_id=sci)
            self._notify_ptg_updated(chain_context, context.original,
                                     context.current, sci)
