#    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 ast
import time

from heatclient import client as heat_client
from heatclient import exc as heat_exc
from keystoneclient.v2_0 import client as keyclient
from neutron._i18n import _LE
from neutron._i18n import _LW
from neutron.db import model_base
from neutron import manager
from neutron.plugins.common import constants as pconst
from oslo_config import cfg
from oslo_log import helpers as log
from oslo_log import log as logging
from oslo_serialization import jsonutils
import sqlalchemy as sa

from gbpservice.neutron.services.servicechain.common import exceptions as exc


LOG = logging.getLogger(__name__)
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')

service_chain_opts = [
    cfg.IntOpt('stack_delete_retries',
               default=5,
               help=_("Number of attempts to retry for stack deletion")),
    cfg.IntOpt('stack_delete_retry_wait',
               default=3,
               help=_("Wait time between two successive stack delete "
                      "retries")),
    cfg.StrOpt('heat_uri',
               default='http://localhost:8004/v1',
               help=_("Heat server address to create services "
                      "specified in the service chain.")),
    cfg.StrOpt('heat_ca_certificates_file', default=None,
               help=_('CA file for heatclient to verify server certificates')),
    cfg.BoolOpt('heat_api_insecure', default=False,
                help=_("If True, ignore any SSL validation issues")),
]


cfg.CONF.register_opts(service_chain_opts, "simplechain")

# Service chain API supported Values
sc_supported_type = [pconst.LOADBALANCER, pconst.FIREWALL]
STACK_DELETE_RETRIES = cfg.CONF.simplechain.stack_delete_retries
STACK_DELETE_RETRY_WAIT = cfg.CONF.simplechain.stack_delete_retry_wait


class ServiceChainInstanceStack(model_base.BASEV2):
    """ServiceChainInstance stacks owned by the servicechain driver."""

    __tablename__ = 'sc_instance_stacks'
    instance_id = sa.Column(sa.String(36),
                            nullable=False, primary_key=True)
    stack_id = sa.Column(sa.String(36),
                         nullable=False, primary_key=True)


class SimpleChainDriver(object):

    @log.log_method_call
    def initialize(self):
        pass

    @log.log_method_call
    def create_servicechain_node_precommit(self, context):
        if context.current['service_profile_id'] is None:
            if context.current['service_type'] not in sc_supported_type:
                raise exc.InvalidServiceTypeForReferenceDriver()
        elif context.current['service_type']:
            LOG.warning(_LW('Both service_profile_id and service_type are'
                            'specified, service_type will be ignored.'))

    @log.log_method_call
    def create_servicechain_node_postcommit(self, context):
        pass

    @log.log_method_call
    def update_servicechain_node_precommit(self, context):
        if (context.original['config'] != context.current['config']):
            filters = {'servicechain_spec': context.original[
                                                'servicechain_specs']}
            sc_instances = context._plugin.get_servicechain_instances(
                context._plugin_context, filters)
            if sc_instances:
                raise exc.NodeUpdateNotSupported()

    @log.log_method_call
    def update_servicechain_node_postcommit(self, context):
        pass

    @log.log_method_call
    def delete_servicechain_node_precommit(self, context):
        pass

    @log.log_method_call
    def delete_servicechain_node_postcommit(self, context):
        pass

    @log.log_method_call
    def create_servicechain_spec_precommit(self, context):
        pass

    @log.log_method_call
    def create_servicechain_spec_postcommit(self, context):
        pass

    @log.log_method_call
    def update_servicechain_spec_precommit(self, context):
        pass

    @log.log_method_call
    def update_servicechain_spec_postcommit(self, context):
        if context.original['nodes'] != context.current['nodes']:
            filters = {'servicechain_spec': [context.original['id']]}
            sc_instances = context._plugin.get_servicechain_instances(
                context._plugin_context, filters)
            for sc_instance in sc_instances:
                self._update_servicechain_instance(context,
                                                   sc_instance,
                                                   context._sc_spec)

    @log.log_method_call
    def delete_servicechain_spec_precommit(self, context):
        pass

    @log.log_method_call
    def delete_servicechain_spec_postcommit(self, context):
        pass

    @log.log_method_call
    def create_servicechain_instance_precommit(self, context):
        pass

    @log.log_method_call
    def create_servicechain_instance_postcommit(self, context):
        sc_instance = context.current
        sc_spec_ids = sc_instance.get('servicechain_specs')
        for sc_spec_id in sc_spec_ids:
            sc_spec = context._plugin.get_servicechain_spec(
                context._plugin_context, sc_spec_id)
            sc_node_ids = sc_spec.get('nodes')
            self._create_servicechain_instance_stacks(context, sc_node_ids,
                                                      sc_instance, sc_spec)

    @log.log_method_call
    def update_servicechain_instance_precommit(self, context):
        pass

    @log.log_method_call
    def update_servicechain_instance_postcommit(self, context):
        original_spec_ids = context.original.get('servicechain_specs')
        new_spec_ids = context.current.get('servicechain_specs')
        if set(original_spec_ids) != set(new_spec_ids):
            for new_spec_id in new_spec_ids:
                newspec = context._plugin.get_servicechain_spec(
                    context._plugin_context, new_spec_id)
                self._update_servicechain_instance(context, context.current,
                                                   newspec)

    @log.log_method_call
    def delete_servicechain_instance_precommit(self, context):
        pass

    @log.log_method_call
    def delete_servicechain_instance_postcommit(self, context):
        self._delete_servicechain_instance_stacks(context._plugin_context,
                                                  context.current['id'])

    @log.log_method_call
    def create_service_profile_precommit(self, context):
        if context.current['service_type'] not in sc_supported_type:
            raise exc.InvalidServiceTypeForReferenceDriver()

    @log.log_method_call
    def create_service_profile_postcommit(self, context):
        pass

    @log.log_method_call
    def update_service_profile_precommit(self, context):
        pass

    @log.log_method_call
    def update_service_profile_postcommit(self, context):
        pass

    @log.log_method_call
    def delete_service_profile_precommit(self, context):
        pass

    @log.log_method_call
    def delete_service_profile_postcommit(self, context):
        pass

    def _get_ptg(self, context, ptg_id):
        return self._get_resource(self._grouppolicy_plugin,
                                  context._plugin_context,
                                  'policy_target_group',
                                  ptg_id)

    def _get_pt(self, context, pt_id):
        return self._get_resource(self._grouppolicy_plugin,
                                  context._plugin_context,
                                  'policy_target',
                                  pt_id)

    def _get_port(self, context, port_id):
        return self._get_resource(self._core_plugin,
                                  context._plugin_context,
                                  'port',
                                  port_id)

    def _get_ptg_subnet(self, context, ptg_id):
        ptg = self._get_ptg(context, ptg_id)
        return ptg.get("subnets")[0]

    def _get_member_ips(self, context, ptg_id):
        ptg = self._get_ptg(context, ptg_id)
        pt_ids = ptg.get("policy_targets")
        member_addresses = []
        for pt_id in pt_ids:
            pt = self._get_pt(context, pt_id)
            port_id = pt.get("port_id")
            port = self._get_port(context, port_id)
            ipAddress = port.get('fixed_ips')[0].get("ip_address")
            member_addresses.append(ipAddress)
        return member_addresses

    def _fetch_template_and_params(self, context, sc_instance,
                                   sc_spec, sc_node):
        stack_template = sc_node.get('config')
        # TODO(magesh):Raise an exception ??
        if not stack_template:
            LOG.error(_LE("Service Config is not defined for the service"
                        " chain Node"))
            return
        stack_template = jsonutils.loads(stack_template)
        config_param_values = sc_instance.get('config_param_values', {})
        stack_params = {}
        # config_param_values has the parameters for all Nodes. Only apply
        # the ones relevant for this Node
        if config_param_values:
            config_param_values = jsonutils.loads(config_param_values)
        config_param_names = sc_spec.get('config_param_names', [])
        if config_param_names:
            config_param_names = ast.literal_eval(config_param_names)

        # This service chain driver knows how to fill in two parameter values
        # for the template at present.
        # 1)Subnet -> Provider PTG subnet is used
        # 2)PoolMemberIPs -> List of IP Addresses of all PTs in Provider PTG

        # TODO(magesh):Process on the basis of ResourceType rather than Name
        # eg: Type: OS::Neutron::PoolMember
        # Variable number of pool members is not handled yet. We may have to
        # dynamically modify the template json to achieve that
        member_ips = []
        provider_ptg_id = sc_instance.get("provider_ptg_id")
        # If we have the key "PoolMemberIP*" in template input parameters,
        # fetch the list of IPs of all PTs in the PTG
        for key in config_param_names or []:
            if "PoolMemberIP" in key:
                member_ips = self._get_member_ips(context, provider_ptg_id)
                break

        member_count = 0
        for key in config_param_names or []:
            if "PoolMemberIP" in key:
                value = (member_ips[member_count]
                         if len(member_ips) > member_count else '0')
                member_count = member_count + 1
                config_param_values[key] = value
            elif key == "Subnet":
                value = self._get_ptg_subnet(context, provider_ptg_id)
                config_param_values[key] = value
        node_params = (stack_template.get('Parameters')
                       or stack_template.get('parameters'))
        if node_params:
            for parameter in config_param_values.keys():
                if parameter in node_params.keys():
                    stack_params[parameter] = config_param_values[parameter]
        return (stack_template, stack_params)

    def _create_servicechain_instance_stacks(self, context, sc_node_ids,
                                             sc_instance, sc_spec):
        heatclient = HeatClient(context._plugin_context)
        for sc_node_id in sc_node_ids:
            sc_node = context._plugin.get_servicechain_node(
                context._plugin_context, sc_node_id)

            stack_template, stack_params = self._fetch_template_and_params(
                context, sc_instance, sc_spec, sc_node)

            stack_name = ("stack_" + sc_instance['name'] + sc_node['name'] +
                          sc_instance['id'][:8])
            stack = heatclient.create(
                stack_name.replace(" ", ""), stack_template, stack_params)

            self._insert_chain_stack_db(
                context._plugin_context.session, sc_instance['id'],
                stack['stack']['id'])

    def _delete_servicechain_instance_stacks(self, context, instance_id):
        stack_ids = self._get_chain_stacks(context.session, instance_id)
        heatclient = HeatClient(context)
        for stack in stack_ids:
            heatclient.delete(stack.stack_id)
        for stack in stack_ids:
            self._wait_for_stack_delete(heatclient, stack.stack_id)
        self._delete_chain_stacks_db(context.session, instance_id)

    # Wait for the heat stack to be deleted for a maximum of 15 seconds
    # we check the status every 3 seconds and call sleep again
    # This is required because cleanup of subnet fails when the stack created
    # some ports on the subnet and the resource delete is not completed by
    # the time subnet delete is triggered by Resource Mapping driver
    def _wait_for_stack_delete(self, heatclient, stack_id):
        stack_delete_retries = STACK_DELETE_RETRIES
        while True:
            try:
                stack = heatclient.get(stack_id)
                if stack.stack_status == 'DELETE_COMPLETE':
                    return
                elif stack.stack_status == 'DELETE_FAILED':
                    heatclient.delete(stack_id)
            except Exception:
                LOG.exception(_LE(
                    "Service Chain Instance cleanup may not have "
                    "happened because Heat API request failed "
                    "while waiting for the stack %(stack)s to be "
                    "deleted"), {'stack': stack_id})
                return
            else:
                time.sleep(STACK_DELETE_RETRY_WAIT)
                stack_delete_retries = stack_delete_retries - 1
                if stack_delete_retries == 0:
                    LOG.warning(_LW(
                        "Resource cleanup for service chain instance"
                        " is not completed within %(wait)s seconds"
                        " as deletion of Stack %(stack)s is not"
                        " completed"),
                        {'wait': (STACK_DELETE_RETRIES *
                                  STACK_DELETE_RETRY_WAIT),
                         'stack': stack_id})
                    return
                else:
                    continue

    def _get_instance_by_spec_id(self, context, spec_id):
        filters = {'servicechain_spec': [spec_id]}
        return context._plugin.get_servicechain_instances(
            context._plugin_context, filters)

    def _update_servicechain_instance(self, context, sc_instance, newspec):
        self._delete_servicechain_instance_stacks(context._plugin_context,
                                                  sc_instance['id'])
        sc_node_ids = newspec.get('nodes')
        self._create_servicechain_instance_stacks(context,
                                                  sc_node_ids,
                                                  sc_instance,
                                                  newspec)

    def _delete_chain_stacks_db(self, session, sc_instance_id):
        with session.begin(subtransactions=True):
            stacks = session.query(ServiceChainInstanceStack
                                   ).filter_by(instance_id=sc_instance_id
                                               ).all()
            for stack in stacks:
                session.delete(stack)

    def _insert_chain_stack_db(self, session, sc_instance_id, stack_id):
        with session.begin(subtransactions=True):
            chainstack = ServiceChainInstanceStack(
                instance_id=sc_instance_id,
                stack_id=stack_id)
            session.add(chainstack)

    def _get_chain_stacks(self, session, sc_instance_id):
        with session.begin(subtransactions=True):
            stack_ids = session.query(ServiceChainInstanceStack.stack_id
                                      ).filter_by(instance_id=sc_instance_id
                                                  ).all()
        return stack_ids

    def _get_resource(self, plugin, context, resource, resource_id):
        obj_getter = getattr(plugin, 'get_' + resource)
        obj = obj_getter(context, resource_id)
        return obj

    @property
    def _core_plugin(self):
        # REVISIT(Magesh): Need initialization method after all
        # plugins are loaded to grab and store plugin.
        return manager.NeutronManager.get_plugin()

    @property
    def _grouppolicy_plugin(self):
        # REVISIT(Magesh): Need initialization method after all
        # plugins are loaded to grab and store plugin.
        plugins = manager.NeutronManager.get_service_plugins()
        grouppolicy_plugin = plugins.get(pconst.GROUP_POLICY)
        if not grouppolicy_plugin:
            LOG.error(_LE("No Grouppolicy service plugin found."))
            raise exc.ServiceChainDeploymentError()
        return grouppolicy_plugin


class HeatClient(object):

    def __init__(self, context, password=None):
        api_version = "1"
        self.tenant = context.tenant

        self._keystone = None
        endpoint = "%s/%s" % (cfg.CONF.simplechain.heat_uri, self.tenant)
        kwargs = {
            'token': self._get_auth_token(self.tenant),
            'username': context.user_name,
            'password': password,
            'cacert': cfg.CONF.simplechain.heat_ca_certificates_file,
            'insecure': cfg.CONF.simplechain.heat_api_insecure
        }
        self.client = heat_client.Client(api_version, endpoint, **kwargs)
        self.stacks = self.client.stacks

    def create(self, name, data, parameters=None):
        fields = {
            'stack_name': name,
            'timeout_mins': 10,
            'disable_rollback': True,
            'password': data.get('password')
        }
        fields['template'] = data
        fields['parameters'] = parameters
        return self.stacks.create(**fields)

    def delete(self, stack_id):
        try:
            self.stacks.delete(stack_id)
        except heat_exc.HTTPNotFound:
            LOG.warning(_LW(
                "Stack %(stack)s created by service chain driver is "
                "not found at cleanup"), {'stack': stack_id})

    def get(self, stack_id):
        return self.stacks.get(stack_id)

    @property
    def keystone(self):
        if not self._keystone:
            keystone_conf = cfg.CONF.keystone_authtoken
            if keystone_conf.get('auth_uri'):
                auth_url = keystone_conf.auth_uri
            else:
                auth_url = ('%s://%s:%s/v2.0/' % (
                    keystone_conf.auth_protocol,
                    keystone_conf.auth_host,
                    keystone_conf.auth_port))
            user = (keystone_conf.get('admin_user') or keystone_conf.username)
            pw = (keystone_conf.get('admin_password') or
                  keystone_conf.password)
            self._keystone = keyclient.Client(
                username=user, password=pw, auth_url=auth_url,
                tenant_id=self.tenant)
        return self._keystone

    def _get_auth_token(self, tenant):
        return self.keystone.get_token(tenant)
