"""
Copyright 2017 Platform9 Systems Inc.(http://www.platform9.com)
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 random

from oslo_log import log

import ipaddr
from neutron.callbacks import events
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron.common.azure.config import azure_conf
from neutron.common.azure import utils
from neutron.manager import NeutronManager
from neutron.plugins.ml2 import driver_api as api
from neutron_lib import constants as n_const
from neutron_lib import exceptions as e

try:
    from neutron_lib.plugins import directory
except ImportError:
    pass

LOG = log.getLogger(__name__)


def log_network_hook(function):
    def wraps(self, context):
        LOG.debug("Called %s with context: %s" % (function.__name__, context))
        return function(self, context)

    return wraps


# Custom VIF type for Azure
VIF_TYPE_AZURE = "azure_nic"


class AzureMechanismDriver(api.MechanismDriver):
    """Ml2 Mechanism driver for Azure"""

    def __init__(self):
        super(AzureMechanismDriver, self).__init__()
        self._network_client = None
        self._resource_client = None
        self._sg_callback_map = {}

    def initialize(self):
        LOG.info("Azure Mechanism driver init with %s project, %s region" %
                 (azure_conf.tenant_id, azure_conf.region))
        self._subscribe_events()

    def _subscribe_events(self):
        events_info = [(resources.SECURITY_GROUP, events.BEFORE_DELETE,
                        self._delete_secgrp),
                       (resources.SECURITY_GROUP, events.BEFORE_UPDATE,
                        self._update_secgrp), (resources.SECURITY_GROUP,
                                               events.BEFORE_CREATE,
                                               self._validate_secgrp),
                       (resources.SECURITY_GROUP, events.AFTER_CREATE,
                        self._create_secgrp), (resources.SECURITY_GROUP_RULE,
                                               events.BEFORE_DELETE,
                                               self._delete_secrule),
                       (resources.SECURITY_GROUP_RULE, events.BEFORE_UPDATE,
                        self._update_secrule), (resources.SECURITY_GROUP_RULE,
                                                events.BEFORE_CREATE,
                                                self._validate_secrule),
                       (resources.SECURITY_GROUP_RULE, events.AFTER_CREATE,
                        self._create_secrule)]
        for resource, event, callback in events_info:
            registry.subscribe(self.secgrp_callback, resource, event)
            self._sg_callback_map[(resource, event)] = callback
        LOG.info("Azure mechanism driver registered security groups callbacks")

    @property
    def network_client(self):
        conf = azure_conf
        if self._network_client is None:
            args = (conf.tenant_id, conf.client_id, conf.client_secret,
                    conf.subscription_id)
            self._network_client = utils.get_network_client(*args)
            self._create_resource_group_if_not_created(conf)
        return self._network_client

    def _create_resource_group_if_not_created(self, conf):
        if self._resource_client is None:
            args = (conf.tenant_id, conf.client_id, conf.client_secret,
                    conf.subscription_id)
            self._resource_client = utils.get_resource_client(*args)
        is_resource_created = utils.check_resource_existence(
            self._resource_client, conf.resource_group)
        if not is_resource_created:
            utils.create_resource_group(
                self._resource_client, conf.resource_group, conf.region)

    def _azure_network_name(self, context):
        return 'net-' + context.current[api.ID]

    def _azure_subnet_name(self, context):
        return 'subnet-' + context.current[api.ID]

    def _azure_subnet_network_name(self, context):
        return 'net-' + context.current['network_id']

    def _azure_secgrp_id(self, openstack_id):
        return "secgrp-" + openstack_id

    def _azure_secrule_id(self, openstack_id):
        return "secrule-" + openstack_id

    @staticmethod
    def is_private_network(cidr):
        return ipaddr.IPNetwork(cidr).is_private

    @log_network_hook
    def create_network_precommit(self, context):
        pass

    @log_network_hook
    def create_network_postcommit(self, context):
        pass

    @log_network_hook
    def update_network_precommit(self, context):
        pass

    @log_network_hook
    def update_network_postcommit(self, context):
        pass

    @log_network_hook
    def delete_network_precommit(self, context):
        pass

    @log_network_hook
    def delete_network_postcommit(self, context):
        name = self._azure_network_name(context)
        utils.delete_network(self.network_client, azure_conf.resource_group,
                             name)

    @log_network_hook
    def create_subnet_precommit(self, context):
        net_svc = self.network_client
        name = self._azure_subnet_name(context)
        network_name = self._azure_subnet_network_name(context)
        cidr = context.current['cidr']
        if self.is_private_network(cidr):
            try:
                azure_network = utils.get_network(
                    net_svc, azure_conf.resource_group, network_name)
                address_prefixes = azure_network.address_space.address_prefixes
                if cidr not in address_prefixes:
                    address_prefixes.append(cidr)
                    utils.update_network(net_svc, azure_conf.resource_group,
                                         network_name, azure_network)
            except e.NetworkNotFound:
                body = {
                    'location': azure_conf.region,
                    'address_space': {
                        'address_prefixes': [
                            cidr,
                        ]
                    }
                }
                utils.create_network(net_svc, azure_conf.resource_group,
                                     network_name, body)
            utils.create_subnet(net_svc, azure_conf.resource_group,
                                network_name, name, {'address_prefix': cidr})

    @log_network_hook
    def create_subnet_postcommit(self, context):
        pass

    @log_network_hook
    def update_subnet_precommit(self, context):
        pass

    @log_network_hook
    def update_subnet_postcommit(self, context):
        pass

    @log_network_hook
    def delete_subnet_precommit(self, context):
        cidr = context.current['cidr']
        if self.is_private_network(cidr):
            name = self._azure_subnet_name(context)
            network_name = self._azure_subnet_network_name(context)
            utils.delete_subnet(self.network_client, azure_conf.resource_group,
                                network_name, name)

    @log_network_hook
    def delete_subnet_postcommit(self, context):
        pass

    def _check_dev_owner(self, port_context):
        dev_owner = port_context.current['device_owner']
        return len(dev_owner) == 0 or dev_owner.startswith(
            n_const.DEVICE_OWNER_COMPUTE_PREFIX)

    @log_network_hook
    def create_port_precommit(self, context):
        LOG.debug("Create_port_precommit: %s" % context.current)
        if not self._check_dev_owner(context):
            return
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        region = azure_conf.region
        network_name = self._azure_subnet_network_name(context)
        details = context.current['fixed_ips'][0]
        subnet_name = 'subnet-' + details['subnet_id']
        ip_address = details['ip_address']
        nic_name = 'nic-' + context.current['id']
        ipc_name = 'ipc-' + context.current['id']
        azure_subnet = utils.get_subnet(net_svc, resource_group, network_name,
                                        subnet_name)
        body = {
            'location':
            region,
            'ip_configurations': [{
                'name': ipc_name,
                'private_ip_address': ip_address,
                'private_ip_allocation_method': 'Static',
                'subnet': {
                    'id': azure_subnet.id
                },
            }]
        }
        security_groups = context.current['security_groups']
        if security_groups and len(security_groups) == 1:
            sg_name = self._azure_secgrp_id(security_groups[0])
            sg = utils.get_sg(net_svc, resource_group, sg_name)
            body['network_security_group'] = {'id': sg.id}
        utils.create_nic(net_svc, resource_group, nic_name, body)
        LOG.info("Created NIC %s on Azure." % nic_name)

    @log_network_hook
    def create_port_postcommit(self, context):
        pass

    @log_network_hook
    def update_port_precommit(self, context):
        pass

    @log_network_hook
    def update_port_postcommit(self, context):
        pass

    @log_network_hook
    def delete_port_precommit(self, context):
        if not self._check_dev_owner(context):
            return
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        nic_name = 'nic-' + context.current['id']
        utils.get_nic(net_svc, resource_group, nic_name)
        utils.delete_nic(net_svc, resource_group, nic_name)
        LOG.info("Deleted NIC %s on Azure." % nic_name)

    @log_network_hook
    def delete_port_postcommit(self, context):
        pass

    def get_secgrp(self, context, id):
        try:
            core_plugin = NeutronManager.get_plugin()
        except AttributeError:
            core_plugin = directory.get_plugin()
        return core_plugin.get_security_group(context, id)

    def get_secgrp_rule(self, context, id):
        try:
            core_plugin = NeutronManager.get_plugin()
        except AttributeError:
            core_plugin = directory.get_plugin()
        return core_plugin.get_security_group_rule(context, id)

    def _validate_secrule(self, **kwargs):
        rule = kwargs['security_group_rule']
        utils.convert_sg_rule(rule)

    def _create_secrule(self, **kwargs):
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        rule = kwargs['security_group_rule']
        azure_rule = utils.convert_sg_rule(rule)
        sg_name = self._azure_secgrp_id(rule['security_group_id'])
        name = self._azure_secrule_id(rule['id'])
        sg = utils.get_sg(net_svc, resource_group, sg_name)
        # Each Azure security rule has a priority.
        # The value can be between 100 and 4096. The priority number must be
        # unique for each rule in the collection. The lower the priority
        # number, the higher the priority of the rule.
        previous_priorities = sorted([i.priority for i in sg.security_rules])
        if previous_priorities:
            priority = previous_priorities[-1] + 1
        else:
            priority = 100
        azure_rule['priority'] = priority
        utils.create_sg_rule(net_svc, resource_group, sg_name, name,
                             azure_rule)

    def _update_secrule(self, **kwargs):
        pass

    def _delete_secrule(self, **kwargs):
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        secrule_id = kwargs['security_group_rule_id']
        sec_rule = self.get_secgrp_rule(kwargs['context'], secrule_id)
        sg_name = self._azure_secgrp_id(sec_rule['security_group_id'])
        name = self._azure_secrule_id(secrule_id)
        utils.delete_sg_rule(net_svc, resource_group, sg_name, name)

    def _validate_secgrp(self, **kwargs):
        pass

    def _create_secgrp(self, **kwargs):
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        region = azure_conf.region
        name = self._azure_secgrp_id(kwargs['security_group']['id'])
        body = {
            'location': region,
        }
        utils.create_sg(net_svc, resource_group, name, body)

    def _update_secgrp(self, **kwargs):
        pass

    def _delete_secgrp(self, **kwargs):
        net_svc = self.network_client
        resource_group = azure_conf.resource_group
        name = self._azure_secgrp_id(kwargs['security_group_id'])
        utils.delete_sg(net_svc, resource_group, name)

    @log_network_hook
    def bind_port(self, context):
        fixed_ip_dict = dict()
        if 'fixed_ips' in context.current:
            if len(context.current['fixed_ips']):
                fixed_ip_dict = context.current['fixed_ips'][0]

        segment_id = random.choice(context.segments_to_bind)[api.ID]
        context.set_binding(
            segment_id, VIF_TYPE_AZURE, fixed_ip_dict, status='ACTIVE')
        return True

    def secgrp_callback(self, resource, event, trigger, **kwargs):
        LOG.info("Secgrp_callback: %s %s %s" % (resource, event, kwargs))
        callback = self._sg_callback_map[(resource, event)]
        callback(**kwargs)
