# Copyright (c) 2013 OpenStack Foundation
# All Rights Reserved.
#
#    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 neutron_lib.api.definitions import external_net as extnet_apidef
from neutron_lib.api.definitions import multiprovidernet as mpnet_apidef
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as provider
from neutron_lib.api import validators
from neutron_lib import constants
from neutron_lib.db import api as db_api
from neutron_lib import exceptions as exc
from neutron_lib.exceptions import multiprovidernet as mpnet_exc
from neutron_lib.exceptions import placement as place_exc
from neutron_lib.exceptions import vlantransparent as vlan_exc
from neutron_lib.plugins.ml2 import api
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
import stevedore

from neutron._i18n import _
from neutron.conf.plugins.ml2 import config
from neutron.db import segments_db
from neutron.objects import ports
from neutron.plugins.ml2.common import exceptions as ml2_exc

LOG = log.getLogger(__name__)

MAX_BINDING_LEVELS = 10
config.register_ml2_plugin_opts()


class TypeManager(stevedore.named.NamedExtensionManager):
    """Manage network segment types using drivers."""

    def __init__(self):
        # Mapping from type name to DriverManager
        self.drivers = {}

        LOG.info("Configured type driver names: %s",
                 cfg.CONF.ml2.type_drivers)
        super(TypeManager, self).__init__('neutron.ml2.type_drivers',
                                          cfg.CONF.ml2.type_drivers,
                                          invoke_on_load=True)
        LOG.info("Loaded type driver names: %s", self.names())
        self._register_types()
        self._check_tenant_network_types(cfg.CONF.ml2.tenant_network_types)
        self._check_external_network_type(cfg.CONF.ml2.external_network_type)

    def _register_types(self):
        for ext in self:
            network_type = ext.obj.get_type()
            if network_type in self.drivers:
                LOG.error("Type driver '%(new_driver)s' ignored because"
                          " type driver '%(old_driver)s' is already"
                          " registered for type '%(type)s'",
                          {'new_driver': ext.name,
                           'old_driver': self.drivers[network_type].name,
                           'type': network_type})
            else:
                self.drivers[network_type] = ext
        LOG.info("Registered types: %s", self.drivers.keys())

    def _check_tenant_network_types(self, types):
        self.tenant_network_types = []
        for network_type in types:
            if network_type in self.drivers:
                self.tenant_network_types.append(network_type)
            else:
                LOG.error("No type driver for tenant network_type: %s. "
                          "Service terminated!", network_type)
                raise SystemExit(1)
        LOG.info("Tenant network_types: %s", self.tenant_network_types)

    def _check_external_network_type(self, ext_network_type):
        if ext_network_type and ext_network_type not in self.drivers:
            LOG.error("No type driver for external network_type: %s. "
                      "Service terminated!", ext_network_type)
            raise SystemExit(1)

    def _process_provider_segment(self, segment):
        (network_type, physical_network,
         segmentation_id) = (self._get_attribute(segment, attr)
                             for attr in provider.ATTRIBUTES)

        if validators.is_attr_set(network_type):
            segment = {api.NETWORK_TYPE: network_type,
                       api.PHYSICAL_NETWORK: physical_network,
                       api.SEGMENTATION_ID: segmentation_id}
            self.validate_provider_segment(segment)
            return segment

        msg = _("network_type required")
        raise exc.InvalidInput(error_message=msg)

    def _process_provider_create(self, network):
        if any(validators.is_attr_set(network.get(attr))
               for attr in provider.ATTRIBUTES):
            # Verify that multiprovider and provider attributes are not set
            # at the same time.
            if validators.is_attr_set(network.get(mpnet_apidef.SEGMENTS)):
                raise mpnet_exc.SegmentsSetInConjunctionWithProviders()
            segment = self._get_provider_segment(network)
            return [self._process_provider_segment(segment)]
        elif validators.is_attr_set(network.get(mpnet_apidef.SEGMENTS)):
            segments = [self._process_provider_segment(s)
                        for s in network[mpnet_apidef.SEGMENTS]]
            mpnet_apidef.check_duplicate_segments(
                segments, self.is_partial_segment)
            return segments

    def _match_segment(self, segment, filters):
        return all(not filters.get(attr) or segment.get(attr) in filters[attr]
                   for attr in provider.ATTRIBUTES)

    def _get_provider_segment(self, network):
        # TODO(manishg): Placeholder method
        # Code intended for operating on a provider segment should use
        # this method to extract the segment, even though currently the
        # segment attributes are part of the network dictionary. In the
        # future, network and segment information will be decoupled and
        # here we will do the job of extracting the segment information.
        return network

    def network_matches_filters(self, network, filters):
        if not filters:
            return True
        if any(validators.is_attr_set(network.get(attr))
               for attr in provider.ATTRIBUTES):
            segments = [self._get_provider_segment(network)]
        elif validators.is_attr_set(network.get(mpnet_apidef.SEGMENTS)):
            segments = self._get_attribute(network, mpnet_apidef.SEGMENTS)
        else:
            return True
        return any(self._match_segment(s, filters) for s in segments)

    def _get_attribute(self, attrs, key):
        value = attrs.get(key)
        if value is constants.ATTR_NOT_SPECIFIED:
            value = None
        return value

    def extend_network_dict_provider(self, context, network):
        # this method is left for backward compat even though it would be
        # easy to change the callers in tree to use the bulk function
        return self.extend_networks_dict_provider(context, [network])

    def extend_networks_dict_provider(self, context, networks):
        ids = [network['id'] for network in networks]
        net_segments = segments_db.get_networks_segments(context, ids)
        for network in networks:
            segments = net_segments[network['id']]
            self._extend_network_dict_provider(network, segments)

    def _extend_network_dict_provider(self, network, segments):
        if not segments:
            LOG.debug("Network %s has no segments", network['id'])
            for attr in provider.ATTRIBUTES:
                network[attr] = None
        elif len(segments) > 1:
            network[mpnet_apidef.SEGMENTS] = [
                {provider.NETWORK_TYPE: segment[api.NETWORK_TYPE],
                 provider.PHYSICAL_NETWORK: segment[api.PHYSICAL_NETWORK],
                 provider.SEGMENTATION_ID: segment[api.SEGMENTATION_ID]}
                for segment in segments]
        else:
            segment = segments[0]
            network[provider.NETWORK_TYPE] = segment[api.NETWORK_TYPE]
            network[provider.PHYSICAL_NETWORK] = segment[
                api.PHYSICAL_NETWORK]
            network[provider.SEGMENTATION_ID] = segment[
                api.SEGMENTATION_ID]

    def initialize(self):
        for network_type, driver in self.drivers.items():
            LOG.info("Initializing driver for type '%s'", network_type)
            driver.obj.initialize()

    def initialize_network_segment_range_support(self):
        for network_type, driver in self.drivers.items():
            if network_type in constants.NETWORK_SEGMENT_RANGE_TYPES:
                LOG.info("Initializing driver network segment range support "
                         "for type '%s'", network_type)
                driver.obj.initialize_network_segment_range_support()

    def _add_network_segment(self, context, network_id, segment,
                             segment_index=0):
        segments_db.add_network_segment(
            context, network_id, segment, segment_index)

    def _update_network_segment(self, context, network_id, segmentation_id):
        segments_db.update_network_segment(
            context, network_id, segmentation_id)

    def create_network_segments(self, context, network, tenant_id):
        """Call type drivers to create network segments."""
        segments = self._process_provider_create(network)
        filters = {'project_id': tenant_id}
        with db_api.CONTEXT_WRITER.using(context):
            network_id = network['id']
            if segments:
                for segment_index, segment in enumerate(segments):
                    segment = self.reserve_provider_segment(
                        context, segment, filters=filters)
                    self._add_network_segment(context, network_id, segment,
                                              segment_index)
            elif (cfg.CONF.ml2.external_network_type and
                  self._get_attribute(network, extnet_apidef.EXTERNAL)):
                segment = self._allocate_ext_net_segment(
                    context, filters=filters)
                self._add_network_segment(context, network_id, segment)
            else:
                segment = self._allocate_tenant_net_segment(
                    context, filters=filters)
                self._add_network_segment(context, network_id, segment)

    def update_network_segment(self, context, network, net_data, segment):
        """Call type drivers to update a network segment.

        Update operation is currently only supported for VLAN type segments,
        and only the SEGMENTATION_ID field can be changed.
        """
        project_id = network['project_id']
        segmentation_id = net_data.get(provider.SEGMENTATION_ID)
        network_type = segment[api.NETWORK_TYPE]
        if network_type != constants.TYPE_VLAN:
            msg = (_('Only VLAN type networks can be updated.'))
            raise exc.InvalidInput(error_message=msg)
        if not segmentation_id:
            msg = (_('Only %s field can be updated in VLAN type networks') %
                   api.SEGMENTATION_ID)
            raise exc.InvalidInput(error_message=msg)

        new_segment = {api.NETWORK_TYPE: segment[api.NETWORK_TYPE],
                       api.PHYSICAL_NETWORK: segment[api.PHYSICAL_NETWORK],
                       api.SEGMENTATION_ID: segmentation_id}
        self.validate_provider_segment(new_segment)
        self.reserve_provider_segment(context, new_segment,
                                      filters={'project_id': project_id})
        self._update_network_segment(context, segment['id'], segmentation_id)
        self.release_network_segment(context, segment)

    def reserve_network_segment(self, context, segment_data):
        """Call type drivers to reserve a network segment."""
        # Validate the data of segment
        if not validators.is_attr_set(segment_data[api.NETWORK_TYPE]):
            msg = _("network_type required")
            raise exc.InvalidInput(error_message=msg)

        net_type = self._get_attribute(segment_data, api.NETWORK_TYPE)
        phys_net = self._get_attribute(segment_data, api.PHYSICAL_NETWORK)
        seg_id = self._get_attribute(segment_data, api.SEGMENTATION_ID)
        segment = {api.NETWORK_TYPE: net_type,
                   api.PHYSICAL_NETWORK: phys_net,
                   api.SEGMENTATION_ID: seg_id}

        self.validate_provider_segment(segment)

        # Reserve segment in type driver
        with db_api.CONTEXT_WRITER.using(context):
            return self.reserve_provider_segment(context, segment)

    def is_partial_segment(self, segment):
        network_type = segment[api.NETWORK_TYPE]
        driver = self.drivers.get(network_type)