# Copyright (c) 2016 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 availability_zone as az_def
from neutron_lib.api.validators import availability_zone as az_validator
from neutron_lib import constants
from neutron_lib.objects import common_types
from oslo_utils import versionutils
from oslo_versionedobjects import exception
from oslo_versionedobjects import fields as obj_fields

from neutron.db.models import dns as dns_models
from neutron.db.models import external_net as ext_net_model
from neutron.db.models import segment as segment_model
from neutron.db import models_v2
from neutron.db.network_dhcp_agent_binding import models as ndab_models
from neutron.db.port_security import models as ps_models
from neutron.db import rbac_db_models
from neutron.objects import agent as agent_obj
from neutron.objects import base
from neutron.objects.extensions import port_security as base_ps
from neutron.objects.qos import binding
from neutron.objects import rbac
from neutron.objects import rbac_db


@base.NeutronObjectRegistry.register
class NetworkRBAC(rbac.RBACBaseObject):
    # Version 1.0: Initial version
    # Version 1.1: Added 'id' and 'project_id'
    # Version 1.2: Inherit from rbac.RBACBaseObject; changed 'object_id' from
    #              StringField to UUIDField
    # Version 1.3: Changed 'target_tenant' to 'target_project'
    VERSION = '1.3'

    db_model = rbac_db_models.NetworkRBAC

    def obj_make_compatible(self, primitive, target_version):
        _target_version = versionutils.convert_version_to_tuple(target_version)
        if _target_version < (1, 1):
            standard_fields = ['id', 'project_id']
            for f in standard_fields:
                primitive.pop(f, None)


@base.NeutronObjectRegistry.register
class NetworkDhcpAgentBinding(base.NeutronDbObject):
    # Version 1.0: Initial version
    # Version 1.1: Added 'binding_index'

    VERSION = '1.1'

    db_model = ndab_models.NetworkDhcpAgentBinding

    primary_keys = ['network_id', 'dhcp_agent_id']

    fields = {
        'network_id': common_types.UUIDField(),
        'dhcp_agent_id': common_types.UUIDField(),
        'binding_index': obj_fields.IntegerField(),
    }

    # NOTE(ndahiwade): The join was implemented this way as get_objects
    # currently doesn't support operators like '<' or '>'
    @classmethod
    def get_down_bindings(cls, context, cutoff):
        agent_objs = agent_obj.Agent.get_objects(context)
        dhcp_agent_ids = [obj.id for obj in agent_objs
                          if obj.heartbeat_timestamp < cutoff]
        return cls.get_objects(context, dhcp_agent_id=dhcp_agent_ids)


@base.NeutronObjectRegistry.register
class NetworkSegment(base.NeutronDbObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    db_model = segment_model.NetworkSegment

    fields = {
        'id': common_types.UUIDField(),
        'network_id': common_types.UUIDField(),
        'name': obj_fields.StringField(nullable=True),
        'network_type': obj_fields.StringField(),
        'physical_network': obj_fields.StringField(nullable=True),
        'segmentation_id': obj_fields.IntegerField(nullable=True),
        'is_dynamic': obj_fields.BooleanField(default=False),
        'segment_index': obj_fields.IntegerField(default=0),
        'hosts': obj_fields.ListOfStringsField(nullable=True)
    }

    synthetic_fields = ['hosts']

    fields_no_update = ['network_id']

    foreign_keys = {
        'Network': {'network_id': 'id'},
        'PortBindingLevel': {'id': 'segment_id'},
    }

    def create(self):
        fields = self.obj_get_changes()
        with self.db_context_writer(self.obj_context):
            hosts = self.hosts
            if hosts is None:
                hosts = []
            super(NetworkSegment, self).create()
            if 'hosts' in fields:
                self._attach_hosts(hosts)

    def update(self):
        fields = self.obj_get_changes()
        with self.db_context_writer(self.obj_context):
            super(NetworkSegment, self).update()
            if 'hosts' in fields:
                self._attach_hosts(fields['hosts'])

    def _attach_hosts(self, hosts):
        SegmentHostMapping.delete_objects(
            self.obj_context, segment_id=self.id,
        )
        if hosts:
            for host in hosts:
                SegmentHostMapping(
                    self.obj_context, segment_id=self.id, host=host).create()
        self.hosts = hosts
        self.obj_reset_changes(['hosts'])

    def obj_load_attr(self, attrname):
        if attrname == 'hosts':
            return self._load_hosts()
        super(NetworkSegment, self).obj_load_attr(attrname)

    def _load_hosts(self, db_obj=None):
        if db_obj:
            hosts = db_obj.get('segment_host_mapping', [])
        else:
            hosts = SegmentHostMapping.get_objects(self.obj_context,
                                                   segment_id=self.id)

        self.hosts = [host['host'] for host in hosts]
        self.obj_reset_changes(['hosts'])

    def from_db_object(self, db_obj):
        super(NetworkSegment, self).from_db_object(db_obj)
        self._load_hosts(db_obj)

    @classmethod
    def get_objects(cls, context, _pager=None, **kwargs):
        if not _pager:
            _pager = base.Pager()
        if not _pager.sorts:
            # (NOTE) True means ASC, False is DESC
            _pager.sorts = [
                (field, True) for field in ('network_id', 'segment_index')
            ]
        return super(NetworkSegment, cls).get_objects(context, _pager,
                                                      **kwargs)


@base.NeutronObjectRegistry.register
class NetworkPortSecurity(base_ps._PortSecurity):
    # Version 1.0: Initial version
    VERSION = "1.0"

    db_model = ps_models.NetworkSecurityBinding

    fields_need_translation = {'id': 'network_id'}


@base.NeutronObjectRegistry.register
class ExternalNetwork(base.NeutronDbObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    db_model = ext_net_model.ExternalNetwork

    foreign_keys = {'Network': {'network_id': 'id'}}

    primary_keys = ['network_id']

    fields = {
        'network_id': common_types.UUIDField(),
        'is_default': obj_fields.BooleanField(default=False),
    }


@base.NeutronObjectRegistry.register
class Network(rbac_db.NeutronRbacObject):
    # Version 1.0: Initial version
    # Version 1.1: Changed 'mtu' to be not nullable
    VERSION = '1.1'

    rbac_db_cls = NetworkRBAC
    db_model = models_v2.Network

    fields = {
        'id': common_types.UUIDField(),
        'project_id': obj_fields.StringField(nullable=True),
        'name': obj_fields.StringField(nullable=True),
        'status': obj_fields.StringField(nullable=True),
        'admin_state_up': obj_fields.BooleanField(nullable=True),
        'vlan_transparent': obj_fields.BooleanField(nullable=True),
        # TODO(ihrachys): consider converting to a field of stricter type
        'availability_zone_hints': obj_fields.ListOfStringsField(
            nullable=True),
        'shared': obj_fields.BooleanField(default=False),

        'mtu': obj_fields.IntegerField(default=constants.DEFAULT_NETWORK_MTU),

        # TODO(ihrachys): consider exposing availability zones

        # TODO(ihrachys): consider converting to boolean
        'security': obj_fields.ObjectField(
            'NetworkPortSecurity', nullable=True),
        'segments': obj_fields.ListOfObjectsField(
            'NetworkSegment', nullable=True),
        'dns_domain': common_types.DomainNameField(nullable=True),
        'qos_policy_id': common_types.UUIDField(nullable=True, default=None),

        # TODO(ihrachys): add support for tags, probably through a base class
        # since it's a feature that will probably later be added for other
        # resources too

        # TODO(ihrachys): expose external network attributes
    }

    synthetic_fields = [
        'dns_domain',
        'qos_policy_id',
        'security',
        'segments',
    ]

    fields_need_translation = {
        'security': 'port_security',
    }

    def create(self):
        fields = self.obj_get_changes()
        with self.db_context_writer(self.obj_context):
            dns_domain = self.dns_domain
            qos_policy_id = self.qos_policy_id
            super(Network, self).create()
            if 'dns_domain' in fields:
                self._set_dns_domain(dns_domain)
            if 'qos_policy_id' in fields:
                self._attach_qos_policy(qos_policy_id)

    def update(self):
        fields = self.obj_get_changes()
        with self.db_context_writer(self.obj_context):
            super(Network, self).update()
            if 'dns_domain' in fields:
                self._set_dns_domain(fields['dns_domain'])
            if 'qos_policy_id' in fields:
                self._attach_qos_policy(fields['qos_policy_id'])

    def _attach_qos_policy(self, qos_policy_id):
        binding.QosPolicyNetworkBinding.delete_objects(
            self.obj_context, network_id=self.id)
        if qos_policy_id:
            net_binding_obj = binding.QosPolicyNetworkBinding(
                self.obj_context, policy_id=qos_policy_id, network_id=self.id)
            net_binding_obj.create()

        self.qos_policy_id = qos_policy_id
        self.obj_reset_changes(['qos_policy_id'])

    def _set_dns_domain(self, dns_domain):
        NetworkDNSDomain.delete_objects(self.obj_context, network_id=self.id)
        if dns_domain:
            NetworkDNSDomain(self.obj_context, network_id=self.id,
                             dns_domain=dns_domain).create()
        self.dns_domain = dns_domain
        self.obj_reset_changes(['dns_domain'])

    @classmethod
    def modify_fields_from_db(cls, db_obj):
        result = super(Network, cls).modify_fields_from_db(db_obj)
        if az_def.AZ_HINTS in result:
            result[az_def.AZ_HINTS] = (
                az_validator.convert_az_string_to_list(
                    result[az_def.AZ_HINTS]))
        return result

    @classmethod
    def modify_fields_to_db(cls, fields):
        result = super(Network, cls).modify_fields_to_db(fields)
        if az_def.AZ_HINTS in result:
            result[az_def.AZ_HINTS] = (
                az_validator.convert_az_list_to_string(
                    result[az_def.AZ_HINTS]))
        return result

    def from_db_object(self, *objs):
        super(Network, self).from_db_object(*objs)
        for db_obj in objs:
            # extract domain name
            if db_obj.get('dns_domain'):
                self.dns_domain = (
                    db_obj.dns_domain.dns_domain
                )
            else:
                self.dns_domain = None
            self.obj_reset_changes(['dns_domain'])

            # extract qos policy binding
            if db_obj.get('qos_policy_binding'):
                self.qos_policy_id = (
                    db_obj.qos_policy_binding.policy_id
                )
            else:
                self.qos_policy_id = None
            self.obj_reset_changes(['qos_policy_id'])

    @classmethod
    def get_bound_project_ids(cls, context, policy_id):
        # TODO(ihrachys): provide actual implementation
        return set()

    def obj_make_compatible(self, primitive, target_version):
        _target_version = versionutils.convert_version_to_tuple(target_version)
        if _target_version >= (1, 1):
            if primitive['mtu'] is None:
                # mtu will not be nullable after
                raise exception.IncompatibleObjectVersion(
                    objver=target_version, objname=self.__class__.__name__)


@base.NeutronObjectRegistry.register
class SegmentHostMapping(base.NeutronDbObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    db_model = segment_model.SegmentHostMapping

    fields = {
        'segment_id': common_types.UUIDField(),
        'host': obj_fields.StringField(),
    }

    primary_keys = ['segment_id', 'host']


@base.NeutronObjectRegistry.register
class NetworkDNSDomain(base.NeutronDbObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    db_model = dns_models.NetworkDNSDomain

    primary_keys = ['network_id']

    fields = {
        'network_id': common_types.UUIDField(),
        'dns_domain': common_types.DomainNameField(),
    }

    @classmethod
    def get_net_dns_from_port(cls, context, port_id):
        net_dns = context.session.query(cls.db_model).join(
            models_v2.Port, cls.db_model.network_id ==
            models_v2.Port.network_id).filter_by(
                id=port_id).one_or_none()
        if net_dns is None:
            return None
        return super(NetworkDNSDomain, cls)._load_object(context, net_dns)
