# Copyright 2013 VMware, Inc.  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.

import sqlalchemy as sa

from sqlalchemy import orm
from sqlalchemy.orm import exc as sa_orm_exc
from webob import exc as web_exc

from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.common import exceptions
from neutron.db import model_base
from neutron.db import models_v2
from neutron.openstack.common import log as logging
from neutron.openstack.common import uuidutils
from neutron.plugins.vmware.extensions import networkgw


LOG = logging.getLogger(__name__)
DEVICE_OWNER_NET_GW_INTF = 'network:gateway-interface'
NETWORK_ID = 'network_id'
SEGMENTATION_TYPE = 'segmentation_type'
SEGMENTATION_ID = 'segmentation_id'
ALLOWED_CONNECTION_ATTRIBUTES = set((NETWORK_ID,
                                     SEGMENTATION_TYPE,
                                     SEGMENTATION_ID))
# Constants for gateway device operational status
STATUS_UNKNOWN = "UNKNOWN"
STATUS_ERROR = "ERROR"
STATUS_ACTIVE = "ACTIVE"
STATUS_DOWN = "DOWN"


class GatewayInUse(exceptions.InUse):
    message = _("Network Gateway '%(gateway_id)s' still has active mappings "
                "with one or more neutron networks.")


class GatewayNotFound(exceptions.NotFound):
    message = _("Network Gateway %(gateway_id)s could not be found")


class GatewayDeviceInUse(exceptions.InUse):
    message = _("Network Gateway Device '%(device_id)s' is still used by "
                "one or more network gateways.")


class GatewayDeviceNotFound(exceptions.NotFound):
    message = _("Network Gateway Device %(device_id)s could not be found.")


class NetworkGatewayPortInUse(exceptions.InUse):
    message = _("Port '%(port_id)s' is owned by '%(device_owner)s' and "
                "therefore cannot be deleted directly via the port API.")


class GatewayConnectionInUse(exceptions.InUse):
    message = _("The specified mapping '%(mapping)s' is already in use on "
                "network gateway '%(gateway_id)s'.")


class MultipleGatewayConnections(exceptions.NeutronException):
    message = _("Multiple network connections found on '%(gateway_id)s' "
                "with provided criteria.")


class GatewayConnectionNotFound(exceptions.NotFound):
    message = _("The connection %(network_mapping_info)s was not found on the "
                "network gateway '%(network_gateway_id)s'")


class NetworkGatewayUnchangeable(exceptions.InUse):
    message = _("The network gateway %(gateway_id)s "
                "cannot be updated or deleted")

# Add exceptions to HTTP Faults mappings
base.FAULT_MAP.update({GatewayInUse: web_exc.HTTPConflict,
                       NetworkGatewayPortInUse: web_exc.HTTPConflict,
                       GatewayConnectionInUse: web_exc.HTTPConflict,
                       GatewayConnectionNotFound: web_exc.HTTPNotFound,
                       MultipleGatewayConnections: web_exc.HTTPConflict})


class NetworkConnection(model_base.BASEV2, models_v2.HasTenant):
    """Defines a connection between a network gateway and a network."""
    # We use port_id as the primary key as one can connect a gateway
    # to a network in multiple ways (and we cannot use the same port form
    # more than a single gateway)
    network_gateway_id = sa.Column(sa.String(36),
                                   sa.ForeignKey('networkgateways.id',
                                                 ondelete='CASCADE'))
    network_id = sa.Column(sa.String(36),
                           sa.ForeignKey('networks.id', ondelete='CASCADE'))
    segmentation_type = sa.Column(
        sa.Enum('flat', 'vlan',
                name='networkconnections_segmentation_type'))
    segmentation_id = sa.Column(sa.Integer)
    __table_args__ = (sa.UniqueConstraint(network_gateway_id,
                                          segmentation_type,
                                          segmentation_id),)
    # Also, storing port id comes back useful when disconnecting a network
    # from a gateway
    port_id = sa.Column(sa.String(36),
                        sa.ForeignKey('ports.id', ondelete='CASCADE'),
                        primary_key=True)


class NetworkGatewayDeviceReference(model_base.BASEV2):
    id = sa.Column(sa.String(36), primary_key=True)
    network_gateway_id = sa.Column(sa.String(36),
                                   sa.ForeignKey('networkgateways.id',
                                                 ondelete='CASCADE'),
                                   primary_key=True)
    interface_name = sa.Column(sa.String(64), primary_key=True)


class NetworkGatewayDevice(model_base.BASEV2, models_v2.HasId,
                           models_v2.HasTenant):
    nsx_id = sa.Column(sa.String(36))
    # Optional name for the gateway device
    name = sa.Column(sa.String(255))
    # Transport connector type. Not using enum as range of
    # connector types might vary with backend version
    connector_type = sa.Column(sa.String(10))
    # Transport connector IP Address
    connector_ip = sa.Column(sa.String(64))
    # operational status
    status = sa.Column(sa.String(16))


class NetworkGateway(model_base.BASEV2, models_v2.HasId,
                     models_v2.HasTenant):
    """Defines the data model for a network gateway."""
    name = sa.Column(sa.String(255))
    # Tenant id is nullable for this resource
    tenant_id = sa.Column(sa.String(36))
    default = sa.Column(sa.Boolean())
    devices = orm.relationship(NetworkGatewayDeviceReference,
                               backref='networkgateways',
                               cascade='all,delete')
    network_connections = orm.relationship(NetworkConnection, lazy='joined')


class NetworkGatewayMixin(networkgw.NetworkGatewayPluginBase):

    gateway_resource = networkgw.GATEWAY_RESOURCE_NAME
    device_resource = networkgw.DEVICE_RESOURCE_NAME

    def _get_network_gateway(self, context, gw_id):
        try:
            gw = self._get_by_id(context, NetworkGateway, gw_id)
        except sa_orm_exc.NoResultFound:
            raise GatewayNotFound(gateway_id=gw_id)
        return gw

    def _make_gw_connection_dict(self, gw_conn):
        return {'port_id': gw_conn['port_id'],
                'segmentation_type': gw_conn['segmentation_type'],
                'segmentation_id': gw_conn['segmentation_id']}

    def _make_network_gateway_dict(self, network_gateway, fields=None):
        device_list = []
        for d in network_gateway['devices']:
            device_list.append({'id': d['id'],
                                'interface_name': d['interface_name']})
        res = {'id': network_gateway['id'],
               'name': network_gateway['name'],
               'default': network_gateway['default'],
               'devices': device_list,
               'tenant_id': network_gateway['tenant_id']}
        # Query gateway connections only if needed
        if (fields and 'ports' in fields) or not fields:
            res['ports'] = [self._make_gw_connection_dict(conn)
                            for conn in network_gateway.network_connections]
        return self._fields(res, fields)

    def _set_mapping_info_defaults(self, mapping_info):
        if not mapping_info.get('segmentation_type'):
            mapping_info['segmentation_type'] = 'flat'
        if not mapping_info.get('segmentation_id'):
            mapping_info['segmentation_id'] = 0

    def _validate_network_mapping_info(self, network_mapping_info):
        self._set_mapping_info_defaults(network_mapping_info)
        network_id = network_mapping_info.get(NETWORK_ID)
        if not network_id:
            raise exceptions.InvalidInput(
                error_message=_("A network identifier must be specified "
                                "when connecting a network to a network "
                                "gateway. Unable to complete operation"))
        connection_attrs = set(network_mapping_info.keys())
        if not connection_attrs.issubset(ALLOWED_CONNECTION_ATTRIBUTES):
            raise exceptions.InvalidInput(
                error_message=(_("Invalid keys found among the ones provided "
                                 "in request body: %(connection_attrs)s."),
                               connection_attrs))
        seg_type = network_mapping_info.get(SEGMENTATION_TYPE)
        seg_id = network_mapping_info.get(SEGMENTATION_ID)
        if not seg_type and seg_id:
            msg = _("In order to specify a segmentation id the "
                    "segmentation type must be specified as well")
            raise exceptions.InvalidInput(error_message=msg)
        elif seg_type and seg_type.lower() == 'flat' and seg_id:
            msg = _("Cannot specify a segmentation id when "
                    "the segmentation type is flat")
            raise exceptions.InvalidInput(error_message=msg)
        return network_id

    def _retrieve_gateway_connections(self, context, gateway_id,
                                      mapping_info={}, only_one=False):
        filters = {'network_gateway_id': [gateway_id]}
        for k, v in mapping_info.iteritems():
            if v and k != NETWORK_ID:
                filters[k] = [v]
        query = self._get_collection_query(context,
                                           NetworkConnection,
                                           filters)
        return only_one and query.one() or query.all()

    def _unset_default_network_gateways(self, context):
        with context.session.begin(subtransactions=True):
            context.session.query(NetworkGateway).update(
                {NetworkGateway.default: False})

    def _set_default_network_gateway(self, context, gw_id):
        with context.session.begin(subtransactions=True):
            gw = (context.session.query(NetworkGateway).
                  filter_by(id=gw_id).one())
            gw['default'] = True

    def prevent_network_gateway_port_deletion(self, context, port):
        """Pre-deletion check.

        Ensures a port will not be deleted if is being used by a network
        gateway. In that case an exception will be raised.
        """
        if port['device_owner'] == DEVICE_OWNER_NET_GW_INTF:
            raise NetworkGatewayPortInUse(port_id=port['id'],
                                          device_owner=port['device_owner'])

    def create_network_gateway(self, context, network_gateway):
        gw_data = network_gateway[self.gateway_resource]
        tenant_id = self._get_tenant_id_for_create(context, gw_data)
        with context.session.begin(subtransactions=True):
            gw_db = NetworkGateway(
                id=gw_data.get('id', uuidutils.generate_uuid()),
                tenant_id=tenant_id,
                name=gw_data.get('name'))
            # Device list is guaranteed to be a valid list
            # TODO(salv-orlando): Enforce that gateway device identifiers
            # in this list are among the tenant's NSX network gateway devices
            # to avoid risk a tenant 'guessing' other tenant's network devices
            gw_db.devices.extend([NetworkGatewayDeviceReference(**device)
                                  for device in gw_data['devices']])
            context.session.add(gw_db)
        LOG.debug(_("Created network gateway with id:%s"), gw_db['id'])
        return self._make_network_gateway_dict(gw_db)

    def update_network_gateway(self, context, id, network_gateway):
        gw_data = network_gateway[self.gateway_resource]
        with context.session.begin(subtransactions=True):
            gw_db = self._get_network_gateway(context, id)
            if gw_db.default:
                raise NetworkGatewayUnchangeable(gateway_id=id)
            # Ensure there is something to update before doing it
            if any([gw_db[k] != gw_data[k] for k in gw_data]):
                gw_db.update(gw_data)
        LOG.debug(_("Updated network gateway with id:%s"), id)
        return self._make_network_gateway_dict(gw_db)

    def get_network_gateway(self, context, id, fields=None):
        gw_db = self._get_network_gateway(context, id)
        return self._make_network_gateway_dict(gw_db, fields)

    def delete_network_gateway(self, context, id):
        with context.session.begin(subtransactions=True):
            gw_db = self._get_network_gateway(context, id)
            if gw_db.network_connections:
                raise GatewayInUse(gateway_id=id)
            if gw_db.default:
                raise NetworkGatewayUnchangeable(gateway_id=id)
            context.session.delete(gw_db)
        LOG.debug(_("Network gateway '%s' was destroyed."), id)

    def get_network_gateways(self, context, filters=None, fields=None,
                             sorts=None, limit=None, marker=None,
                             page_reverse=False):
        marker_obj = self._get_marker_obj(
            context, 'network_gateway', limit, marker)
        return self._get_collection(context, NetworkGateway,
                                    self._make_network_gateway_dict,
                                    filters=filters, fields=fields,
                                    sorts=sorts, limit=limit,
                                    marker_obj=marker_obj,
                                    page_reverse=page_reverse)

    def connect_network(self, context, network_gateway_id,
                        network_mapping_info):
        network_id = self._validate_network_mapping_info(network_mapping_info)
        LOG.debug(_("Connecting network '%(network_id)s' to gateway "
                    "'%(network_gateway_id)s'"),
                  {'network_id': network_id,
                   'network_gateway_id': network_gateway_id})
        with context.session.begin(subtransactions=True):
            gw_db = self._get_network_gateway(context, network_gateway_id)
            tenant_id = self._get_tenant_id_for_create(context, gw_db)
            # TODO(salvatore-orlando): Leverage unique constraint instead
            # of performing another query!
            if self._retrieve_gateway_connections(context,
                                                  network_gateway_id,
                                                  network_mapping_info):
                raise GatewayConnectionInUse(mapping=network_mapping_info,
                                             gateway_id=network_gateway_id)
            # TODO(salvatore-orlando): Creating a port will give it an IP,
            # but we actually do not need any. Instead of wasting an IP we
            # should have a way to say a port shall not be associated with
            # any subnet
            try:
                # We pass the segmentation type and id too - the plugin
                # might find them useful as the network connection object
                # does not exist yet.
                # NOTE: they're not extended attributes, rather extra data
                # passed in the port structure to the plugin
                # TODO(salvatore-orlando): Verify optimal solution for
                # ownership of the gateway port
                port = self.create_port(context, {
                    'port':
                    {'tenant_id': tenant_id,
                     'network_id': network_id,
                     'mac_address': attributes.ATTR_NOT_SPECIFIED,
                     'admin_state_up': True,
                     'fixed_ips': [],
                     'device_id': network_gateway_id,
                     'device_owner': DEVICE_OWNER_NET_GW_INTF,
                     'name': '',
                     'gw:segmentation_type':
                     network_mapping_info.get('segmentation_type'),
                     'gw:segmentation_id':
                     network_mapping_info.get('segmentation_id')}})
            except exceptions.NetworkNotFound:
                err_msg = (_("Requested network '%(network_id)s' not found."
                             "Unable to create network connection on "
                             "gateway '%(network_gateway_id)s") %
                           {'network_id': network_id,
                            'network_gateway_id': network_gateway_id})
                LOG.error(err_msg)
                raise exceptions.InvalidInput(error_message=err_msg)
            port_id = port['id']
            LOG.debug(_("Gateway port for '%(network_gateway_id)s' "
                        "created on network '%(network_id)s':%(port_id)s"),
                      {'network_gateway_id': network_gateway_id,
                       'network_id': network_id,
                       'port_id': port_id})
            # Create NetworkConnection record
            network_mapping_info['port_id'] = port_id
            network_mapping_info['tenant_id'] = tenant_id
            gw_db.network_connections.append(
                NetworkConnection(**network_mapping_info))
            port_id = port['id']
            # now deallocate and recycle ip from the port
            for fixed_ip in port.get('fixed_ips', []):
                self._delete_ip_allocation(context, network_id,
                                           fixed_ip['subnet_id'],
                                           fixed_ip['ip_address'])
            LOG.debug(_("Ensured no Ip addresses are configured on port %s"),
                      port_id)
            return {'connection_info':
                    {'network_gateway_id': network_gateway_id,
                     'network_id': network_id,
                     'port_id': port_id}}

    def disconnect_network(self, context, network_gateway_id,
                           network_mapping_info):
        network_id = self._validate_network_mapping_info(network_mapping_info)
        LOG.debug(_("Disconnecting network '%(network_id)s' from gateway "
                    "'%(network_gateway_id)s'"),
                  {'network_id': network_id,
                   'network_gateway_id': network_gateway_id})
        with context.session.begin(subtransactions=True):
            # Uniquely identify connection, otherwise raise
            try:
                net_connection = self._retrieve_gateway_connections(
                    context, network_gateway_id,
                    network_mapping_info, only_one=True)
            except sa_orm_exc.NoResultFound:
                raise GatewayConnectionNotFound(
                    network_mapping_info=network_mapping_info,
                    network_gateway_id=network_gateway_id)
            except sa_orm_exc.MultipleResultsFound:
                raise MultipleGatewayConnections(
                    gateway_id=network_gateway_id)
            # Remove gateway port from network
            # FIXME(salvatore-orlando): Ensure state of port in NVP is
            # consistent with outcome of transaction
            self.delete_port(context, net_connection['port_id'],
                             nw_gw_port_check=False)
            # Remove NetworkConnection record
            context.session.delete(net_connection)

    def _make_gateway_device_dict(self, gateway_device, fields=None,
                                  include_nsx_id=False):
        res = {'id': gateway_device['id'],
               'name': gateway_device['name'],
               'status': gateway_device['status'],
               'connector_type': gateway_device['connector_type'],
               'connector_ip': gateway_device['connector_ip'],
               'tenant_id': gateway_device['tenant_id']}
        if include_nsx_id:
            # Return the NSX mapping as well. This attribute will not be
            # returned in the API response anyway. Ensure it will not be
            # filtered out in field selection.
            if fields:
                fields.append('nsx_id')
            res['nsx_id'] = gateway_device['nsx_id']
        return self._fields(res, fields)

    def _get_gateway_device(self, context, device_id):
        try:
            return self._get_by_id(context, NetworkGatewayDevice, device_id)
        except sa_orm_exc.NoResultFound:
            raise GatewayDeviceNotFound(device_id=device_id)

    def _is_device_in_use(self, context, device_id):
        query = self._get_collection_query(
            context, NetworkGatewayDeviceReference, {'id': [device_id]})
        return query.first()

    def get_gateway_device(self, context, device_id, fields=None,
                           include_nsx_id=False):
        return self._make_gateway_device_dict(
            self._get_gateway_device(context, device_id),
            fields, include_nsx_id)

    def get_gateway_devices(self, context, filters=None, fields=None,
                            sorts=None, limit=None, marker=None,
                            page_reverse=False, include_nsx_id=False):
        marker_obj = self._get_marker_obj(
            context, 'gateway_device', limit, marker)
        query = self._get_collection_query(context,
                                           NetworkGatewayDevice,
                                           filters=filters,
                                           fields=fields,
                                           sorts=sorts,
                                           limit=limit,
                                           marker_obj=marker_obj,
                                           page_reverse=page_reverse)
        return [self._make_gateway_device_dict(row, fields, include_nsx_id)
                for row in query]

    def create_gateway_device(self, context, gateway_device,
                              initial_status=STATUS_UNKNOWN):
        device_data = gateway_device[self.device_resource]
        tenant_id = self._get_tenant_id_for_create(context, device_data)
        with context.session.begin(subtransactions=True):
            device_db = NetworkGatewayDevice(
                id=device_data.get('id', uuidutils.generate_uuid()),
                tenant_id=tenant_id,
                name=device_data.get('name'),
                connector_type=device_data['connector_type'],
                connector_ip=device_data['connector_ip'],
                status=initial_status)
            context.session.add(device_db)
        LOG.debug(_("Created network gateway device: %s"), device_db['id'])
        return self._make_gateway_device_dict(device_db)

    def update_gateway_device(self, context, gateway_device_id,
                              gateway_device, include_nsx_id=False):
        device_data = gateway_device[self.device_resource]
        with context.session.begin(subtransactions=True):
            device_db = self._get_gateway_device(context, gateway_device_id)
            # Ensure there is something to update before doing it
            if any([device_db[k] != device_data[k] for k in device_data]):
                device_db.update(device_data)
        LOG.debug(_("Updated network gateway device: %s"),
                  gateway_device_id)
        return self._make_gateway_device_dict(
            device_db, include_nsx_id=include_nsx_id)

    def delete_gateway_device(self, context, device_id):
        with context.session.begin(subtransactions=True):
            # A gateway device should not be deleted
            # if it is used in any network gateway service
            if self._is_device_in_use(context, device_id):
                raise GatewayDeviceInUse(device_id=device_id)
            device_db = self._get_gateway_device(context, device_id)
            context.session.delete(device_db)
        LOG.debug(_("Deleted network gateway device: %s."), device_id)
