# 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

import six

from vmware_nsxlib.v3 import policy_constants

TENANTS_PATH_PATTERN = "%s/"
DOMAINS_PATH_PATTERN = TENANTS_PATH_PATTERN + "domains/"
COMM_PROF_PATH_PATTERN = TENANTS_PATH_PATTERN + "communication-profiles/"
SERVICES_PATH_PATTERN = TENANTS_PATH_PATTERN + "services/"
REALIZED_STATE_EF = (TENANTS_PATH_PATTERN +
                     "realized-state/enforcement-points/%s/")
REALIZED_STATE_GROUP = REALIZED_STATE_EF + "groups/nsgroups/%s-%s"
REALIZED_STATE_COMM_MAP = REALIZED_STATE_EF + "firewalls/firewall-sections/%s"
REALIZED_STATE_SERVICE = REALIZED_STATE_EF + "services/nsservices/services:%s"


@six.add_metaclass(abc.ABCMeta)
class ResourceDef(object):
    def __init__(self):
        self.tenant = None
        self.id = None
        self.name = None
        self.description = None
        self.parent_ids = None
        self.body = {}

    def get_obj_dict(self):
        body = {'display_name': self.name,
                'description': self.description}
        if self.id:
            body['id'] = self.id
        return body

    @abc.abstractproperty
    def path_pattern(self):
        pass

    def get_section_path(self):
        return self.path_pattern % self.parent_ids

    def get_resource_path(self):
        if self.id:
            return self.get_section_path() + self.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

    def update_attributes_in_body(self, **kwargs):
        self.body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        for key, value in six.iteritems(kwargs):
            if key == 'body':
                continue
            if value is not None:
                if key == 'name':
                    self.body['display_name'] = value
                else:
                    self.body[key] = value
        entries_path = self.sub_entries_path()
        # make sure service entries are there
        if entries_path and entries_path not in self.body:
            self.body[entries_path] = []

    @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]


class DomainDef(ResourceDef):

    def __init__(self,
                 domain_id=None,
                 name=None,
                 description=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(DomainDef, self).__init__()
        self.tenant = tenant
        self.id = domain_id
        self.name = name
        self.description = description
        self.parent_ids = (tenant)

    @property
    def path_pattern(self):
        return DOMAINS_PATH_PATTERN


class Condition(object):
    def __init__(self, value, key=policy_constants.CONDITION_KEY_TAG,
                 member_type=policy_constants.CONDITION_MEMBER_PORT,
                 operator=policy_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 GroupDef(ResourceDef):
    def __init__(self,
                 domain_id=None,
                 group_id=None,
                 name=None,
                 description=None,
                 conditions=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(GroupDef, self).__init__()
        self.tenant = tenant
        self.id = group_id
        self.name = name
        self.description = description
        self.domain_id = domain_id
        self.parent_ids = (tenant, domain_id)
        if conditions and isinstance(conditions, Condition):
            self.conditions = [conditions]
        else:
            self.conditions = conditions

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

    def get_obj_dict(self):
        body = super(GroupDef, self).get_obj_dict()
        if self.conditions:
            body['expression'] = [condition.get_obj_dict()
                                  for condition in self.conditions]
        return body

    def update_attributes_in_body(self, **kwargs):
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        # Fix params that need special conversions
        if kwargs.get('conditions') is not None:
            body['expression'] = [cond.get_obj_dict()
                                  for cond in kwargs['conditions']]
            del kwargs['conditions']
        super(GroupDef, self).update_attributes_in_body(body=body, **kwargs)

    def get_realized_state_path(self, ep_id):
        return REALIZED_STATE_GROUP % (self.tenant, ep_id,
                                       self.domain_id, self.id)


class ServiceDef(ResourceDef):
    def __init__(self,
                 service_id=None,
                 name=None,
                 description=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(ServiceDef, self).__init__()
        self.tenant = tenant
        self.id = service_id
        self.name = name
        self.description = description
        self.parent_ids = (tenant)
        self.service_entries = []

    @property
    def path_pattern(self):
        return SERVICES_PATH_PATTERN

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

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

    def get_realized_state_path(self, ep_id):
        return REALIZED_STATE_SERVICE % (self.tenant, ep_id,
                                         self.id)


class ServiceEntryDef(ResourceDef):

    def __init__(self):
        super(ServiceEntryDef, self).__init__()

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


class L4ServiceEntryDef(ServiceEntryDef):
    def __init__(self,
                 service_id=None,
                 service_entry_id=None,
                 name=None,
                 description=None,
                 protocol=policy_constants.TCP,
                 dest_ports=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(L4ServiceEntryDef, self).__init__()
        self.tenant = tenant
        self.id = service_entry_id
        self.name = name
        self.description = description
        self.protocol = protocol.upper()
        self.dest_ports = dest_ports
        self.parent_ids = (tenant, service_id)

    def get_obj_dict(self):
        body = super(L4ServiceEntryDef, self).get_obj_dict()
        body['resource_type'] = 'L4PortSetServiceEntry'
        body['l4_protocol'] = self.protocol
        body['destination_ports'] = self.dest_ports
        return body

    def update_attributes_in_body(self, **kwargs):
        # Fix params that need special conversions
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']

        if kwargs.get('protocol') is not None:
            body['l4_protocol'] = kwargs['protocol'].upper()
            del kwargs['protocol']
        if kwargs.get('dest_ports') is not None:
            body['destination_ports'] = kwargs['dest_ports']
            del kwargs['dest_ports']
        super(L4ServiceEntryDef, self).update_attributes_in_body(
            body=body, **kwargs)


class IcmpServiceEntryDef(ServiceEntryDef):
    def __init__(self,
                 service_id=None,
                 service_entry_id=None,
                 name=None,
                 description=None,
                 version=4,
                 icmp_type=None,
                 icmp_code=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(IcmpServiceEntryDef, self).__init__()
        self.tenant = tenant
        self.id = service_entry_id
        self.name = name
        self.description = description
        self.version = version
        self.icmp_type = icmp_type
        self.icmp_code = icmp_code
        self.parent_ids = (tenant, service_id)

    def get_obj_dict(self):
        body = super(IcmpServiceEntryDef, self).get_obj_dict()
        body['resource_type'] = 'ICMPTypeServiceEntry'
        body['protocol'] = 'ICMPv' + str(self.version)
        if self.icmp_type:
            body['icmp_type'] = self.icmp_type
        if self.icmp_code:
            body['icmp_code'] = self.icmp_code
        return body

    def update_attributes_in_body(self, **kwargs):
        # Fix params that need special conversions
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']

        if kwargs.get('version') is not None:
            body['protocol'] = 'ICMPv' + str(kwargs.get('version'))
            del kwargs['version']
        super(IcmpServiceEntryDef, self).update_attributes_in_body(
            body=body, **kwargs)


class CommunicationProfileDef(ResourceDef):
    def __init__(self,
                 profile_id=None,
                 name=None,
                 description=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(CommunicationProfileDef, self).__init__()
        self.tenant = tenant
        self.id = profile_id
        self.name = name
        self.description = description
        self.parent_ids = (tenant)

    @property
    def path_pattern(self):
        return COMM_PROF_PATH_PATTERN

    def get_obj_dict(self):
        body = super(CommunicationProfileDef, self).get_obj_dict()
        body['communication_profile_entries'] = []
        return body

    @staticmethod
    def sub_entries_path():
        entryDef = CommunicationProfileEntryDef()
        return entryDef.get_last_section_dict_key

    def update_attributes_in_body(self, **kwargs):
        super(CommunicationProfileDef, self).update_attributes_in_body(
            **kwargs)
        # make sure entries are there
        entries_path = self.sub_entries_path()
        if entries_path not in self.body:
            self.body[entries_path] = []


class CommunicationProfileEntryDef(ResourceDef):
    def __init__(self,
                 profile_id=None,
                 profile_entry_id=None,
                 name=None,
                 description=None,
                 services=None,
                 action=policy_constants.ACTION_ALLOW,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(CommunicationProfileEntryDef, self).__init__()
        self.tenant = tenant
        self.id = profile_entry_id
        self.name = name
        self.description = description
        self.services = services
        self.action = action.upper()
        self.parent_ids = (tenant, profile_id)

    @property
    def path_pattern(self):
        return COMM_PROF_PATH_PATTERN + "%s/communication-profile-entries/"

    def get_obj_dict(self):
        body = super(CommunicationProfileEntryDef, self).get_obj_dict()
        body['services'] = self.services
        body['action'] = self.action
        return body

    def update_attributes_in_body(self, **kwargs):
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        if kwargs.get('action') is not None:
            body['action'] = kwargs['action'].upper()
            del kwargs['action']
        super(CommunicationProfileEntryDef, self).update_attributes_in_body(
            body=body, **kwargs)


class CommunicationMapDef(ResourceDef):
    def __init__(self,
                 domain_id=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(CommunicationMapDef, self).__init__()
        self.tenant = tenant
        self.domain_id = domain_id
        self.parent_ids = (tenant, domain_id)

    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN + "%s/communication-map/")

    def get_realized_state_path(self, ep_id):
        return REALIZED_STATE_COMM_MAP % (self.tenant, ep_id, self.domain_id)


class CommunicationMapEntryDef(ResourceDef):
    def __init__(self,
                 domain_id=None,
                 map_id=None,
                 sequence_number=None,
                 source_groups=None,
                 dest_groups=None,
                 profile_id=None,
                 name=None,
                 description=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(CommunicationMapEntryDef, self).__init__()
        self.tenant = tenant
        self.domain_id = domain_id
        self.id = map_id
        self.name = name
        self.description = description
        self.sequence_number = sequence_number

        self.source_groups = self.get_groups_path(domain_id, source_groups)
        self.dest_groups = self.get_groups_path(domain_id, dest_groups)
        self.profile_path = self.get_profile_path(
            profile_id) if profile_id else None
        self.parent_ids = (tenant, domain_id)

    # convert groups and communication profile to full path
    def get_groups_path(self, domain_id, group_ids):
        if not group_ids:
            return [policy_constants.ANY_GROUP]
        return [GroupDef(domain_id,
                         group_id,
                         tenant=self.tenant).get_resource_full_path()
                for group_id in group_ids]

    def get_profile_path(self, profile_id):
        return CommunicationProfileDef(
            profile_id,
            tenant=self.tenant).get_resource_full_path()

    @property
    def path_pattern(self):
        return (DOMAINS_PATH_PATTERN +
                "%s/communication-map/communication-entries/")

    def get_obj_dict(self):
        body = super(CommunicationMapEntryDef, self).get_obj_dict()
        body['source_groups'] = self.source_groups
        body['destination_groups'] = self.dest_groups
        body['sequence_number'] = self.sequence_number
        body['communication_profile_path'] = self.profile_path
        return body

    def update_attributes_in_body(self, **kwargs):
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        # Fix params that need special conversions
        if kwargs.get('profile_id') is not None:
            profile_path = self.get_profile_path(kwargs['profile_id'])
            body['communication_profile_path'] = profile_path
            del kwargs['profile_id']

        if kwargs.get('dest_groups') is not None:
            groups = self.get_groups_path(
                self.domain_id, kwargs['dest_groups'])
            body['destination_groups'] = groups
            del kwargs['dest_groups']

        if kwargs.get('source_groups') is not None:
            groups = self.get_groups_path(
                self.domain_id, kwargs['source_groups'])
            body['source_groups'] = groups
            del kwargs['source_groups']

        super(CommunicationMapEntryDef, self).update_attributes_in_body(
            body=body, **kwargs)


# Currently supports only NSXT
class EnforcementPointDef(ResourceDef):

    def __init__(self, ep_id=None,
                 name=None,
                 description=None,
                 ip_address=None,
                 username=None,
                 password=None,
                 thumbprint=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(EnforcementPointDef, self).__init__()
        self.id = ep_id
        self.name = name
        self.description = description
        self.tenant = tenant
        self.username = username
        self.password = password
        self.ip_address = ip_address
        self.thumbprint = thumbprint
        self.parent_ids = (tenant)

    @property
    def path_pattern(self):
        return (TENANTS_PATH_PATTERN +
                'deployment-zones/default/enforcement-points/')

    def get_obj_dict(self):
        body = super(EnforcementPointDef, self).get_obj_dict()
        body['id'] = self.id
        body['connection_info'] = {
            'thumbprint': self.thumbprint,
            'username': self.username,
            'password': self.password,
            'enforcement_point_address': self.ip_address,
            'resource_type': 'NSXTConnectionInfo'}
        body['resource_type'] = 'EnforcementPoint'
        return body

    def update_attributes_in_body(self, **kwargs):
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        # Fix params that need special conversions
        if body.get('connection_info'):
            body['connection_info'][0]['resource_type'] = 'NSXTConnectionInfo'

            for attr in ('username', 'password', 'ip_address', 'thumbprint'):
                if kwargs.get(attr) is not None:
                    body['connection_info'][0][attr] = kwargs[attr]
                    del kwargs[attr]

        super(EnforcementPointDef, self).update_attributes_in_body(
            body=body, **kwargs)

    def get_realized_state_path(self):
        return REALIZED_STATE_EF % (self.tenant, self.id)


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

    def __init__(self, map_id=None,
                 name=None,
                 description=None,
                 domain_id=None,
                 ep_id=None,
                 tenant=policy_constants.POLICY_INFRA_TENANT):
        super(DeploymentMapDef, self).__init__()
        self.id = map_id
        self.name = name
        self.description = description
        # convert enforcement point id to path
        self.ep_path = EnforcementPointDef(
            ep_id,
            tenant=tenant).get_resource_full_path() if ep_id else None
        self.tenant = tenant
        self.parent_ids = (tenant, domain_id)

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

    def get_obj_dict(self):
        body = super(DeploymentMapDef, self).get_obj_dict()
        body['id'] = self.id
        body['enforcement_point_path'] = self.ep_path
        return body

    def update_attributes_in_body(self, **kwargs):
        body = self._get_body_from_kwargs(**kwargs)
        if 'body' in kwargs:
            del kwargs['body']
        # Fix params that need special conversions
        if kwargs.get('domain_id') is not None:
            domain_id = kwargs.get('domain_id')
            domain_path = DomainDef(
                domain_id, tenant=self.tenant).get_resource_full_path()
            body['domain_path'] = domain_path
            del kwargs['domain_id']

        if kwargs.get('ep_id') is not None:
            ep_id = kwargs.get('ep_id')
            ep_path = EnforcementPointDef(
                ep_id, tenant=self.tenant).get_resource_full_path()
            body['enforcement_point_paths'] = [ep_path]
            del kwargs['ep_id']

        super(DeploymentMapDef, self).update_attributes_in_body(
            body=body, **kwargs)


class NsxPolicyApi(object):

    def __init__(self, client):
        self.client = client

    def create_or_update(self, resource_def):
        """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 POST for update too
        """
        path = resource_def.get_resource_path()
        body = resource_def.body
        if not body:
            body = resource_def.get_obj_dict()
        return self.client.create(path, body)

    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()]
        return self.client.create(path, body)

    def delete(self, resource_def):
        path = resource_def.get_resource_path()
        self.client.delete(path)

    def get(self, resource_def):
        path = resource_def.get_resource_path()
        return self.client.get(path)

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

    def get_by_path(self, path):
        return self.client.get(path)
