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

from neutron import manager
from neutron.openstack.common import lockutils  # noqa
from neutron.openstack.common import log as logging
from neutron.plugins.common import constants

from gbpservice.neutron.db.grouppolicy import group_policy_mapping_db as gpdb
from gbpservice.neutron.services.grouppolicy.common import constants as g_const
from gbpservice.neutron.services.grouppolicy.common import exceptions as gpexc
from gbpservice.neutron.services.grouppolicy.drivers import (
    resource_mapping as api)
from gbpservice.neutron.services.grouppolicy.drivers.odl import odl_manager


LOG = logging.getLogger(__name__)


class ExternalSegmentNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("External Segment currently not supported on ODL GBP "
                "driver.")


class UpdateL3PolicyNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update L3 Policy currently not supported on ODL GBP "
                "driver.")


class UpdateL2PolicyNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update L2 Policy currently not supported on ODL GBP "
                "driver.")


class UpdatePTNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update Policy Target currently not supported on ODL GBP "
                "driver.")


class UpdatePTGNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update Policy Target Group currently not supported on ODL "
                "GBP driver.")


class L2PolicyMultiplePolicyTargetGroupNotSupportedOnOdlDriver(
        gpexc.GroupPolicyBadRequest):
    message = _("An L2 policy can't have multiple policy target groups on "
                "ODL GBP driver.")


class UpdatePolicyActionNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update Policy Action currently not supported on ODL GBP "
                "driver.")


class RedirectActionNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Redirect action is currently not supported for ODL GBP "
                "driver.")


class OnlyAllowActionSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Currently only allow action is supported for ODL GBP "
                "driver.")


class UpdateClassifierNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Update Policy Classifier currently not supported on ODL GBP "
                "driver.")


class PolicyRuleUpdateNotSupportedOnOdlDriver(gpexc.GroupPolicyBadRequest):
    message = _("Policy rule update is not supported on for ODL GBP"
                "driver.")


class ExactlyOneActionPerRuleIsSupportedOnOdlDriver(
        gpexc.GroupPolicyBadRequest):
    message = _("Exactly one action per rule is supported on ODL GBP driver.")


class ClassifierTcpUdpPortRangeNotSupportedOnOdlDriver(
        gpexc.GroupPolicyBadRequest):
    message = _("Tcp or Udp port range is not supported on ODL GBP driver.")


class ClassifierUnknownIPProtocolNotSupportedOnOdlDriver(
        gpexc.GroupPolicyBadRequest):
    message = _("Unknown IP Protocol is not supported on ODL GBP driver.")


class OdlMappingDriver(api.ResourceMappingDriver):
    """ODL Mapping driver for Group Policy plugin.

    This driver implements group policy semantics by mapping group
    policy resources to various other neutron resources, and leverages
    ODL backend for enforcing the policies.
    """

    me = None
    manager = None

    @staticmethod
    def get_odl_manager():
        if not OdlMappingDriver.manager:
            OdlMappingDriver.manager = odl_manager.OdlManager()
        return OdlMappingDriver.manager

    def initialize(self):
        super(OdlMappingDriver, self).initialize()
        self.odl_manager = OdlMappingDriver.get_odl_manager()
        self._gbp_plugin = None
        OdlMappingDriver.me = self

    @property
    def gbp_plugin(self):
        if not self._gbp_plugin:
            self._gbp_plugin = (manager.NeutronManager.get_service_plugins()
                                .get("GROUP_POLICY"))
        return self._gbp_plugin

    @staticmethod
    def get_initialized_instance():
        return OdlMappingDriver.me

    def create_dhcp_policy_target_if_needed(self, plugin_context, port):
        session = plugin_context.session
        if (self._port_is_owned(session, port['id'])):
            # Nothing to do
            return

        # Retrieve PTG
        # TODO(ywu): optimize later
        subnets = self._core_plugin._get_subnets_by_network(
            plugin_context, port['network_id']
        )
        ptg = (plugin_context.session.query(gpdb.PolicyTargetGroupMapping).
               join(gpdb.PolicyTargetGroupMapping.subnets).
               filter(gpdb.PTGToSubnetAssociation.subnet_id ==
                      subnets[0]['id']).
               first())

        # Create PolicyTarget
        attrs = {'policy_target':
                 {'tenant_id': port['tenant_id'],
                  'name': 'dhcp-%s' % ptg['id'],
                  'description': ("Implicitly created DHCP policy "
                                  "target"),
                  'policy_target_group_id': ptg['id'],
                  'port_id': port['id']}}
        self.gbp_plugin.create_policy_target(plugin_context, attrs)
        # TODO(ODL): security group is not required
        # sg_id = self._ensure_default_security_group(plugin_context,
        #                                            port['tenant_id'])
        # data = {'port': {'security_groups': [sg_id]}}
        # self._core_plugin.update_port(plugin_context, port['id'], data)

    def create_external_segment_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def update_external_segment_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def delete_external_segment_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def create_external_policy_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def update_external_policy_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def delete_external_policy_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def create_nat_pool_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def update_nat_pool_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def delete_nat_pool_precommit(self, context):
        raise ExternalSegmentNotSupportedOnOdlDriver()

    def create_policy_target_postcommit(self, context):
        super(OdlMappingDriver, self).create_policy_target_postcommit(context)
        pt = self._get_pt_detail(context)
        ep = {
            "endpoint-group": pt['ptg_id'],
            "l2-context": pt['l2ctx_id'],
            "l3-address": pt['l3_list'],
            "mac-address": pt['mac_address'],
            "port-name": pt['neutron_port_id'],
            "tenant": pt['tenant_id']
        }
        self.odl_manager.register_endpoints([ep])

    def update_policy_target_precommit(self, context):
        raise UpdatePTNotSupportedOnOdlDriver()

    def delete_policy_target_postcommit(self, context):
        pt = self._get_pt_detail(context)
        ep = {
            "l2": pt['l2_list'],
            "l3": pt['l3_list']
        }
        self.odl_manager.unregister_endpoints([ep])
        # Delete Neutron's port
        super(OdlMappingDriver, self).delete_policy_target_postcommit(context)

    def create_l3_policy_postcommit(self, context):
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        l3ctx = {
            "id": context.current['id'],
            "name": context.current['name'],
            "description": context.current['description']
        }
        self.odl_manager.create_update_l3_context(tenant_id, l3ctx)

    def update_l3_policy_precommit(self, context):
        raise UpdateL3PolicyNotSupportedOnOdlDriver()

    def delete_l3_policy_postcommit(self, context):
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        l3ctx = {
            "id": context.current['id']
        }
        self.odl_manager.delete_l3_context(tenant_id, l3ctx)

    def create_l2_policy_postcommit(self, context):
        super(OdlMappingDriver, self).create_l2_policy_postcommit(context)
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]

        # l2_policy mapped to l2_bridge_domain in ODL
        l2bd = {
            "id": context.current['id'],
            "name": context.current['name'],
            "description": context.current['description'],
            "parent": context.current['l3_policy_id']
        }
        self.odl_manager.create_update_l2_bridge_domain(tenant_id, l2bd)

        # Implicit network within l2 policy mapped to l2 FD in ODL
        net_id = context.current['network_id']
        network = self._core_plugin.get_network(context._plugin_context,
                                                net_id)
        l2fd = {
            "id": net_id,
            "name": network['name'],
            "parent": context.current['id']
        }
        self.odl_manager.create_update_l2_flood_domain(tenant_id, l2fd)

    def update_l2_policy_precommit(self, context):
        raise UpdateL2PolicyNotSupportedOnOdlDriver()

    def delete_l2_policy_postcommit(self, context):
        super(OdlMappingDriver, self).delete_l2_policy_postcommit(context)
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]

        # l2_policy mapped to l2_bridge_domain in ODL
        l2bd = {
            "id": context.current['id']
        }
        self.odl_manager.delete_l2_bridge_domain(tenant_id, l2bd)

        # Implicit network within l2 policy mapped to l2 FD in ODL
        net_id = context.current['network_id']
        l2fd = {
            "id": net_id,
        }
        self.odl_manager.delete_l2_flood_domain(tenant_id, l2fd)

    def create_policy_target_group_postcommit(self, context):
        super(OdlMappingDriver, self).create_policy_target_group_postcommit(
            context)
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        subnets = context.current['subnets']
        provided_contract = self._make_odl_contract_and_clause(
            context, context.current['provided_policy_rule_sets']
        )
        consumed_contract = self._make_odl_contract_and_clause(
            context, context.current['consumed_policy_rule_sets']
        )

        # PTG mapped to EPG in ODL
        # TODO(ODL); add back description field after PoC
        epg = {
            "id": context.current['id'],
            "name": context.current['name'],
            "network-domain": subnets[0]
        }

        if provided_contract:
            epg['provider-named-selector'] = {
                "name": 'Contract-' + provided_contract['id'],
                "contract": provided_contract['id']
            }
            self.odl_manager.create_update_contract(tenant_id,
                                                    provided_contract)
        if consumed_contract:
            epg['consumer-named-selector'] = {
                "name": 'Contract-' + consumed_contract['id'],
                "contract": consumed_contract['id']
            }
            self.odl_manager.create_update_contract(tenant_id,
                                                    consumed_contract)

        self.odl_manager.create_update_endpoint_group(tenant_id, epg)

        # Implicit subnet within policy target group mapped to subnet in ODL
        for subnet_id in subnets:
            neutron_subnet = self._core_plugin.get_subnet(
                context._plugin_context, subnet_id
            )
            odl_subnet = {
                "id": subnet_id,
                "ip-prefix": neutron_subnet['cidr'],
                "parent": neutron_subnet['network_id'],
                "virtual-router-ip": neutron_subnet['gateway_ip']
            }
            self.odl_manager.create_update_subnet(tenant_id, odl_subnet)

    def _make_odl_contract_and_clause(self, context, rule_sets):
        # As no contract/clause in O.S., they will be generated dynamically
        # when rule sets are associated with PTG:
        # 1. an association is mapped to a contract with single clause
        # 2. rule sets mapped to subjects
        # 3. clause name is concatenation of sorted subject names
        # 4. contract ID is generated based on the clause name
        # 5. As a combination of same rule sets produce same sorted subject
        #    names, consistent clause name and contract ID are guaranteed
        contract = None
        if rule_sets:
            subjects = []
            subject_names = []
            for rule_set_id in rule_sets:
                # a subject is mapped to a rule set
                subject = self._make_subject(context, rule_set_id)
                subjects.append(subject)
                subject_names.append(subject['name'])
            clause_name = "-".join(sorted(subject_names)).encode('ascii',
                                                                 'ignore')
            contract_id = uuid.uuid3(uuid.NAMESPACE_DNS, clause_name).urn[9:]
            clauses = [
                {
                    "name": clause_name,
                    "subject-refs": subject_names
                }
            ]
            contract = {
                "id": contract_id,
                "clause": clauses,
                "subject": subjects
            }
        return contract

    def _make_subject(self, context, rule_set_id):
        rule_set = context._plugin.get_policy_rule_set(
            context._plugin_context, rule_set_id
        )
        rules = []
        for rule_id in rule_set['policy_rules']:
            rule = self._make_odl_rule(context, rule_id)
            rules.append(rule)
        return {
            "name": rule_set['name'],
            "rule": rules
        }

    def _make_odl_rule(self, context, rule_id):
        rule = context._plugin.get_policy_rule(
            context._plugin_context, rule_id
        )
        stack_classifier = context._plugin.get_policy_classifier(
            context._plugin_context, rule['policy_classifier_id']
        )

        # while openstack supports only one classifier per rule, the classifier
        # may mapped to multi classifier in ODL
        classifier_refs = []
        classifiers = self._make_odl_classifiers(stack_classifier)
        for classifier in classifiers:
            classifier_ref = {
                "name": classifier['name']
            }
            if classifier['direction'] != "bidirectional":
                classifier_ref['direction'] = classifier['direction']
            classifier_refs.append(classifier_ref)
        action_refs = []
        for action_id in rule['policy_actions']:
            action = context._plugin.get_policy_action(
                context._plugin_context, action_id
            )
            action_refs.append(
                {
                    "name": action['name']
                }
            )

        # TODO(ODL): send action_refs later but not for PoC
        return {
            "name": rule['name'],
            "classifier-ref": classifier_refs,
        }

    def update_policy_target_group_precommit(self, context):
        raise UpdatePTGNotSupportedOnOdlDriver()

    def delete_policy_target_group_postcommit(self, context):
        # TODO(ODL): delete contract if no one uses it
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        subnets = context.current['subnets']

        # delete mapped subnets in ODL, and clean them up from neutron
        for subnet_id in subnets:
            self._cleanup_subnet(context._plugin_context, subnet_id, None)
            odl_subnet = {
                "id": subnet_id
            }
            self.odl_manager.delete_subnet(tenant_id, odl_subnet)

        # delete mapped EPG in ODL
        epg = {
            "id": context.current['id'],
        }
        self.odl_manager.delete_endpoint_group(tenant_id, epg)

    def create_policy_action_precommit(self, context):
        # TODO(odl): allow redirect for service chaining
        if context.current['action_type'] == g_const.GP_ACTION_REDIRECT:
            raise RedirectActionNotSupportedOnOdlDriver()

    def create_policy_action_postcommit(self, context):
        super(OdlMappingDriver, self).create_policy_action_postcommit(context)
        # TODO(ODL): remove comment out after PoC
        # tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]

        # fill in action instance data
        if context.current['action_type'] == g_const.GP_ACTION_ALLOW:
            # TODO(ODL): remove the return and comment out after POC
            return
            # action_definition_id = "f942e8fd-e957-42b7-bd18-f73d11266d17"
            # action_instance = {
            #     "action-definition-id": action_definition_id,
            #     "name": context.current['name'],
            #     "parameter-value": [
            #         {
            #             "name": context.current['name'],
            #             "string-value": context.current['action_type'],
            #         }
            #     ]
            # }
            # self.odl_manager.create_action(tenant_id, action_instance)
        else:
            raise OnlyAllowActionSupportedOnOdlDriver()

    def update_policy_action_precommit(self, context):
        raise UpdatePolicyActionNotSupportedOnOdlDriver()

    def delete_policy_action_postcommit(self, context):
        super(OdlMappingDriver, self).delete_policy_action_postcommit(context)
        # TODO(ODL): remove comment out after PoC
        # tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        #
        # # fill in action instance data
        # action_instance = {
        #     "name": context.current['name']
        # }
        # self.odl_manager.delete_action(tenant_id, action_instance)

    def create_policy_classifier_postcommit(self, context):
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        classifiers = self._make_odl_classifiers(context.current)

        for classifier in classifiers:
            classifier_instance = {
                "classifier-definition-id":
                    classifier['classifier-definition-id'],
                "name": classifier['name'],
                "parameter-value": classifier['parameter-value']
            }
            self.odl_manager.create_classifier(tenant_id, classifier_instance)

    def _make_odl_classifiers(self, stack_classifier):
        classifiers = []
        if stack_classifier['protocol'] == constants.ICMP:
            direction = stack_classifier['direction']
            if direction == 'bi':
                direction = "bidirectional"
            classifier = {
                # Use hard coded value based on current ODL implementation
                "classifier-definition-id":
                    '79c6fdb2-1e1a-4832-af57-c65baf5c2335',
                "name": stack_classifier['name'],
                "parameter-value": [
                    {
                        "name": "proto",
                        # TODO(yapeng): change the hard code value
                        "int-value": 1,
                    }
                ],
                "direction": direction
            }
            classifiers.append(classifier)
        else:
            # For TCP and UDP protoocol create two classifier (in and out)
            for port in ['sourceport', 'destport']:
                if stack_classifier['direction'] == 'in':
                    if port == 'destport':
                        direction = 'in'
                    else:
                        direction = 'out'
                elif stack_classifier['direction'] == 'out':
                    if port == 'destport':
                        direction = 'out'
                    else:
                        direction = 'in'
                else:
                    direction = 'bidirectional'

                classifier = {
                    # Use hard coded value based on current ODL implementation
                    "classifier-definition-id":
                        '4250ab32-e8b8-445a-aebb-e1bd2cdd291f',
                    "direction": direction,
                    "name": stack_classifier['name'] + '-' + port,
                    "parameter-value": [
                        {
                            "name": "type",
                            "string-value": stack_classifier['protocol'],
                        },
                        {
                            "name": port,
                            "int-value": stack_classifier['port_range'],
                        }
                    ]
                }
                classifiers.append(classifier)
        return classifiers

    def update_policy_classifier_precommit(self, context):
        raise UpdateClassifierNotSupportedOnOdlDriver()

    def delete_policy_classifier_postcommit(self, context):
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]

        if context.current['protocol'] == constants.ICMP:
            # fill in classifier instance data
            classifier_instance = {
                "name": context.current['name']
            }
            self.odl_manager.delete_classifier(tenant_id, classifier_instance)
            return

        # fill in classifier instance data
        for port in ['sourceport', 'destport']:
            classifier_instance = {
                "name": context.current['name'] + '-' + port,
            }
            self.odl_manager.delete_classifier(tenant_id, classifier_instance)

    def create_policy_rule_precommit(self, context):
        if ('policy_actions' in context.current and
                len(context.current['policy_actions']) != 1):
            # TODO(odl): to be fixed when redirect is supported
            raise ExactlyOneActionPerRuleIsSupportedOnOdlDriver()

    def update_policy_rule_precommit(self, context):
        # TODO(ivar): add support for action update on policy rules
        raise PolicyRuleUpdateNotSupportedOnOdlDriver()

    def _get_pt_detail(self, context):
        port_id = context.current['port_id']
        port = self._core_plugin.get_port(context._plugin_context, port_id)
        tenant_id = uuid.UUID(context.current['tenant_id']).urn[9:]
        ptg_id = context.current['policy_target_group_id']
        ptg = self.gbp_plugin.get_policy_target_group(context._plugin_context,
                                                      ptg_id)
        l2ctx_id = ptg['l2_policy_id']
        l2ctx = self.gbp_plugin.get_l2_policy(context._plugin_context,
                                              l2ctx_id)
        l3ctx_id = l2ctx['l3_policy_id']
        mac_address = port['mac_address']
        neutron_port_id = 'tap' + port_id[:11]

        l3_list = []
        for fixed_ip in port['fixed_ips']:
            l3_list.append(
                {
                    "ip-address": fixed_ip['ip_address'],
                    "l3-context": l3ctx_id
                }
            )

        l2_list = [
            {
                "l2-context": l2ctx_id,
                "mac-address": mac_address
            }
        ]

        return {
            "port_id": port_id,
            "tenant_id": tenant_id,
            "ptg_id": ptg_id,
            "l2ctx_id": l2ctx_id,
            "l3ctx_id": l3ctx_id,
            "mac_address": mac_address,
            "neutron_port_id": neutron_port_id,
            "l3_list": l3_list,
            "l2_list": l2_list,
        }
