# 
#   Copyright 2016 RIFT.IO Inc
#
#   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 asyncio
import rift.tasklets

from gi.repository import (
    RwConfigAgentYang as rwcfg_agent,
)

from .riftcm_config_plugin import DEFAULT_CAP_TYPE
from . import RiftCA
from . import jujuconf
import rift.mano.config_agent


class ConfigAgentError(Exception):
    pass


class ConfigAgentExistsError(ConfigAgentError):
    pass


class UnknownAgentTypeError(Exception):
    pass


class ConfigAgentVnfrAddError(Exception):
    pass


class ConfigAgentVnfrTypeError(Exception):
    pass


class ConfigAccountHandler(object):
    def __init__(self, dts, log, loop, on_add_config_agent, on_delete_config_agent):
        self._log = log
        self._dts = dts
        self._loop = loop
        self._on_add_config_agent = on_add_config_agent
        self._on_delete_config_agent = on_delete_config_agent

        self._log.debug("creating config account handler")
        self.cloud_cfg_handler = rift.mano.config_agent.ConfigAgentSubscriber(
            self._dts, self._log,
            rift.mano.config_agent.ConfigAgentCallbacks(
                on_add_apply=self.on_config_account_added,
                on_delete_apply=self.on_config_account_deleted,
            )
        )

    def on_config_account_deleted(self, account):
        self._log.debug("config account deleted: %s", account.name)
        self._on_delete_config_agent(account)

    def on_config_account_added(self, account):
        self._log.debug("config account added")
        self._log.debug(account.as_dict())
        self._on_add_config_agent(account)

    @asyncio.coroutine
    def register(self):
        self.cloud_cfg_handler.register()

class RiftCMConfigPlugins(object):
    """ NSM Config Agent Plugins """
    def __init__(self):
        self._plugin_classes = {
            "juju": jujuconf.JujuConfigPlugin,
            "riftca": RiftCA.RiftCAConfigPlugin,
        }

    @property
    def plugins(self):
        """ Plugin info """
        return self._plugin_classes

    def __getitem__(self, name):
        """ Get item """
        return self._plugin_classes[name]

    def register(self, plugin_name, plugin_class, *args):
        """ Register a plugin to this Nsm"""
        self._plugin_classes[plugin_name] = plugin_class

    def deregister(self, plugin_name, plugin_class, *args):
        """ Deregister a plugin to this Nsm"""
        if plugin_name in self._plugin_classes:
            del self._plugin_classes[plugin_name]

    def class_by_plugin_name(self, name):
        """ Get class by plugin name """
        return self._plugin_classes[name]


class RiftCMConfigAgent(object):
    def __init__(self, dts, log, loop, parent):
        self._dts = dts
        self._log = log
        self._loop = loop
        self._ConfigManagerConfig = parent

        self._config_plugins = RiftCMConfigPlugins()
        self._config_handler = ConfigAccountHandler(
            self._dts, self._log, self._loop, self._on_config_agent, self._on_config_agent_delete)
        self._plugin_instances = {}
        self._default_account_added = False

    @asyncio.coroutine
    def invoke_config_agent_plugins(self, method, nsr, vnfr, *args):
        # Invoke the methods on all config agent plugins registered
        rc = False
        for agent in self._plugin_instances.values():
            if not agent.is_vnfr_managed(vnfr.id):
                continue
            try:
                self._log.debug("Invoke {} on {}".format(method, agent.name))
                rc = yield from agent.invoke(method, nsr, vnfr, *args)
                break
            except Exception as e:
                self._log.error("Error invoking {} on {} : {}".
                                format(method, agent.name, e))
                raise

        self._log.info("vnfr({}), method={}, return rc={}"
                       .format(vnfr.name, method, rc))
        return rc

    def is_vnfr_config_agent_managed(self, vnfr):
        if (not vnfr.has_field('netconf') and
            not vnfr.has_field('juju') and
            not vnfr.has_field('script')):
            return False

        for agent in self._plugin_instances.values():
            try:
                if agent.is_vnfr_managed(vnfr.id):
                    return True
            except Exception as e:
                self._log.debug("Check if VNFR {} is config agent managed: {}".
                                format(vnfr.name, e))
        return False

    def _on_config_agent(self, config_agent):
        self._log.debug("Got nsm plugin config agent account: %s", config_agent)
        try:
            cap_name = config_agent.name
            cap_inst = self._config_plugins.class_by_plugin_name(
                config_agent.account_type)
        except KeyError as e:
            msg = "Config agent nsm plugin type not found: {}". \
                format(config_agent.account_type)
            self._log.error(msg)
            raise UnknownAgentTypeError(msg)

        # Check to see if the plugin was already instantiated
        if cap_name in self._plugin_instances:
            self._log.debug("Config agent nsm plugin {} already instantiated. " \
                            "Using existing.". format(cap_name))
        else:
            # Otherwise, instantiate a new plugin using the config agent account
            self._log.debug("Instantiting new config agent using class: %s", cap_inst)
            new_instance = cap_inst(self._dts, self._log, self._loop, config_agent)
            self._plugin_instances[cap_name] = new_instance

        # TODO (pjoseph): See why this was added, as this deletes the
        # Rift plugin account when Juju account is added
        # if self._default_account_added:
        #     # If the user has provided a config account, chuck the default one.
        #     if self.DEFAULT_CAP_TYPE in self._plugin_instances:
        #         del self._plugin_instances[self.DEFAULT_CAP_TYPE]

    def _on_config_agent_delete(self, config_agent):
        self._log.debug("Got nsm plugin config agent delete, account: %s, type: %s",
                config_agent.name, config_agent.account_type)
        cap_name = config_agent.account_type
        if cap_name in self._plugin_instances:
            self._log.debug("Config agent nsm plugin exists, deleting it.")
            del self._plugin_instances[cap_name]
        else:
            self._log.error("Error deleting - Config Agent nsm plugin %s does not exist.", cap_name)


    @asyncio.coroutine
    def register(self):
        self._log.debug("Registering for config agent nsm plugin manager")
        yield from self._config_handler.register()

        account = rwcfg_agent.ConfigAgentAccount()
        account.account_type = DEFAULT_CAP_TYPE
        account.name = "RiftCA"
        self._on_config_agent(account)
        self._default_account_added = True

        # Also grab any account already configured
        config_agents = yield from self._ConfigManagerConfig.cmdts_obj.get_config_agents(name=None)
        for account in config_agents:
            self._on_config_agent(account)

    def set_config_agent(self, nsr, vnfr, method):
        if method == 'juju':
            agent_type = 'juju'
        elif method in ['netconf', 'script']:
            agent_type = DEFAULT_CAP_TYPE
        else:
            msg = "Unsupported configuration method ({}) for VNF:{}/{}". \
                  format(method, nsr.name, vnfr.name)
            self._log.error(msg)
            raise UnknownAgentTypeError(msg)

        try:
            acc_map = nsr.nsr_cfg_msg.vnf_cloud_account_map
        except AttributeError:
            self._log.debug("Did not find cloud account map for NS {}".
                            format(nsr.name))
            acc_map = []

        for vnfd in acc_map:
            if vnfd.config_agent_account is not None:
                if vnfd.member_vnf_index_ref == vnfr.vnfr_msg.member_index:
                    for agent in self._plugin_instances:
                        # Find the plugin with the same name
                        if agent == vnfd.config_agent_account:
                            # Check if the types are same
                            if  self._plugin_instances[agent].agent_type != agent_type:
                                msg = "VNF {} specified config agent {} is not of type {}". \
                                      format(vnfr.name, agent, agent_type)
                                self._log.error(msg)
                                raise ConfigAgentVnfrTypeError(msg)

                            self._plugin_instances[agent].add_vnfr_managed(vnfr)
                            self._log.debug("Added vnfr {} as config plugin {} managed".
                                            format(vnfr.name, agent))
                            return

        # If no config agent specified for the VNF, use the
        # first available of the same type
        for agent in self._plugin_instances:
            if self._plugin_instances[agent].agent_type == agent_type:
                self._plugin_instances[agent].add_vnfr_managed(vnfr)
                self._log.debug("Added vnfr {} as config plugin {} managed".
                                format(vnfr.name, agent))
                return

        msg = "Error finding config agent of type {} for VNF {}". \
              format(agent_type, vnfr.name)
        self._log.error(msg)
        raise ConfigAgentVnfrAddError(msg)
