############################################################################
# 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 abc
import json
import os
import yaml

from gi.repository import ProjectNsdYang as NsdYang
from gi.repository import ProjectVnfdYang as VnfdYang


class InitialConfigReadError(Exception):
    pass


class InitialConfigMethodError(Exception):
    pass


class InitialConfigPrimitiveReader(object):
    """ Reader for the VNF Initial Config Input Data

    This class interprets the Initial Config Primitive
    Input data and constructs inital config primitive
    protobuf messages.

    The reason for creating a new format is to keep the structure
    as dead-simple as possible for readability.

    The structure (not serialization format) is defined as the
    following.

    [
        {
          "name": <primitive_name>,
          "parameter": {
            "hostname": "pe1"
            "pass": "6windos"
            ...
          }
        }
        ...
    ]

    """
    def __init__(self, primitive_input):
        self._primitives = []

        self._parse_input_data(primitive_input)

    def _parse_input_data(self, input_dict):
        for seq, cfg in enumerate(input_dict):
            if "name" not in cfg:
                raise InitialConfigReadError("Initial config primitive must have a name")

            name = cfg["name"]

            new_primitive = self. get_initial_config_primitive(seq=seq, name=name)
            self._primitives.append(new_primitive)
            if "parameter" in cfg:
                for key, val in cfg["parameter"].items():
                    new_primitive.parameter.add(name=key, value=val)

    @abc.abstractmethod
    def get_initial_config_primitive(self, seq, name):
        '''Override in sub class to provide the correct yang model'''
        raise InitialConfigMethodError(
            "InitialConfigPrimitiveReader Calling abstract class method")

    @property
    def primitives(self):
        """ Returns a copy of the read inital config primitives"""
        return [prim.deep_copy() for prim in self._primitives]

    @classmethod
    def from_yaml_file_hdl(cls, file_hdl):
        """ Create a instance of InitialConfigPrimitiveFileData
        by reading a YAML file handle.

        Arguments:
            file_hdl - A file handle which contains serialized YAML which
                       follows the documented structure.

        Returns:
            A new InitialConfigPrimitiveFileData() instance

        Raises:
            InitialConfigReadError: Input Data was malformed or could not be read
        """
        try:
            input_dict = yaml.safe_load(file_hdl)
        except yaml.YAMLError as e:
            raise InitialConfigReadError(e)

        return cls(input_dict)


class VnfInitialConfigPrimitiveReader(InitialConfigPrimitiveReader):
    '''Class to read the VNF initial config primitives'''

    def __init__(self, primitive_input):
        super(VnfInitialConfigPrimitiveReader, self).__init__(primitive_input)

    def get_initial_config_primitive(self, seq, name):
        return VnfdYang.InitialConfigPrimitive(seq=seq, name=name)


class NsInitialConfigPrimitiveReader(InitialConfigPrimitiveReader):
    '''Class to read the NS initial config primitives'''

    def __init__(self, primitive_input):
        super(NsInitialConfigPrimitiveReader, self).__init__(primitive_input)

    def get_initial_config_primitive(self, seq, name):
        return NsdYang.NsdInitialConfigPrimitive(seq=seq, name=name)


class ConfigPrimitiveConvertor(object):
    PARAMETER = "parameter"
    PARAMETER_GROUP = "parameter_group"
    CONFIG_PRIMITIVE = "service_primitive"
    INITIAL_CONFIG_PRIMITIVE = "initial_config_primitive"

    def _extract_param(self, param, field="default_value"):
        key = param.name
        value = getattr(param, field, None)

        if value is not None:
            setattr(param, field, None)

        return key, value

    def _extract_parameters(self, parameters, input_data, field="default_value"):
        input_data[self.PARAMETER] = {}
        for param in parameters:
            key, value = self._extract_param(param, field)

            if value is None:
                continue

            input_data[self.PARAMETER][key] = value

        if not input_data[self.PARAMETER]:
            del input_data[self.PARAMETER]

    def _extract_parameter_group(self, param_groups, input_data):
        input_data[self.PARAMETER_GROUP] = {}
        for param_group in param_groups:
            input_data[self.PARAMETER_GROUP][param_group.name] = {}
            for param in param_group.parameter:
                key, value = self._extract_param(param)

                if value is None:
                    continue

                input_data[self.PARAMETER_GROUP][param_group.name][key] = value

        if not input_data[self.PARAMETER_GROUP]:
            del input_data[self.PARAMETER_GROUP]

    def extract_config(self,
                       config_primitives=None,
                       initial_configs=None,
                       format="yaml"):
        input_data = {}

        if config_primitives:
            input_data[self.CONFIG_PRIMITIVE] = {}
            for config_primitive in config_primitives:
                input_data[self.CONFIG_PRIMITIVE][config_primitive.name] = {}
                self._extract_parameters(
                    config_primitive.parameter,
                    input_data[self.CONFIG_PRIMITIVE][config_primitive.name])

                try:
                    self._extract_parameter_group(
                        config_primitive.parameter_group,
                        input_data[self.CONFIG_PRIMITIVE][config_primitive.name])
                except AttributeError:
                    pass

                if not input_data[self.CONFIG_PRIMITIVE][config_primitive.name]:
                    del input_data[self.CONFIG_PRIMITIVE][config_primitive.name]

            if not input_data[self.CONFIG_PRIMITIVE]:
                del input_data[self.CONFIG_PRIMITIVE]


        if initial_configs:
            input_data[self.INITIAL_CONFIG_PRIMITIVE] = []
            for in_config_primitive in initial_configs:
                primitive = {}
                self._extract_parameters(
                    in_config_primitive.parameter,
                    primitive,
                    field="value")

                if primitive:
                    input_data[self.INITIAL_CONFIG_PRIMITIVE].append(
                        {
                            "name": in_config_primitive.name,
                            self.PARAMETER: primitive[self.PARAMETER],
                        }
                    )

            if not input_data[self.INITIAL_CONFIG_PRIMITIVE]:
                del input_data[self.INITIAL_CONFIG_PRIMITIVE]

        if len(input_data):
            if format == "json":
                return json.dumps(input_data)
            elif format == "yaml":
                return yaml.dump(input_data, default_flow_style=False)
        else:
            return ''

    def extract_nsd_config(self, nsd, format="yaml"):
        config_prim = None
        try:
            config_prim = nsd.service_primitive
        except AttributeError:
            pass

        initial_conf = None
        try:
            initial_conf = nsd.initial_config_primitive
        except AttributeError:
            pass

        return self.extract_config(
            config_primitives=config_prim,
            initial_configs=initial_conf,
            format=format)

    def extract_vnfd_config(self, vnfd, format="yaml"):
        config_prim = None
        try:
            config_prim = vnfd.vnf_configuration.service_primitive
        except AttributeError:
            pass

        initial_conf = None
        try:
            initial_conf = vnfd.vnf_configuration.initial_config_primitive
        except AttributeError:
            pass

        return self.extract_config(
            config_primitives=config_prim,
            initial_configs=initial_conf,
            format=format)

    def merge_params(self, parameters, input_config, field="default_value"):
        for param in parameters:
            try:
                setattr(param, field, input_config[param.name])
            except KeyError:
                pass

    def add_nsd_initial_config(self, nsd_init_cfg_prim_msg, input_data):
        """ Add initial config primitives from NS Initial Config Input Data

        Arguments:
            nsd_init_cfg_prim_msg - manotypes:nsd/initial_config_primitive pb msg
            ns_input_data - NsInitialConfigPrimitiveReader documented input data

        Raises:
           InitialConfigReadError: VNF input data was malformed
        """
        if self.INITIAL_CONFIG_PRIMITIVE in input_data:
            ns_input_data = input_data[self.INITIAL_CONFIG_PRIMITIVE]

            reader = NsInitialConfigPrimitiveReader(ns_input_data)
            for prim in reader.primitives:
                nsd_init_cfg_prim_msg.append(prim)

    def merge_nsd_initial_config(self, nsd, input_data):
        try:
            for config_primitive in nsd.initial_config_primitive:
                for cfg in input_data[self.INITIAL_CONFIG_PRIMITIVE]:
                    if cfg['name'] == config_primitive.name:
                        self.merge_params(
                            config_primitive.parameter,
                            cfg[self.PARAMETER],
                            field="value")
                        break

        except AttributeError as e:
            self._log.debug("Did not find initial-config-primitive for NSD {}: {}".
                            format(nsd.name, e))


    def merge_nsd_config(self, nsd, input_data):
        for config_primitive in nsd.service_primitive:
            try:
                cfg = input_data[self.CONFIG_PRIMITIVE][config_primitive.name]
            except KeyError:
                continue

            self.merge_params(
                    config_primitive.parameter,
                    cfg[self.PARAMETER])

            for param_group in config_primitive.parameter_group:
                self.merge_params(
                        param_group.parameter,
                        cfg[self.PARAMETER_GROUP][param_group.name])

    def add_vnfd_initial_config(self, vnfd_init_cfg_prim_msg, input_data):
        """ Add initial config primitives from VNF Initial Config Input Data

        Arguments:
            vnfd_init_cfg_prim_msg - manotypes:vnf-configuration/initial_config_primitive pb msg
            vnf_input_data - VnfInitialConfigPrimitiveReader documented input data

        Raises:
           InitialConfigReadError: VNF input data was malformed
        """
        if self.INITIAL_CONFIG_PRIMITIVE in input_data:
            vnf_input_data = input_data[self.INITIAL_CONFIG_PRIMITIVE]

            reader = VnfInitialConfigPrimitiveReader(vnf_input_data)
            for prim in reader.primitives:
                vnfd_init_cfg_prim_msg.append(prim)

    def merge_vnfd_config(self, vnfd, input_data):
        for config_primitive in vnfd.vnf_configuration.service_primitive:
            try:
                cfg = input_data[self.CONFIG_PRIMITIVE][config_primitive.name]
            except KeyError:
                continue

            self.merge_params(
                config_primitive.parameter,
                cfg[self.PARAMETER])


class ConfigStore(object):
    """Convenience class that fetches all the instance related data from the
    $RIFT_ARTIFACTS/launchpad/libs directory.
    """

    def __init__(self, log):
        """
        Args:
            log : Log handle.
        """
        self._log = log
        self.converter = ConfigPrimitiveConvertor()

    def merge_vnfd_config(self, nsd_id, vnfd, member_vnf_index):
        """Merges the vnfd config from the config directory.

        Args:
            nsd_id (str): Id of the NSD object
            vnfd_msg : VNFD pb message containing the VNFD id and
                       the member index ref.
        """
        nsd_archive = os.path.join(
            os.getenv('RIFT_ARTIFACTS'),
            "launchpad/libs",
            nsd_id,
            "config")

        self._log.info("Looking for config from the archive {}".format(nsd_archive))

        if not os.path.exists(nsd_archive):
            return

        config_file = os.path.join(nsd_archive,
                                   "{}__{}.yaml".format(vnfd.id, member_vnf_index))

        if not os.path.exists(config_file):
            self._log.info("Could not find VNF initial config in archive: %s", config_file)
            return

        input_data = self.read_from_file(config_file)
        self._log.info("Loaded VNF config file {}: {}".format(config_file, input_data))

        self.converter.merge_vnfd_config(vnfd, input_data)

        self.converter.add_vnfd_initial_config(
            vnfd.vnf_configuration.initial_config_primitive,
            input_data,
        )

    def read_from_file(self, filename):
        with open(filename) as fh:
            input_data = yaml.load(fh)
        return input_data

    def merge_nsd_config(self, nsd):
        nsd_archive = os.path.join(
            os.getenv('RIFT_ARTIFACTS'),
            "launchpad/libs",
            nsd.id,
            "config")

        self._log.info("Looking for config from the archive {}".format(nsd_archive))

        if not os.path.exists(nsd_archive):
            return

        config_file = os.path.join(nsd_archive,
                                   "{}.yaml".format(nsd.id))
        if not os.path.exists(config_file):
            self._log.info("Could not find NS config in archive: %s", config_file)
            return

        input_data = self.read_from_file(config_file)
        self._log.info("Loaded NS config file {}: {}".format(config_file, input_data))

        self.converter.merge_nsd_config(nsd, input_data)

        self.converter.merge_nsd_initial_config(nsd, input_data)
