# Copyright 2017 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 abc
from distutils import version

from oslo_log import log as logging
import six

from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3.policy import constants
from vmware_nsxlib.v3 import utils

LOG = logging.getLogger(__name__)

LOG = logging.getLogger(__name__)

TENANTS_PATH_PATTERN = "%s/"
DOMAINS_PATH_PATTERN = TENANTS_PATH_PATTERN + "domains/"
IP_BLOCKS_PATH_PATTERN = TENANTS_PATH_PATTERN + "ip-blocks/"
IP_POOLS_PATH_PATTERN = TENANTS_PATH_PATTERN + "ip-pools/"
SEGMENTS_PATH_PATTERN = TENANTS_PATH_PATTERN + "segments/"
PROVIDERS_PATH_PATTERN = TENANTS_PATH_PATTERN + "providers/"
TIER0S_PATH_PATTERN = TENANTS_PATH_PATTERN + "tier-0s/"
TIER1S_PATH_PATTERN = TENANTS_PATH_PATTERN + "tier-1s/"
SERVICES_PATH_PATTERN = TENANTS_PATH_PATTERN + "services/"
GLOBAL_CONFIG_PATH_PATTERN = TENANTS_PATH_PATTERN + "global-config/"
ENFORCEMENT_POINT_PATTERN = (TENANTS_PATH_PATTERN +
                             "sites/default/enforcement-points/")
TRANSPORT_ZONE_PATTERN = ENFORCEMENT_POINT_PATTERN + "%s/transport-zones/"
EDGE_CLUSTER_PATTERN = ENFORCEMENT_POINT_PATTERN + "%s/edge-clusters/"

SEGMENT_SECURITY_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                                          "segment-security-profiles/")
QOS_PROFILES_PATH_PATTERN = TENANTS_PATH_PATTERN + "qos-profiles/"
SPOOFGUARD_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                                    "spoofguard-profiles/")
IP_DISCOVERY_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                                      "ip-discovery-profiles/")
MAC_DISCOVERY_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                                       "mac-discovery-profiles/")
IPV6_NDRA_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                                   "ipv6-ndra-profiles/")
WAF_PROFILES_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                             "waf-profiles/")
CERTIFICATE_PATH_PATTERN = TENANTS_PATH_PATTERN + "certificates/"
EXCLUDE_LIST_PATH_PATTERN = (TENANTS_PATH_PATTERN +
                             "settings/firewall/security/exclude-list")

REALIZATION_PATH = "infra/realized-state/realized-entities?intent_path=%s"
DHCP_REALY_PATTERN = TENANTS_PATH_PATTERN + "dhcp-relay-configs/"
DHCP_SERVER_PATTERN = TENANTS_PATH_PATTERN + "dhcp-server-configs/"
MDPROXY_PATTERN = TENANTS_PATH_PATTERN + "metadata-proxies/"

TIER0_LOCALE_SERVICES_PATH_PATTERN = (TIER0S_PATH_PATTERN +
                                      "%s/locale-services/")
TIER1_LOCALE_SERVICES_PATH_PATTERN = (TIER1S_PATH_PATTERN +
                                      "%s/locale-services/")


@six.add_metaclass(abc.ABCMeta)
class ResourceDef(object):
    def __init__(self, nsx_version=None, **kwargs):
        self.attrs = kwargs

        # nsx_version should be passed in on init if the resource has
        # version-dependant attributes. Otherwise this is ignored
        self.nsx_version = nsx_version

        # init default tenant
        self.attrs['tenant'] = self.get_tenant()

        self.body = {}

        # Whether this entry needs to be deleted
        self.delete = False

        # As of now, for some defs (ex: services) child entry is required,
        # meaning parent creation will fail without the child.
        # Unfortunately in transactional API policy still fails us, even if
        # child is specified as ChildEntry in same transaction.
        # To provide a workaround, we need keep reference to the child and
        # populate child entry inside parent clause in transactional API.
        # TODO(annak): remove this if/when policy solves this
        self.mandatory_child_def = None

    def set_delete(self):
        self.delete = True

    def get_delete(self):
        return self.delete

    def get_obj_dict(self):
        body = self.body if self.body else {}
        if self.resource_type():
            body['resource_type'] = self.resource_type()

        self._set_attr_if_specified(body, 'name', 'display_name')
        self._set_attrs_if_specified(body, ['description', 'tags'])

        resource_id = self.get_id()
        if resource_id:
            body['id'] = resource_id
        return body

    # This is needed for sake of update due to policy issue.
    # Policy refuses to update without requires attributes provided,
    # so we need to run an extra GET to acquire these.
    # This should be removed when/if this issue is fixed on backend.
    def set_obj_dict(self, obj_dict):
        self.body = obj_dict

    @abc.abstractproperty
    def path_pattern(self):
        pass

    @abc.abstractproperty
    def path_ids(self):
        pass

    @staticmethod
    def resource_type():
        pass

    @classmethod
    def resource_class(cls):
        # Returns base resource type for polymorphic objects
        # if not overriden, would return resource_type
        return cls.resource_type()

    @staticmethod
    def resource_use_cache():
        return False

    def path_defs(self):
        pass

    def get_id(self):
        if self.attrs and self.path_ids:
            return self.attrs.get(self.path_ids[-1])

    def get_attr(self, attr):
        return self.attrs.get(attr)

    def has_attr(self, attr):
        return attr in self.attrs

    def get_tenant(self):
        if self.attrs.get('tenant'):
            return self.attrs.get('tenant')

        return constants.POLICY_INFRA_TENANT

    def get_section_path(self):
        path_ids = [self.get_attr(path_id) for path_id in self.path_ids[:-1]]
        return self.path_pattern % (tuple(path_ids))

    def get_resource_path(self):
        resource_id = self.get_id()
        if resource_id:
            return self.get_section_path() + resource_id
        return self.get_section_path()

    def get_resource_full_path(self):
        return '/' + self.get_resource_path()

    @property
    def get_last_section_dict_key(self):
        last_section = self.path_pattern.split("/")[-2]
        return last_section.replace('-', '_')

    @staticmethod
    def sub_entries_path():
        pass

    def _get_body_from_kwargs(self, **kwargs):
        if 'body' in kwargs:
            body = kwargs['body']
        else:
            body = {}
        return body

    # Helper to set attr in body if user specified it
    # Can be used if body name is different than attr name
    # If value is different than self.get_attr(attr), it can be set in arg
    def _set_attr_if_specified(self, body, attr, body_attr=None, **kwargs):
        if self.has_attr(attr):
            value = (kwargs['value'] if 'value' in kwargs
                     else self.get_attr(attr))
            if body_attr:
                # Body attr is different that attr exposed by resource def
                body[body_attr] = value
            else:
                # Body attr is the same
                body[attr] = value

    # Helper to set attrs in body if user specified them
    # Body name must match attr name
    def _set_attrs_if_specified(self, body, attr_list):
        for attr in attr_list:
            self._set_attr_if_specified(body, attr)

    # Helper to set attr in body if user specified it
    # and current nsx version supports it
    # Body name must match attr name
    def _set_attr_if_supported(self, body, attr, value=None):
        if self.has_attr(attr) and self._version_dependant_attr_supported(
                attr):
            value = value if value is not None else self.get_attr(attr)
            body[attr] = value

    # Helper to set attrs in body if user specified them
    # and current nsx version supports it
    # Body name must match attr name
    def _set_attrs_if_supported(self, body, attr_list):
        for attr in attr_list:
            self._set_attr_if_supported(body, attr)

    @property
    def version_dependant_attr_map(self):
        """Specify version depenand attributes and supporting NSX version

        Resources that contain version dependant attributes should specify
        attribute name and first supporting version in map returned from
        this call.
        """
        return {}

    def _version_dependant_attr_supported(self, attr):
        """Check if a version dependent attr is supported on current NSX

        For each resource def, there could be some attributes which only exist
        on NSX after certain versions. These attrs should be defined on def
        level via version_dependant_attr_map, where map value indicates NSX
        version that first exposes the support.

        By design, Devs should use _set_attr_if_supported() to add any attrs
        that are only known to NSX after a certain version. This method works
        as a registry for _set_attrs_if_supported() to know the baseline
        version of each version dependent attr.

        Non-version-dependent attributes should be added to the request body
        by using _set_attr_if_specified(). This method defaults to false since
        any version dependent attr unknown to this lib should be excluded
        for security and safety reasons.
        """
        supporting_version = self.version_dependant_attr_map.get(attr)
        if not supporting_version:
            LOG.warning("Supporting version not defined for attr %s. Assuming "
                        "no support", attr)
            return False

        if (version.LooseVersion(self.nsx_version) >=
            version.LooseVersion(supporting_version)):
            return True

        LOG.warning(
            "Ignoring %s for %s %s: this feature is not supported."
            "Current NSX version: %s. Minimum supported version: %s",
            attr, self.resource_type, self.attrs.get('name', ''),
            self.nsx_version, supporting_version)
        return False

    @classmethod
    def get_single_entry(cls, obj_body):
        """Return the single sub-entry from the object body.

        If there are no entries, or more than 1 - return None.
        """
        entries_path = cls.sub_entries_path()
        if not entries_path:
            # This sub class doesn't support this
            return

        if (entries_path not in obj_body or
            len(obj_body[entries_path]) != 1):
            return

        return obj_body[entries_path][0]

    def bodyless(self):
        """Return True if args contain only keys and meta attrs"""

        meta = ['resource_type']
        meta.extend(self.path_ids)
        body_args = [key for key in self.attrs.keys()
                     if key not in meta]
        return len(body_args) == 0

    def set_default_mandatory_vals(self):
        pass


class TenantDef(ResourceDef):
    @property
    def path_pattern(self):
        return TENANTS_PATH_PATTERN

    @staticmethod
    def resource_type():
        return 'Infra'

    def path_defs(self):
        return ()

    @property
    def path_ids(self):
        return ('tenant',)

    def get_resource_path(self):
        return 'infra/'

    def get_section_path(self):
        return 'infra/'


class DomainDef(ResourceDef):

    @property
    def path_pattern(self):
        return DOMAINS_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'domain_id')

    @staticmethod
    def resource_type():
        return 'Domain'

    def path_defs(self):
        return (TenantDef,)


class RouteAdvertisement(object):

    types = {'static_routes': constants.ADV_RULE_TYPE_TIER1_STATIC_ROUTES,
             'subnets': constants.ADV_RULE_TIER1_CONNECTED,
             'nat': constants.ADV_RULE_TIER1_NAT,
             'lb_vip': constants.ADV_RULE_TIER1_LB_VIP,
             'lb_snat': constants.ADV_RULE_TIER1_LB_SNAT,
             'dns_forwarder_ip': constants.ADV_RULE_TIER1_DNS_FORWARDER_IP,
             'ipsec_endpoints': constants.ADV_RULE_TIER1_IPSEC_LOCAL_ENDPOINT}

    def __init__(self, **kwargs):
        self.attrs = kwargs

    def get_obj_dict(self):
        return [value for key, value in self.types.items()
                if self.attrs.get(key) is True]

    def set_obj_dict(self, obj_dict):
        # This initializes object based on list coming from backend
        # f.e. [TIER1_NAT, TIER1_LB_SNAT]

        for key, value in self.types.items():
            self.attrs[key] = value in obj_dict

    def update(self, **kwargs):
        # "None" will be passed as value when user does not specify adv type
        # True/False will be passed when user wants to switch adv ON/OFF
        for key, value in kwargs.items():
            if value is not None:
                self.attrs[key] = value


class RouterDef(ResourceDef):
    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(RouterDef, self).get_obj_dict()

        self._set_attrs_if_specified(body, ['failover_mode',
                                            'force_whitelisting',
                                            'default_rule_logging',
                                            'disable_firewall'])

        # Add dhcp relay config
        # TODO(asarfaty): this can be either dhcp or dhcp relay config
        if self.has_attr('dhcp_config'):
            paths = ""
            if self.get_attr('dhcp_config'):
                dhcp_conf = DhcpRelayConfigDef(
                    config_id=self.get_attr('dhcp_config'),
                    tenant=self.get_tenant())
                paths = [dhcp_conf.get_resource_full_path()]
            self._set_attr_if_specified(body, 'dhcp_config',
                                        body_attr='dhcp_config_paths',
                                        value=paths)

        if self.has_attr('ipv6_ndra_profile_id'):
            if self.get_attr('ipv6_ndra_profile_id'):
                ndra_profile = Ipv6NdraProfileDef(
                    profile_id=self.get_attr('ipv6_ndra_profile_id'),
                    tenant=self.get_tenant())
            else:
                # Set it to the default profile
                # This will allow removing the old profile,
                # as the NSX does not support empty value.
                ndra_profile = Ipv6NdraProfileDef(
                    profile_id=Ipv6NdraProfileDef.default_profile(),
                    tenant=self.get_tenant())

            paths = [ndra_profile.get_resource_full_path()]
            self._set_attr_if_specified(body, 'ipv6_ndra_profile_id',
                                        body_attr='ipv6_profile_paths',
                                        value=paths)

        return body


class Tier0Def(RouterDef):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id')

    @staticmethod
    def resource_type():
        return 'Tier0'

    @staticmethod
    def resource_use_cache():
        return True

    def get_obj_dict(self):
        body = super(Tier0Def, self).get_obj_dict()

        self._set_attrs_if_specified(body, ['ha_mode', 'transit_subnets'])

        return body


class Tier1Def(RouterDef):

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id')

    @staticmethod
    def resource_type():
        return 'Tier1'

    def get_obj_dict(self):
        body = super(Tier1Def, self).get_obj_dict()

        if self.has_attr('tier0'):
            tier0 = self.get_attr('tier0')
            tier0_path = ""
            if tier0:
                tenant = TENANTS_PATH_PATTERN % self.get_tenant()
                tier0_path = "/%stier-0s/%s" % (tenant, tier0)

            self._set_attr_if_specified(body, 'tier0',
                                        body_attr='tier0_path',
                                        value=tier0_path)

        if self.has_attr('route_advertisement'):
            body['route_advertisement_types'] = self.get_attr(
                'route_advertisement').get_obj_dict()

        self._set_attrs_if_specified(body, ['enable_standby_relocation'])
        self._set_attr_if_supported(body, 'pool_allocation')
        if self.has_attr('route_advertisement_rules'):
            body['route_advertisement_rules'] = [
                a.get_obj_dict()
                if isinstance(a, RouteAdvertisementRule) else a
                for a in self.get_attr('route_advertisement_rules')]

        return body

    @staticmethod
    def get_route_adv(obj_dict):
        route_adv = RouteAdvertisement()
        if 'route_advertisement_types' in obj_dict:
            route_adv.set_obj_dict(obj_dict['route_advertisement_types'])
        return route_adv

    @property
    def version_dependant_attr_map(self):
        return {'pool_allocation': nsx_constants.NSX_VERSION_3_0_0}


class RouterLocaleServiceDef(ResourceDef):

    @staticmethod
    def resource_type():
        return 'LocaleServices'

    def get_obj_dict(self):
        body = super(RouterLocaleServiceDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'edge_cluster_path')
        return body


class Tier0LocaleServiceDef(RouterLocaleServiceDef):

    @property
    def path_pattern(self):
        return TIER0_LOCALE_SERVICES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'service_id')

    @property
    def version_dependant_attr_map(self):
        return {'route_redistribution_config': nsx_constants.NSX_VERSION_3_0_0}

    def path_defs(self):
        return (TenantDef, Tier0Def)

    def get_obj_dict(self):
        body = super(Tier0LocaleServiceDef, self).get_obj_dict()

        if (self.has_attr('route_redistribution_config') and
            self._version_dependant_attr_supported(
                'route_redistribution_config')):
            config = self.get_attr('route_redistribution_config')
            body['route_redistribution_config'] = (
                config.get_obj_dict()
                if isinstance(config, Tier0RouteRedistributionConfig)
                else config)
        return body


class Tier1LocaleServiceDef(RouterLocaleServiceDef):

    @property
    def path_pattern(self):
        return TIER1_LOCALE_SERVICES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'service_id')

    def path_defs(self):
        return (TenantDef, Tier1Def)


class Tier0InterfaceDef(ResourceDef):

    @staticmethod
    def resource_type():
        return 'Tier0Interface'

    @property
    def path_pattern(self):
        return TIER0_LOCALE_SERVICES_PATH_PATTERN + "%s/interfaces/"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'service_id', 'interface_id')


class Tier1InterfaceDef(ResourceDef):

    @staticmethod
    def resource_type():
        return 'Tier1Interface'

    @property
    def path_pattern(self):
        return TIER1_LOCALE_SERVICES_PATH_PATTERN + "%s/interfaces/"

    def get_obj_dict(self):
        body = super(Tier1InterfaceDef, self).get_obj_dict()
        if self.has_attr('subnets'):
            # subnets expected to be of type InterfaceSubnet
            if self.get_attr('subnets'):
                subnets = [subnet.get_obj_dict()
                           if isinstance(subnet, InterfaceSubnet) else subnet
                           for subnet in self.get_attr('subnets')]
                self._set_attr_if_specified(body, 'subnets',
                                            value=subnets)

        if self.has_attr('segment_id'):
            path = ""
            if self.get_attr('segment_id'):
                tier1 = SegmentDef(segment_id=self.get_attr('segment_id'),
                                   tenant=self.get_tenant())
                path = tier1.get_resource_full_path()
            self._set_attr_if_specified(body, 'segment_id',
                                        body_attr='segment_path',
                                        value=path)

        if self.has_attr('ipv6_ndra_profile_id'):
            if self.get_attr('ipv6_ndra_profile_id'):
                ndra_profile = Ipv6NdraProfileDef(
                    profile_id=self.get_attr('ipv6_ndra_profile_id'),
                    tenant=self.get_tenant())
            else:
                # Set it to the default profile
                # This will allow removing the old profile,
                # as the NSX does not support empty value.
                ndra_profile = Ipv6NdraProfileDef(
                    profile_id=Ipv6NdraProfileDef.default_profile(),
                    tenant=self.get_tenant())

            paths = [ndra_profile.get_resource_full_path()]
            self._set_attr_if_specified(body, 'ipv6_ndra_profile_id',
                                        body_attr='ipv6_profile_paths',
                                        value=paths)
        return body

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'service_id', 'interface_id')


class RouterNatRule(ResourceDef):

    @staticmethod
    def resource_type():
        return 'PolicyNatRule'

    def get_obj_dict(self):
        body = super(RouterNatRule, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['action',
                                            'source_network',
                                            'destination_network',
                                            'translated_network',
                                            'firewall_match',
                                            'log',
                                            'sequence_number',
                                            'enabled'])
        return body

    def set_default_mandatory_vals(self):
        if not self.has_attr('action'):
            self.attrs['action'] = constants.NAT_ACTION_DNAT


class Tier1NatDef(RouterDef):

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN + "%s/nat"

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id')

    @staticmethod
    def resource_type():
        return 'PolicyNat'


class Tier1NatRule(RouterNatRule):

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN + "%s/nat/%s/nat-rules/"

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'nat_id', 'nat_rule_id')

    def path_defs(self):
        return (TenantDef, Tier1Def, Tier1NatDef)


class RouteAdvertisementRule(object):

    def __init__(self, name, action=constants.ADV_RULE_PERMIT,
                 prefix_operator=constants.ADV_RULE_OPERATOR_GE,
                 route_advertisement_types=None,
                 subnets=None):
        self.name = name
        self.action = action
        self.prefix_operator = prefix_operator
        self.route_advertisement_types = route_advertisement_types
        self.subnets = subnets

    def get_obj_dict(self):
        return {'name': self.name,
                'action': self.action,
                'prefix_operator': self.prefix_operator,
                'route_advertisement_types': self.route_advertisement_types,
                'subnets': self.subnets}


class RouterStaticRoute(ResourceDef):

    @staticmethod
    def resource_type():
        return 'StaticRoutes'

    def get_obj_dict(self):
        body = super(RouterStaticRoute, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['network'])

        # next hops
        if self.has_attr('next_hop'):
            next_hop = self.get_attr('next_hop')
            next_hops = [{'ip_address': next_hop}]
            self._set_attr_if_specified(body, 'next_hop',
                                        body_attr='next_hops',
                                        value=next_hops)
        return body


class Tier1StaticRoute(RouterStaticRoute):

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN + "%s/static-routes/"

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'static_route_id')

    def path_defs(self):
        return (TenantDef, Tier1Def)


class Tier0StaticRoute(RouterStaticRoute):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN + "%s/static-routes/"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'static_route_id')

    def path_defs(self):
        return (TenantDef, Tier0Def)


class Tier0NatDef(RouterDef):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN + "%s/nat"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id')

    @staticmethod
    def resource_type():
        return 'PolicyNat'


class Tier0NatRule(RouterNatRule):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN + "%s/nat/%s/nat-rules/"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'nat_id', 'nat_rule_id')

    def path_defs(self):
        return (TenantDef, Tier0Def, Tier0NatDef)


class Subnet(object):
    def __init__(self, gateway_address, dhcp_ranges=None, dhcp_config=None):
        self.gateway_address = gateway_address
        self.dhcp_ranges = dhcp_ranges
        self.dhcp_config = dhcp_config

    def get_obj_dict(self):
        body = {'gateway_address': self.gateway_address}
        if self.dhcp_ranges:
            body['dhcp_ranges'] = self.dhcp_ranges
        if self.dhcp_config:
            body['dhcp_config'] = (
                self.dhcp_config.get_obj_dict()
                if isinstance(self.dhcp_config, SegmentDhcpConfig)
                else self.dhcp_config)

        return body


class SegmentDhcpConfig(object):
    def __init__(self, server_address=None, dns_servers=None,
                 lease_time=None, options=None, is_ipv6=False):
        if is_ipv6:
            self.resource_type = 'SegmentDhcpV6Config'
        else:
            self.resource_type = 'SegmentDhcpV4Config'

        self.server_address = server_address
        self.dns_servers = dns_servers
        self.lease_time = lease_time
        self.options = options

    def get_obj_dict(self):
        body = {'resource_type': self.resource_type}
        if self.server_address:
            body['server_address'] = self.server_address
        if self.dns_servers:
            body['dns_servers'] = self.dns_servers
        if self.lease_time:
            body['lease_time'] = self.lease_time
        if self.options:
            body['options'] = (
                self.options.get_obj_dict()
                if isinstance(self.options, DhcpOptions)
                else self.options)

        return body


class DhcpOptions(object):
    def __init__(self, option_121=None, others=None, is_ipv6=False):
        if is_ipv6:
            self.resource_type = 'DhcpV6Options'
        else:
            self.resource_type = 'DhcpV4Options'

        self.option_121 = option_121
        self.others = others

    def get_obj_dict(self):
        body = {'resource_type': self.resource_type}
        if self.option_121:
            body['option_121'] = self.option_121
        if self.others:
            body['others'] = self.others

        return body


class InterfaceSubnet(object):
    def __init__(self, ip_addresses, prefix_len):
        self.ip_addresses = ip_addresses
        self.prefix_len = prefix_len

    def get_obj_dict(self):
        body = {'ip_addresses': self.ip_addresses,
                'prefix_len': self.prefix_len}
        return body


class BaseSegmentDef(ResourceDef):

    def get_obj_dict(self):
        body = super(BaseSegmentDef, self).get_obj_dict()
        if self.has_attr('subnets'):
            subnets = []
            if self.get_attr('subnets'):
                subnets = [subnet.get_obj_dict()
                           for subnet in self.get_attr('subnets')]
            self._set_attr_if_specified(body, 'subnets', value=subnets)

        if self.has_attr('ip_pool_id'):
            ip_pool_id = self.get_attr('ip_pool_id')
            adv_cfg = self._get_adv_config(ip_pool_id)
            self._set_attr_if_specified(body, 'ip_pool_id',
                                        body_attr='advanced_config',
                                        value=adv_cfg)
        self._set_attrs_if_specified(body, ['domain_name', 'vlan_ids'])
        return body

    @staticmethod
    def resource_type():
        return 'Segment'

    def _get_adv_config(self, ip_pool_id):
        ip_pool_def = IpPoolDef(ip_pool_id=ip_pool_id)
        ip_pool_path = ip_pool_def.get_resource_full_path()
        return {'address_pool_paths': [ip_pool_path]}


class Tier1SegmentDef(BaseSegmentDef):
    '''Tier1 segments can not move to different tier1 '''

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN + "%s/segments/"

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'segment_id')

    def path_defs(self):
        return (TenantDef, Tier1Def)


class SegmentDef(BaseSegmentDef):
    '''These segments don't belong to particular tier1.

       And can be attached and re-attached to different tier1s
    '''

    @property
    def path_pattern(self):
        return SEGMENTS_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'segment_id')

    def path_defs(self):
        return (TenantDef,)

    @property
    def version_dependant_attr_map(self):
        return {'metadata_proxy_id': nsx_constants.NSX_VERSION_3_0_0,
                'dhcp_server_config_id': nsx_constants.NSX_VERSION_3_0_0,
                'admin_state': nsx_constants.NSX_VERSION_3_0_0}

    def get_obj_dict(self):
        body = super(SegmentDef, self).get_obj_dict()
        if self.has_attr('tier1_id'):
            path = ""
            if self.get_attr('tier1_id'):
                tier1 = Tier1Def(tier1_id=self.get_attr('tier1_id'),
                                 tenant=self.get_tenant())
                path = tier1.get_resource_full_path()
            self._set_attr_if_specified(body, 'tier1_id',
                                        body_attr='connectivity_path',
                                        value=path)

        if self.has_attr('tier0_id'):
            path = ""
            if self.get_attr('tier0_id'):
                tier0 = Tier0Def(tier0_id=self.get_attr('tier0_id'),
                                 tenant=self.get_tenant())
                path = tier0.get_resource_full_path()
            self._set_attr_if_specified(body, 'tier0_id',
                                        body_attr='connectivity_path',
                                        value=path)

        if self.has_attr('transport_zone_id'):
            path = ""
            if self.get_attr('transport_zone_id'):
                tz = TransportZoneDef(
                    tz_id=self.get_attr('transport_zone_id'),
                    ep_id=constants.DEFAULT_ENFORCEMENT_POINT,
                    tenant=self.get_tenant())
                path = tz.get_resource_full_path()
            self._set_attr_if_specified(body, 'transport_zone_id',
                                        body_attr='transport_zone_path',
                                        value=path)

        if (self.has_attr('metadata_proxy_id') and
            self._version_dependant_attr_supported('metadata_proxy_id')):
            # To remove the metadata proxy, paths must be set to None
            paths = None
            if self.get_attr('metadata_proxy_id'):
                mdproxy = MetadataProxyDef(
                    mdproxy_id=self.get_attr('metadata_proxy_id'),
                    tenant=self.get_tenant())
                paths = [mdproxy.get_resource_full_path()]
            self._set_attr_if_specified(body, 'metadata_proxy_id',
                                        body_attr='metadata_proxy_paths',
                                        value=paths)

        # TODO(asarfaty): Also support relay config here
        if (self.has_attr('dhcp_server_config_id') and
            self._version_dependant_attr_supported('dhcp_server_config_id')):
            # To remove the dhcp config, path must be set to None
            path = None
            if self.get_attr('dhcp_server_config_id'):
                dhcp_config = DhcpServerConfigDef(
                    config_id=self.get_attr('dhcp_server_config_id'),
                    tenant=self.get_tenant())
                path = dhcp_config.get_resource_full_path()
            self._set_attr_if_specified(body, 'dhcp_server_config_id',
                                        body_attr='dhcp_config_path',
                                        value=path)

        if (self.has_attr('admin_state') and
            self._version_dependant_attr_supported('admin_state')):
            if self.get_attr('admin_state'):
                admin_state = nsx_constants.ADMIN_STATE_UP
            else:
                admin_state = nsx_constants.ADMIN_STATE_DOWN
            self._set_attr_if_specified(body, 'admin_state',
                                        value=admin_state)

        return body


class DhcpV4StaticBindingConfig(ResourceDef):

    @property
    def path_pattern(self):
        return SEGMENTS_PATH_PATTERN + "%s/dhcp-static-binding-configs/"

    @property
    def path_ids(self):
        return ('tenant', 'segment_id', 'binding_id')

    @staticmethod
    def resource_type():
        return 'DhcpV4StaticBindingConfig'

    def path_defs(self):
        return (TenantDef, SegmentDef)

    def get_obj_dict(self):
        body = super(DhcpV4StaticBindingConfig, self).get_obj_dict()
        # TODO(asarfaty): add object or v4/6 options
        self._set_attrs_if_specified(body,
                                     ['gateway_address',
                                      'host_name',
                                      'ip_address',
                                      'lease_time',
                                      'mac_address',
                                      'options'])
        return body


class DhcpV6StaticBindingConfig(DhcpV4StaticBindingConfig):

    @staticmethod
    def resource_type():
        return 'DhcpV6StaticBindingConfig'

    def path_defs(self):
        return (TenantDef, SegmentDef)

    def get_obj_dict(self):
        body = super(DhcpV6StaticBindingConfig, self).get_obj_dict()
        self._set_attrs_if_specified(body,
                                     ['domain_names',
                                      'dns_nameservers',
                                      'ip_addresses',
                                      'sntp_servers',
                                      'preferred_time'])

        return body


class PortAddressBinding(object):
    def __init__(self, ip_address, mac_address, vlan_id=None):
        self.ip_address = ip_address
        self.mac_address = mac_address
        self.vlan_id = vlan_id

    def get_obj_dict(self):
        data = {'ip_address': self.ip_address,
                'mac_address': self.mac_address}
        if self.vlan_id is not None:
            data['vlan_id'] = self.vlan_id
        return data


class SegmentPortDef(ResourceDef):
    '''Infra segment port'''

    @property
    def path_pattern(self):
        return SEGMENTS_PATH_PATTERN + "%s/ports/"

    @property
    def path_ids(self):
        return ('tenant', 'segment_id', 'port_id')

    @staticmethod
    def resource_type():
        return 'SegmentPort'

    def path_defs(self):
        return (TenantDef, SegmentDef)

    def get_obj_dict(self):
        body = super(SegmentPortDef, self).get_obj_dict()
        address_bindings = self.get_attr('address_bindings')
        if address_bindings:
            body['address_bindings'] = [binding.get_obj_dict()
                                        for binding in address_bindings]
        if (self.has_attr('attachment_type') or self.has_attr('vif_id') or
            self.has_attr('hyperbus_mode')):
            if (not self.get_attr('attachment_type') and
                not self.get_attr('vif_id') and
                not self.get_attr('hyperbus_mode')):
                # detach operation
                body['attachment'] = None
            else:
                attachment = {}
                if self.get_attr('attachment_type'):
                    attachment['type'] = self.get_attr('attachment_type')
                if self.get_attr('vif_id'):
                    attachment['id'] = self.get_attr('vif_id')
                if self.get_attr('hyperbus_mode'):
                    self._set_attr_if_supported(attachment, 'hyperbus_mode')

                self._set_attrs_if_specified(attachment,
                                             ['context_id',
                                              'app_id',
                                              'traffic_tag',
                                              'allocate_addresses'])

                body['attachment'] = attachment

        if (self.has_attr('admin_state') and
            self._version_dependant_attr_supported('admin_state')):
            if self.get_attr('admin_state'):
                admin_state = nsx_constants.ADMIN_STATE_UP
            else:
                admin_state = nsx_constants.ADMIN_STATE_DOWN
            self._set_attr_if_specified(body, 'admin_state',
                                        value=admin_state)

        return body

    @property
    def version_dependant_attr_map(self):
        return {'hyperbus_mode': nsx_constants.NSX_VERSION_3_0_0,
                'admin_state': nsx_constants.NSX_VERSION_3_0_0}


class SegmentBindingMapDefBase(ResourceDef):

    @property
    def path_ids(self):
        return ('tenant', 'segment_id', 'map_id')

    def path_defs(self):
        return (TenantDef, SegmentDef)


class SegmentSecProfilesBindingMapDef(SegmentBindingMapDefBase):
    @property
    def path_pattern(self):
        return (SEGMENTS_PATH_PATTERN +
                "%s/segment-security-profile-binding-maps/")

    @staticmethod
    def resource_type():
        return 'SegmentSecurityProfileBindingMap'

    def get_obj_dict(self):
        body = super(SegmentSecProfilesBindingMapDef, self).get_obj_dict()

        if self.has_attr('segment_security_profile_id'):
            path = ""
            if self.get_attr('segment_security_profile_id'):
                profile = SegmentSecurityProfileDef(
                    profile_id=self.get_attr('segment_security_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'segment_security_profile_id',
                body_attr='segment_security_profile_path',
                value=path)

        if self.has_attr('spoofguard_profile_id'):
            path = ""
            if self.get_attr('spoofguard_profile_id'):
                profile = SpoofguardProfileDef(
                    profile_id=self.get_attr('spoofguard_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'spoofguard_profile_id',
                body_attr='spoofguard_profile_path',
                value=path)

        return body


class SegmentPortBindingMapDefBase(ResourceDef):

    @property
    def path_ids(self):
        return ('tenant', 'segment_id', 'port_id', 'map_id')

    def path_defs(self):
        return (TenantDef, SegmentDef, SegmentPortDef)


class SegmentPortSecProfilesBindingMapDef(SegmentPortBindingMapDefBase):
    @property
    def path_pattern(self):
        return (SEGMENTS_PATH_PATTERN +
                "%s/ports/%s/port-security-profile-binding-maps/")

    @staticmethod
    def resource_type():
        return 'PortSecurityProfileBindingMap'

    def get_obj_dict(self):
        body = super(SegmentPortSecProfilesBindingMapDef, self).get_obj_dict()

        if self.has_attr('segment_security_profile_id'):
            path = ""
            if self.get_attr('segment_security_profile_id'):
                profile = SegmentSecurityProfileDef(
                    profile_id=self.get_attr('segment_security_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'segment_security_profile_id',
                body_attr='segment_security_profile_path',
                value=path)

        if self.has_attr('spoofguard_profile_id'):
            path = ""
            if self.get_attr('spoofguard_profile_id'):
                profile = SpoofguardProfileDef(
                    profile_id=self.get_attr('spoofguard_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'spoofguard_profile_id',
                body_attr='spoofguard_profile_path',
                value=path)

        return body


class SegmentPortDiscoveryProfilesBindingMapDef(SegmentPortBindingMapDefBase):
    @property
    def path_pattern(self):
        return (SEGMENTS_PATH_PATTERN +
                "%s/ports/%s/port-discovery-profile-binding-maps/")

    @staticmethod
    def resource_type():
        return 'PortDiscoveryProfileBindingMap'

    def get_obj_dict(self):
        body = super(SegmentPortDiscoveryProfilesBindingMapDef,
                     self).get_obj_dict()

        if self.has_attr('mac_discovery_profile_id'):
            path = ""
            if self.get_attr('mac_discovery_profile_id'):
                profile = MacDiscoveryProfileDef(
                    profile_id=self.get_attr('mac_discovery_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'mac_discovery_profile_id',
                body_attr='mac_discovery_profile_path',
                value=path)

        if self.has_attr('ip_discovery_profile_id'):
            path = ""
            if self.get_attr('ip_discovery_profile_id'):
                profile = IpDiscoveryProfileDef(
                    profile_id=self.get_attr('ip_discovery_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'ip_discovery_profile_id',
                body_attr='ip_discovery_profile_path',
                value=path)

        return body


class SegmentPortQoSProfilesBindingMapDef(SegmentPortBindingMapDefBase):
    @property
    def path_pattern(self):
        return (SEGMENTS_PATH_PATTERN +
                "%s/ports/%s/port-qos-profile-binding-maps/")

    @staticmethod
    def resource_type():
        return 'PortQoSProfileBindingMap'

    def get_obj_dict(self):
        body = super(SegmentPortQoSProfilesBindingMapDef,
                     self).get_obj_dict()

        if self.has_attr('qos_profile_id'):
            path = ""
            if self.get_attr('qos_profile_id'):
                profile = QosProfileDef(
                    profile_id=self.get_attr('qos_profile_id'),
                    tenant=self.get_tenant())
                path = profile.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'qos_profile_id',
                body_attr='qos_profile_path',
                value=path)

        return body


class Tier1SegmentPortDef(SegmentPortDef):
    '''Tier1 segment port'''

    @property
    def path_pattern(self):
        return TIER1S_PATH_PATTERN + "%s/segments/%s/ports/"

    @property
    def path_ids(self):
        return ('tenant', 'tier1_id', 'segment_id', 'port_id')

    def path_defs(self):
        return (TenantDef, Tier1Def, SegmentDef)


class IpBlockDef(ResourceDef):
    '''Infra IpBlock'''

    @property
    def path_pattern(self):
        return IP_BLOCKS_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'ip_block_id')

    @staticmethod
    def resource_type():
        return 'IpAddressBlock'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(IpBlockDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'cidr')
        return body


class IpPoolDef(ResourceDef):
    '''Infra IpPool'''

    @property
    def path_pattern(self):
        return IP_POOLS_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'ip_pool_id')

    @staticmethod
    def resource_type():
        return 'IpAddressPool'

    def path_defs(self):
        return (TenantDef,)


class IpPoolAllocationDef(ResourceDef):
    '''Infra IpPoolAllocation'''

    @property
    def path_pattern(self):
        return IP_POOLS_PATH_PATTERN + "%s/ip-allocations/"

    @property
    def path_ids(self):
        return ('tenant', 'ip_pool_id', 'ip_allocation_id')

    @staticmethod
    def resource_type():
        return 'IpAddressAllocation'

    def path_defs(self):
        return (TenantDef, IpPoolDef)

    def get_obj_dict(self):
        body = super(IpPoolAllocationDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'allocation_ip')
        return body


class IpPoolSubnetDef(ResourceDef):
    '''Infra IpPool Subnet'''

    @property
    def path_pattern(self):
        return IP_POOLS_PATH_PATTERN + "%s/ip-subnets/"

    @property
    def path_ids(self):
        return ('tenant', 'ip_pool_id', 'ip_subnet_id')

    @classmethod
    def resource_class(cls):
        return 'IpAddressPoolSubnet'

    def path_defs(self):
        return (TenantDef, IpPoolDef)


class IpPoolBlockSubnetDef(IpPoolSubnetDef):
    '''Infra IpPoolSubnet belonging to IpBlock'''

    @staticmethod
    def resource_type():
        return 'IpAddressPoolBlockSubnet'

    def get_obj_dict(self):
        body = super(IpPoolBlockSubnetDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['auto_assign_gateway', 'size'])
        if self.has_attr('ip_block_id'):
            # Format the IP Block ID to its path
            ip_block_id = self.get_attr('ip_block_id')
            ip_block_def = IpBlockDef(ip_block_id=ip_block_id,
                                      tenant=self.get_tenant())
            ip_block_path = ip_block_def.get_resource_full_path()
            self._set_attr_if_specified(
                body, 'ip_block_id', body_attr='ip_block_path',
                value=ip_block_path)
        self._set_attr_if_supported(body, 'start_ip')
        return body

    @property
    def version_dependant_attr_map(self):
        return {'start_ip': nsx_constants.NSX_VERSION_3_0_0}


class IpPoolStaticSubnetDef(IpPoolSubnetDef):
    '''Infra IpPool static subnet'''

    @staticmethod
    def resource_type():
        return 'IpAddressPoolStaticSubnet'

    def get_obj_dict(self):
        body = super(IpPoolStaticSubnetDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['cidr',
                                            'allocation_ranges',
                                            'gateway_ip'])
        return body


class Condition(object):
    def __init__(self, value, key=constants.CONDITION_KEY_TAG,
                 member_type=constants.CONDITION_MEMBER_PORT,
                 operator=constants.CONDITION_OP_EQUALS):
        self.value = value
        self.key = key
        self.member_type = member_type
        self.operator = operator

    def get_obj_dict(self):
        return {'resource_type': 'Condition',
                'member_type': self.member_type,
                'key': self.key,
                'value': self.value,
                'operator': self.operator}


class IPAddressExpression(object):
    def __init__(self, ip_addresses):
        self.ip_addresses = ip_addresses

    def get_obj_dict(self):
        return {'resource_type': 'IPAddressExpression',
                'ip_addresses': self.ip_addresses}


class PathExpression(object):
    def __init__(self, paths):
        self.paths = paths

    def get_obj_dict(self):
        return {'resource_type': 'PathExpression',
                'paths': self.paths}


class ConjunctionOperator(object):
    def __init__(self, operator=constants.CONDITION_OP_AND):
        self.operator = operator

    def get_obj_dict(self):
        return {'resource_type': 'ConjunctionOperator',
                'conjunction_operator': self.operator}


class NestedExpression(object):
    def __init__(self, expressions=None):
        self.expressions = expressions or []

    def get_obj_dict(self):
        return {'resource_type': 'NestedExpression',
                'expressions': [ex.get_obj_dict() for ex in self.expressions]}


class GroupDef(ResourceDef):

    @property
    def path_pattern(self):
        return DOMAINS_PATH_PATTERN + "%s/groups/"

    @property
    def path_ids(self):
        return ('tenant', 'domain_id', 'group_id')

    @staticmethod
    def resource_type():
        return 'Group'

    def path_defs(self):
        return (TenantDef, DomainDef)

    def get_obj_dict(self):
        body = super(GroupDef, self).get_obj_dict()
        conds = self.get_attr('conditions')
        # If conditions were IGNORE, conds would be None here.
        # Otherwise, conds could be an empty list which denotes
        # updating group expression to empty list.
        if conds is not None:
            conds = conds if isinstance(conds, list) else [conds]
            body['expression'] = [condition.get_obj_dict()
                                  for condition in conds]
        return body


class ServiceDef(ResourceDef):
    def __init__(self, **kwargs):
        super(ServiceDef, self).__init__(**kwargs)
        self.service_entries = []

    @property
    def path_pattern(self):
        return SERVICES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'service_id')

    @staticmethod
    def resource_type():
        return 'Service'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(ServiceDef, self).get_obj_dict()
        entries = [entry.get_obj_dict()
                   for entry in self.service_entries]
        if entries:
            body['service_entries'] = entries
        return body

    @staticmethod
    def sub_entries_path():
        return ServiceEntryDef().get_last_section_dict_key


class ServiceEntryDef(ResourceDef):

    @property
    def path_pattern(self):
        return SERVICES_PATH_PATTERN + "%s/service-entries/"

    @property
    def path_ids(self):
        return ('tenant', 'service_id', 'entry_id')

    def path_defs(self):
        return (TenantDef, ServiceDef)

    @classmethod
    def resource_class(cls):
        return 'ServiceEntry'


class L4ServiceEntryDef(ServiceEntryDef):

    @staticmethod
    def resource_type():
        return 'L4PortSetServiceEntry'

    def get_obj_dict(self):
        body = super(L4ServiceEntryDef, self).get_obj_dict()

        self._set_attr_if_specified(body, 'protocol', 'l4_protocol')
        self._set_attr_if_specified(body, 'dest_ports', 'destination_ports')
        self._set_attr_if_specified(body, 'source_ports', 'source_ports')
        return body


class IcmpServiceEntryDef(ServiceEntryDef):

    @staticmethod
    def resource_type():
        return 'ICMPTypeServiceEntry'

    def get_obj_dict(self):
        body = super(IcmpServiceEntryDef, self).get_obj_dict()

        if self.get_attr('version'):
            body['protocol'] = 'ICMPv' + str(self.get_attr('version'))

        for attr in ('icmp_type', 'icmp_code'):
            # Note that icmp_type and icmp_code could be 0.
            if self.get_attr(attr) is not None:
                body[attr] = self.get_attr(attr)
        return body


class IPProtocolServiceEntryDef(ServiceEntryDef):

    @staticmethod
    def resource_type():
        return 'IPProtocolServiceEntry'

    def get_obj_dict(self):
        body = super(IPProtocolServiceEntryDef, self).get_obj_dict()
        if self.get_attr('protocol_number') is not None:
            # Note that protocol_number could be 0.
            body['protocol_number'] = self.get_attr('protocol_number')
        return body


class SecurityPolicyBaseDef(ResourceDef):

    @property
    def path_ids(self):
        return ('tenant', 'domain_id', 'map_id')

    def path_defs(self):
        return (TenantDef, DomainDef)

    def get_obj_dict(self):
        body = super(SecurityPolicyBaseDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'category')
        if self.has_attr('map_sequence_number'):
            seq_number = self.get_attr('map_sequence_number')
            self._set_attr_if_specified(body, 'map_sequence_number',
                                        body_attr='sequence_number',
                                        value=seq_number)
        return body


class CommunicationMapDef(SecurityPolicyBaseDef):
    """AKA security policy"""
    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN + "%s/security-policies/")

    @staticmethod
    def resource_type():
        return 'SecurityPolicy'

    @staticmethod
    def sub_entries_path():
        return CommunicationMapEntryDef().get_last_section_dict_key


class GatewayPolicyDef(SecurityPolicyBaseDef):
    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN + "%s/gateway-policies/")

    @staticmethod
    def resource_type():
        return 'GatewayPolicy'

    @staticmethod
    def sub_entries_path():
        return GatewayPolicyRuleDef().get_last_section_dict_key


class SecurityPolicyRuleBaseDef(ResourceDef):
    def get_groups_path(self, domain_id, group_ids):
        if not group_ids:
            return [constants.ANY_GROUP]
        return [GroupDef(domain_id=domain_id,
                         group_id=group_id,
                         tenant=self.get_tenant()).get_resource_full_path()
                for group_id in group_ids]

    def get_service_path(self, service_id):
        return ServiceDef(
            service_id=service_id,
            tenant=self.get_tenant()).get_resource_full_path()

    def get_services_path(self, service_ids):
        if service_ids:
            return [self.get_service_path(service_id)
                    for service_id in service_ids]

        return [constants.ANY_SERVICE]

    @property
    def path_ids(self):
        return ('tenant', 'domain_id', 'map_id', 'entry_id')

    @staticmethod
    def resource_type():
        return 'Rule'

    def get_obj_dict(self):
        body = super(SecurityPolicyRuleBaseDef, self).get_obj_dict()
        domain_id = self.get_attr('domain_id')
        if self.has_attr('source_groups'):
            body['source_groups'] = self.get_groups_path(
                domain_id, self.get_attr('source_groups'))
        if self.has_attr('dest_groups'):
            body['destination_groups'] = self.get_groups_path(
                domain_id, self.get_attr('dest_groups'))

        self._set_attrs_if_specified(body, ['sequence_number', 'scope',
                                            'action', 'direction', 'logged',
                                            'ip_protocol', 'tag'])

        if self.has_attr('service_ids'):
            service_ids = self.get_attr('service_ids')
            body['services'] = self.get_services_path(service_ids)

        self._set_attr_if_supported(body, 'service_entries')
        return body

    @classmethod
    def adapt_from_rule_dict(cls, rule_dict, domain_id, map_id):
        entry_id = rule_dict.pop('id', None)
        name = rule_dict.pop('display_name', None)

        rule_def = cls(tenant=constants.POLICY_INFRA_TENANT,
                       domain_id=domain_id, map_id=map_id, entry_id=entry_id,
                       name=name)
        rule_def.set_obj_dict(rule_dict)
        return rule_def

    @property
    def version_dependant_attr_map(self):
        return {'service_entries': nsx_constants.NSX_VERSION_3_0_0}


class CommunicationMapEntryDef(SecurityPolicyRuleBaseDef):

    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN +
                "%s/security-policies/%s/rules/")

    def path_defs(self):
        return (TenantDef, DomainDef, CommunicationMapDef)


class GatewayPolicyRuleDef(SecurityPolicyRuleBaseDef):

    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN +
                "%s/gateway-policies/%s/rules/")

    def path_defs(self):
        return (TenantDef, DomainDef, GatewayPolicyDef)


# Currently supports only NSXT
class EnforcementPointDef(ResourceDef):

    @property
    def path_pattern(self):
        return ENFORCEMENT_POINT_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'ep_id')

    @staticmethod
    def resource_type():
        return 'EnforcementPoint'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(EnforcementPointDef, self).get_obj_dict()
        body['id'] = self.get_id()
        if 'connection_info' not in body:
            body['connection_info'] = {'resource_type': 'NSXTConnectionInfo'}

        info = body['connection_info']
        self._set_attrs_if_specified(info,
                                     ['thumbprint', 'username', 'password',
                                      'ip_address'])

        if self.get_attr('ip_address'):
            info['enforcement_point_address'] = self.get_attr('ip_address')

        if self.get_attr('edge_cluster_id'):
            body['connection_info']['edge_cluster_ids'] = [
                self.get_attr('edge_cluster_id')]

        if self.get_attr('transport_zone_id'):
            body['connection_info']['transport_zone_ids'] = [
                self.get_attr('transport_zone_id')]

        return body


class TransportZoneDef(ResourceDef):

    @property
    def path_pattern(self):
        return TRANSPORT_ZONE_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'ep_id', 'tz_id')

    @staticmethod
    def resource_type():
        return 'PolicyTransportZone'

    @staticmethod
    def resource_use_cache():
        return True


class EdgeClusterDef(ResourceDef):

    @property
    def path_pattern(self):
        return EDGE_CLUSTER_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'ep_id', 'ec_id')

    @staticmethod
    def resource_type():
        return 'PolicyEdgeCluster'

    @staticmethod
    def resource_use_cache():
        return True


class EdgeClusterNodeDef(ResourceDef):

    @property
    def path_pattern(self):
        return (EDGE_CLUSTER_PATTERN + '%s/edge-nodes/')

    @property
    def path_ids(self):
        return ('tenant', 'ep_id', 'ec_id', 'node_id')

    @staticmethod
    def resource_type():
        return 'PolicyEdgeNode'

    @staticmethod
    def resource_use_cache():
        return True


# Currently assumes one deployment point per id
class DeploymentMapDef(ResourceDef):

    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN + '%s/domain-deployment-maps/')

    @property
    def path_ids(self):
        return ('tenant', 'domain_id', 'map_id')

    @staticmethod
    def resource_type():
        return 'DeploymentMap'

    def path_defs(self):
        return (TenantDef, DomainDef)

    def get_obj_dict(self):
        body = super(DeploymentMapDef, self).get_obj_dict()
        body['id'] = self.get_id()
        ep_id = self.get_attr('ep_id')
        tenant = self.get_tenant()
        body['enforcement_point_path'] = EnforcementPointDef(
            ep_id=ep_id,
            tenant=tenant).get_resource_full_path() if ep_id else None
        return body


class SegmentSecurityProfileDef(ResourceDef):
    DEFAULT_PROFILE = 'default-segment-security-profile'

    @property
    def path_pattern(self):
        return SEGMENT_SECURITY_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'SegmentSecurityProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(SegmentSecurityProfileDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['bpdu_filter_enable',
                                            'dhcp_client_block_enabled',
                                            'dhcp_client_block_v6_enabled',
                                            'dhcp_server_block_enabled',
                                            'dhcp_server_block_v6_enabled',
                                            'non_ip_traffic_block_enabled',
                                            'ra_guard_enabled',
                                            'rate_limits_enabled'])
        return body


class QoSObjectBase(object):

    keys = []

    def __init__(self, **kwargs):
        self.attrs = kwargs

    def get_obj_dict(self):
        obj_dict = {}
        for key in self.attrs:
            if key in self.keys:
                obj_dict[key] = self.attrs[key]
        return obj_dict


class QoSRateLimiter(QoSObjectBase):

    INGRESS_RATE_LIMITER_TYPE = 'IngressRateLimiter'
    EGRESS_RATE_LIMITER_TYPE = 'EgressRateLimiter'
    INGRESS_BRD_RATE_LIMITER_TYPE = 'IngressBroadcastRateLimiter'

    keys = ['resource_type',
            'average_bandwidth',  # Mb/s
            'peak_bandwidth',  # Mb/s
            'burst_size',  # byes
            'enabled'
            ]


class QoSDscp(QoSObjectBase):
    QOS_DSCP_TRUSTED = 'TRUSTED'
    QOS_DSCP_UNTRUSTED = 'UNTRUSTED'
    keys = ['mode', 'priority']


class QosProfileDef(ResourceDef):
    @property
    def path_pattern(self):
        return QOS_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'QoSProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(QosProfileDef, self).get_obj_dict()

        self._set_attr_if_specified(body, 'class_of_service')

        if self.has_attr('dscp'):
            value = None
            if self.get_attr('dscp'):
                value = self.get_attr('dscp').get_obj_dict()
            self._set_attr_if_specified(body, 'dscp', value=value)

        if self.has_attr('shaper_configurations'):
            value = None
            if self.get_attr('shaper_configurations'):
                value = [s.get_obj_dict()
                         for s in self.get_attr('shaper_configurations')]
            self._set_attr_if_specified(body, 'shaper_configurations',
                                        value=value)

        return body


class SpoofguardProfileDef(ResourceDef):
    DEFAULT_PROFILE = 'default-spoofguard-profile'

    @property
    def path_pattern(self):
        return SPOOFGUARD_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'SpoofGuardProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(SpoofguardProfileDef, self).get_obj_dict()
        # TODO(asarfaty): add all attributes here
        self._set_attr_if_specified(body, 'address_binding_whitelist')
        return body


class IpDiscoveryProfileDef(ResourceDef):
    DEFAULT_PROFILE = 'default-ip-discovery-profile'

    @property
    def path_pattern(self):
        return IP_DISCOVERY_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'IPDiscoveryProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(IpDiscoveryProfileDef, self).get_obj_dict()
        # TODO(asarfaty): add all attributes here. currently used for read only
        return body


class MacDiscoveryProfileDef(ResourceDef):
    DEFAULT_PROFILE = 'default-mac-discovery-profile'

    @property
    def path_pattern(self):
        return MAC_DISCOVERY_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'MacDiscoveryProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(MacDiscoveryProfileDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['mac_change_enabled',
                                            'mac_learning_enabled',
                                            'unknown_unicast_flooding_enabled',
                                            'mac_limit_policy', 'mac_limit'])
        return body


class Ipv6NdraProfileDef(ResourceDef):

    @property
    def path_pattern(self):
        return IPV6_NDRA_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'Ipv6NdraProfile'

    @staticmethod
    def default_profile():
        return 'default'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(Ipv6NdraProfileDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['ra_mode',
                                            'reachable_timer',
                                            'retransmit_interval'])
        # Use default settings for dns and RA for now
        # TODO(annak): expose when required
        body['dns_config'] = {}
        body['ra_config'] = {}
        return body


class DhcpRelayConfigDef(ResourceDef):

    @property
    def path_pattern(self):
        return DHCP_REALY_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'config_id')

    @staticmethod
    def resource_type():
        return 'DhcpRelayConfig'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(DhcpRelayConfigDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'server_addresses')
        return body


class DhcpServerConfigDef(ResourceDef):

    @property
    def path_pattern(self):
        return DHCP_SERVER_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'config_id')

    @staticmethod
    def resource_type():
        return 'DhcpServerConfig'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(DhcpServerConfigDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['edge_cluster_path',
                                            'server_addresses',
                                            'lease_time'])
        return body


class WAFProfileDef(ResourceDef):
    @property
    def path_pattern(self):
        return WAF_PROFILES_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'profile_id')

    @staticmethod
    def resource_type():
        return 'WAFProfile'

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(WAFProfileDef, self).get_obj_dict()
        # TODO(asarfaty): add all attributes here.
        # Currently used for read only
        return body


class MetadataProxyDef(ResourceDef):

    @property
    def path_pattern(self):
        return MDPROXY_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'mdproxy_id')

    @staticmethod
    def resource_type():
        return 'MetadataProxyConfig'

    @staticmethod
    def resource_use_cache():
        return True

    def path_defs(self):
        return (TenantDef,)

    def get_obj_dict(self):
        body = super(MetadataProxyDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['edge_cluster_path',
                                            'enable_standby_relocation',
                                            'secret', 'server_address'])
        return body


class CertificateDef(ResourceDef):

    @property
    def path_pattern(self):
        return CERTIFICATE_PATH_PATTERN

    @property
    def path_ids(self):
        return ('tenant', 'certificate_id')

    @staticmethod
    def resource_type():
        return "TlsTrustData"

    def get_obj_dict(self):
        body = super(CertificateDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['pem_encoded', 'key_algo',
                                            'private_key', 'passphrase'])
        return body


class GlobalConfigDef(ResourceDef):

    @property
    def path_pattern(self):
        return GLOBAL_CONFIG_PATH_PATTERN

    @property
    def path_ids(self):
        # Adding dummy 2nd key to satisfy get_section_path
        # This resource has no keys, since it is a single object
        return ('tenant', 'dummy')

    @staticmethod
    def resource_type():
        return "GlobalConfig"

    def get_obj_dict(self):
        body = super(GlobalConfigDef, self).get_obj_dict()
        self._set_attrs_if_specified(body, ['l3_forwarding_mode'])
        return body


class ExcludeListDef(ResourceDef):

    @property
    def path_pattern(self):
        return EXCLUDE_LIST_PATH_PATTERN

    @property
    def path_ids(self):
        # Adding dummy 2nd key to satisfy get_section_path
        # This resource has no keys, since it is a single object
        return ('tenant', 'Dummy')

    @staticmethod
    def resource_type():
        return "PolicyExcludeList"

    def get_obj_dict(self):
        body = super(ExcludeListDef, self).get_obj_dict()
        self._set_attr_if_specified(body, 'members')
        return body


class NsxPolicyApi(object):

    def __init__(self, client):
        self.client = client
        self.cache = utils.NsxLibCache(utils.DEFAULT_CACHE_AGE_SEC)
        self.partial_updates = True

    def disable_partial_updates(self):
        self.partial_updates = False

    def partial_updates_supported(self):
        return self.partial_updates

    def create_or_update(self, resource_def, partial_updates=False):
        """Create or update a policy object.

        This api will update an existing object, or create a new one if it
        doesn't exist.
        The policy API supports PATCH for create/update operations
        """
        path = resource_def.get_resource_path()
        if resource_def.resource_use_cache():
            self.cache.remove(path)
        body = resource_def.get_obj_dict()

        headers = None
        if partial_updates:
            headers = {'nsx-enable-partial-patch': 'true'}
        self.client.patch(path, body, headers=headers)

    def create_with_parent(self, parent_def, resource_def):
        path = parent_def.get_resource_path()
        body = parent_def.get_obj_dict()
        if isinstance(resource_def, list):
            child_dict_key = resource_def[0].get_last_section_dict_key
            body[child_dict_key] = [r.get_obj_dict() for r in resource_def]
        else:
            child_dict_key = resource_def.get_last_section_dict_key
            body[child_dict_key] = [resource_def.get_obj_dict()]

        self.client.patch(path, body)

    def delete(self, resource_def):
        path = resource_def.get_resource_path()
        if resource_def.resource_use_cache():
            self.cache.remove(path)
        self.client.delete(path)

    def get(self, resource_def, silent=False):
        path = resource_def.get_resource_path()
        if resource_def.resource_use_cache():
            # try to get it from the cache
            result = self.cache.get(path)
            if result:
                return result
        # call the client
        result = self.client.get(path, silent=silent)
        if resource_def.resource_use_cache():
            # add the result to the cache
            self.cache.update(path, result)
        return result

    def list(self, resource_def, silent=False):
        path = resource_def.get_section_path()
        return self.client.list(path, silent=silent)

    def get_realized_entities(self, path, silent=False):
        return self.client.list(REALIZATION_PATH % path,
                                silent=silent)['results']

    def get_realized_entity(self, path, silent=False):
        # Return first realization entity if exists
        # Useful for resources with single realization entity
        entities = self.get_realized_entities(path, silent=silent)
        if entities:
            return entities[0]

    def get_realized_state(self, path, silent=False):
        entity = self.get_realized_entity(path, silent=silent)
        if entity:
            return entity['state']


class RouteMapEntry(object):
    def __init__(self, action, community_list_matches=None,
                 prefix_list_matches=None, entry_set=None):
        self.action = action
        self.community_list_matches = community_list_matches
        self.prefix_list_matches = prefix_list_matches
        self.entry_set = entry_set

    def get_obj_dict(self):
        body = {'action': self.action}
        if self.community_list_matches:
            body['community_list_matches'] = [community.get_obj_dict()
                                              for community in
                                              self.community_list_matches]
        if self.prefix_list_matches:
            body['prefix_list_matches'] = (
                self.prefix_list_matches
                if isinstance(self.prefix_list_matches, list) else
                [self.prefix_list_matches])
        if self.entry_set:
            body['set'] = self.entry_set.get_obj_dict()
        return body


class RouteMapEntrySet(object):
    def __init__(self, local_preference=100, as_path_prepend=None,
                 community=None, med=None, weight=None):
        self.local_preference = local_preference
        self.as_path_prepend = as_path_prepend
        self.community = community
        self.med = med
        self.weight = weight

    def get_obj_dict(self):
        body = {'local_preference': self.local_preference}
        if self.as_path_prepend:
            body['as_path_prepend'] = self.as_path_prepend
        if self.community:
            body['community'] = self.community
        if self.med:
            body['med'] = self.med
        if self.weight:
            body['weight'] = self.weight
        return body


class CommunityMatchCriteria(object):
    def __init__(self, criteria, match_operator=None):
        self.criteria = criteria
        self.match_operator = match_operator

    def get_obj_dict(self):
        body = {'criteria': self.criteria}
        if self.match_operator:
            body['match_operator'] = self.match_operator
        return body


class Tier0RouteMapDef(ResourceDef):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN + "%s/route-maps/"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'route_map_id')

    @staticmethod
    def resource_type():
        return 'Tier0RouteMap'

    def path_defs(self):
        return (TenantDef, Tier0Def)

    def get_obj_dict(self):
        body = super(Tier0RouteMapDef, self).get_obj_dict()
        entries = self.get_attr('entries')
        if entries:
            entries = [entry.get_obj_dict()
                       if isinstance(entry, RouteMapEntry) else entry
                       for entry in self.get_attr('entries')]
            body['entries'] = entries
        return body


class PrefixEntry(object):
    def __init__(self, network, le=None, ge=None,
                 action=constants.ADV_RULE_PERMIT):
        self.network = network
        self.le = le
        self.ge = ge
        self.action = action

    def get_obj_dict(self):
        body = {'network': self.network,
                'action': self.action}
        if self.le is not None:
            body['le'] = self.le
        if self.ge is not None:
            body['ge'] = self.ge

        return body


class Tier0PrefixListDef(ResourceDef):

    @property
    def path_pattern(self):
        return TIER0S_PATH_PATTERN + "%s/prefix-lists/"

    @property
    def path_ids(self):
        return ('tenant', 'tier0_id', 'prefix_list_id')

    @staticmethod
    def resource_type():
        return 'PrefixList'

    def path_defs(self):
        return (TenantDef, Tier0Def)

    def get_obj_dict(self):
        body = super(Tier0PrefixListDef, self).get_obj_dict()
        prefixes = self.get_attr('prefixes')
        if prefixes:
            prefixes = [prefix.get_obj_dict() for prefix in prefixes]
            body['prefixes'] = prefixes
        return body


class BgpRoutingConfigDef(ResourceDef):

    @staticmethod
    def resource_type():
        return 'BgpRoutingConfig'

    @property
    def path_pattern(self):
        return TIER0_LOCALE_SERVICES_PATH_PATTERN + "%s/bgp"

    @property
    def path_ids(self):
        # Adding dummy key to satisfy get_section_path
        # This resource has no keys, since it is a single object
        return ('tenant', 'tier0_id', 'service_id', 'dummy')


class Tier0RouteRedistributionConfig(object):

    def __init__(self, enabled=None, redistribution_rules=None):
        self.enabled = enabled
        self.redistribution_rules = redistribution_rules

    def get_obj_dict(self):
        body = {}
        if self.enabled:
            body['enabled'] = self.enabled
        if self.redistribution_rules is not None:
            rules = [rule.get_obj_dict()
                     if isinstance(rule, Tier0RouteRedistributionRule) else
                     rule for rule in self.redistribution_rules]
            body['redistribution_rules'] = rules

        return body


class Tier0RouteRedistributionRule(object):

    def __init__(self, name=None, route_redistribution_types=None,
                 route_map_path=None):
        self.name = name
        self.route_redistribution_types = route_redistribution_types or []
        self.route_map_path = route_map_path

    def get_obj_dict(self):
        body = {'route_redistribution_types': self.route_redistribution_types}
        if self.name:
            body['name'] = self.name
        if self.route_map_path:
            body['route_map_path'] = self.route_map_path

        return body
