#!/usr/bin/env python3
"""
The Policy QoS VCI entrypoint module
"""
# Copyright (c) 2019-2021, AT&T Intellectual Property.
# All rights reserved.
#
# SPDX-License-Identifier: LGPL-2.1-only
#

import argparse
import logging
import logging.handlers
import sys

from copy import deepcopy
from traceback import format_tb
from vyatta_policy_qos_vci.qos_config_bond_members import QosConfigBondMembers
from systemd.journal import JournalHandler

import vci

from zmq.error import Again

from vplaned import Controller, ControllerException
from vyatta import configd

from vyatta_policy_qos_vci.provisioner import Provisioner
from vyatta_policy_qos_vci.provisioner import get_config
from vyatta_policy_qos_vci.provisioner import save_config
from vyatta_policy_qos_vci.qos_config import QosConfig
from vyatta_policy_qos_vci.qos_op_mode import convert_if_list
from vyatta_policy_qos_vci.platform import is_hardware_qos_bond_enabled
from vyatta_policy_qos_vci.bond_membership import BondMembership

# Local cache of the system's LAG membership state. It is updated during the
# QoS VCI component start up (the state is fetched from the kernel) or whenever
# the LAG membership state changes (the state is obtained from VCI notifications
# from the LAG component).
_bond_membership = None

def remove_deferred_ingress_maps(config, deferred_in_map_list):
    """
    Delete any deferred ingress-maps from the JSON config dictionary.
    """
    policy_dict = config.get('vyatta-policy-v1:policy')
    if policy_dict is None:
        return

    in_map_list = policy_dict.get('vyatta-policy-qos-v1:ingress-map')
    if in_map_list is None:
        return

    for in_map_name in deferred_in_map_list:
        for index, in_map_dict in enumerate(in_map_list):
            if in_map_dict['id'] == in_map_name:
                del in_map_list[index]
                break


def remove_deferred_egress_maps(config, deferred_eg_map_list):
    """
    Delete any deferred egress-maps from the JSON config dictionary.
    """
    policy_dict = config.get('vyatta-policy-v1:policy')
    if policy_dict is None:
        return

    eg_map_list = policy_dict.get('vyatta-policy-qos-v1:egress-map')
    if eg_map_list is None:
        return

    for eg_map_name in deferred_eg_map_list:
        for index, eg_map_dict in enumerate(eg_map_list):
            if eg_map_dict['id'] == eg_map_name:
                del eg_map_list[index]
                break


def send_qos_config_to_dataplane(new_config):
    """
    We are now ready to send the QoS configuration down to the
    vyatta-dataplane.
    """
    global _bond_membership
    try:
        old_config = get_config()
        LOG.debug(f'old-config: {old_config}')
        LOG.debug(f'new-config: {new_config}')
        prov = Provisioner(old_config, new_config,
            cur_bond_membership=_bond_membership)
        with Controller() as ctrl:
            prov.commands(ctrl)

        LOG.debug(f"deferred ingress-maps: {prov.deferred_ingress_maps}")
        # Copy the requested config, then remove any deferred maps, then
        # save it as the actioned config.
        actioned_config = deepcopy(new_config)
        remove_deferred_ingress_maps(actioned_config,
                                     prov.deferred_ingress_maps)
        LOG.debug(f"deferred egress-maps: {prov.deferred_egress_maps}")
        remove_deferred_egress_maps(actioned_config,
                                    prov.deferred_egress_maps)
        save_config(actioned_config)

    except ControllerException:
        LOG.error("Failed to connect to vplane-controller")

    except Exception:
        tb_type = sys.exc_info()[0]
        tb_value = sys.exc_info()[1]
        tb_info = format_tb(sys.exc_info()[2])
        tb_output = ""
        for line in tb_info:
            tb_output += line

        LOG.error(f"Unhandled exception: {tb_type}\n{tb_value}\n{tb_output}")

    return actioned_config

def bond_membership_update(data):
    """
    VCI notification handler for LAG membership changes reported by the LAG
    component.
    """
    global _bond_membership
    try:
        LOG.debug(f"Received LAG membership change notification: {data}")
        if not is_hardware_qos_bond_enabled():
            LOG.debug("Hardware QoS on LAG feature not supported. Ignore!")
            return
        config = get_config()
        LOG.debug(f'config: {config}')

        # Create LAG membership object from the notification contents
        ntfy_bond_membership = BondMembership(notification=data)

        prov = Provisioner(config, config, cur_bond_membership=_bond_membership,
            bonding_ntfy=ntfy_bond_membership)

        # Update the LAG membership cache
        _bond_membership = ntfy_bond_membership

        with Controller() as ctrl:
            prov.commands(ctrl)

    except ControllerException:
        LOG.error("Failed to connect to vplane-controller")

    except Exception:
        tb_type = sys.exc_info()[0]
        tb_value = sys.exc_info()[1]
        tb_info = format_tb(sys.exc_info()[2])
        tb_output = ""
        for line in tb_info:
            tb_output += line

        LOG.error(f"Unhandled exception: {tb_type}\n{tb_value}\n{tb_output}")


class Config(vci.Config):
    """
    The Configuration mode class for QoS VCI
    """
    config = {}

    def set(self, new_config):
        """
        Update the QoS config down in the dataplane from the VCI JSON
        """
        LOG.debug(f"Config:set - {new_config}")

        # Issue the required NPF and QoS commands, and save the actioned
        # configuration
        self.config = send_qos_config_to_dataplane(new_config)
        return

    def get(self):
        """ What do we need to put in here? """
        LOG.debug("Config:get")
        # If the QoS VCI process has been restarted we may an empty self.config
        # in which case we need to read the current config from the saved
        # config file.
        if not self.config:
            self.config = get_config()

        return self.config

    def check(self, proposed_config):
        """ Move the validation checks from perl to here? """
        global _bond_membership
        LOG.debug(f"Config:check - {proposed_config}")
        if is_hardware_qos_bond_enabled():
            config = QosConfigBondMembers(proposed_config,
                bond_membership=_bond_membership)
        else:
            config = QosConfig(proposed_config)

        for policy_qos in config.policies.values():
            result, error, path = policy_qos.check(f"policy/qos/name/{policy_qos.name}")
            if not result:
                raise vci.Exception("vyatta-policy-qos-vci",
                                    f"{error}",
                                    path)

        for ingress_map in config.ingress_maps.values():
            if not ingress_map.check(proposed_config):
                LOG.info("Config:check failed for ingress-map: "
                         f"{ingress_map.name}")
                raise vci.Exception("vyatta-policy-qos-vci",
                                    "Incomplete ingress-map "
                                    f"{ingress_map.name}",
                                    "policy/ingress-map")

        for egress_map in config.egress_maps.values():
            if not egress_map.check(proposed_config):
                LOG.info("Config:check failed for egress-map: "
                         f"{egress_map.name}")
                raise vci.Exception("vyatta-policy-qos-vci",
                                    "Incomplete egress-map "
                                    f"{egress_map.name}",
                                    "policy/egress-map")

        return


class State(vci.State):
    """
    The Operational mode class for QoS VCI
    """
    def get(self):
        """
        Ask the vyatta-dataplane to generate the JSON for the
        current QoS state and return it
        """
        try:
            op_mode_state = None
            with Controller() as ctrl:
                for dataplane in ctrl.get_dataplanes():
                    with dataplane:
                        cmd = "qos optimised-show"
                        op_mode_state = dataplane.json_command(cmd)

        except ControllerException:
            LOG.error("Failed to connect to vplane-controller")

        except Again:
            LOG.error("op-mode state temporarily unavailable")

        except Exception:
            tb_type = sys.exc_info()[0]
            tb_value = sys.exc_info()[1]
            tb_info = format_tb(sys.exc_info()[2])
            tb_output = ""
            for line in tb_info:
                tb_output += line

            LOG.error(f"Unhandled exception: {tb_type}\n{tb_value}\n"
                      f"{tb_output}")

        try:
            yang_state = {}
            if op_mode_state is not None:
                if_list = convert_if_list("all", op_mode_state,
                                          bond_membership=_bond_membership)
                if if_list:
                    yang_state["if-list"] = if_list

        except Exception:
            tb_type = sys.exc_info()[0]
            tb_value = sys.exc_info()[1]
            tb_info = format_tb(sys.exc_info()[2])
            tb_output = ""
            for line in tb_info:
                tb_output += line

            LOG.error(f"Unhandled exception: {tb_type}\n{tb_value}\n"
                      f"{tb_output}")

        # We must include the namespace at the top level, and everywhere
        # that the namespace changes
        if not yang_state:
            return {}

        return {'vyatta-policy-v1:policy': {'vyatta-policy-qos-v1:qos':
                                            {'state': yang_state}}}


if __name__ == "__main__":
    try:
        PARSER = argparse.ArgumentParser(description='Policy QoS VCI Service')
        PARSER.add_argument('--debug', action='store_true',
                            help='Enabled debugging')
        ARGS = PARSER.parse_args()

        SYSLOG_ID = 'vyatta-policy-qos-vci'
        logging.root.addHandler(JournalHandler(SYSLOG_IDENTIFIER=SYSLOG_ID))
        LOG = logging.getLogger('Policy QoS VCI')

        if ARGS.debug:
            LOG.setLevel(logging.DEBUG)
            LOG.debug("Debug enabled")

        if is_hardware_qos_bond_enabled():
            LOG.debug("Fetching LAG membership state from the kernel")
            _bond_membership = BondMembership()

        LOG.debug("About to register with VCI")

        (vci.Component("net.vyatta.vci.policy.qos")
         .model(vci.Model("net.vyatta.vci.policy.qos.v1")
                .config(Config())
                .state(State())
               )
         .subscribe("vyatta-interfaces-bonding-v1",
                    "bond-membership-update",
                    bond_membership_update)
         .run()
         .wait())

    except Exception:
        LOG.error(f"Unexpected error: {sys.exc_info()[0]}")
