# ==================================================================================
#       Copyright (c) 2020 AT&T Intellectual Property.
#       Copyright (c) 2020 Nokia
#
#   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.
# ==================================================================================
"""
Provides classes and methods to define, raise, reraise and clear alarms.
All actions are implemented by sending RMR messages to the Alarm Adapter.
The alarm target host and port are set by environment variables. The alarm
message contents comply with the JSON schema in file alarm-schema.json.
"""

from ctypes import c_void_p
from enum import Enum, auto
import json
import os
import time
from mdclogpy import Logger
from ricxappframe.rmr import rmr
from ricxappframe.alarm.exceptions import InitFailed

##############
# PRIVATE API
##############

mdc_logger = Logger(name=__name__)
RETRIES = 4

##############
# PUBLIC API
##############

# constants
RIC_ALARM_UPDATE = 110  # message type
ALARM_MGR_SERVICE_NAME_ENV = "ALARM_MGR_SERVICE_NAME"
ALARM_MGR_SERVICE_PORT_ENV = "ALARM_MGR_SERVICE_PORT"

# Publish dict keys as constants for convenience of client code.
# Mixed lower/upper casing to comply with the Adapter JSON requirements.
KEY_ALARM = "alarm"
KEY_MANAGED_OBJECT_ID = "managedObjectId"
KEY_APPLICATION_ID = "applicationId"
KEY_SPECIFIC_PROBLEM = "specificProblem"
KEY_PERCEIVED_SEVERITY = "perceivedSeverity"
KEY_ADDITIONAL_INFO = "additionalInfo"
KEY_IDENTIFYING_INFO = "identifyingInfo"
KEY_ALARM_ACTION = "AlarmAction"
KEY_ALARM_TIME = "AlarmTime"


class AlarmAction(Enum):
    """
    Action to perform at the Alarm Adapter
    """
    RAISE = auto()
    CLEAR = auto()
    CLEARALL = auto()


class AlarmSeverity(Enum):
    """
    Severity of an alarm
    """
    UNSPECIFIED = auto()
    CRITICAL = auto()
    MAJOR = auto()
    MINOR = auto()
    WARNING = auto()
    CLEARED = auto()
    DEFAULT = auto()


class AlarmDetail(dict):
    """
    An alarm that can be raised or cleared.

    Parameters
    ----------
    managed_object_id: str
        The name of the managed object that is the cause of the fault (required)

    application_id: str
        The name of the process that raised the alarm (required)

    specific_problem: int
        The problem that is the cause of the alarm

    perceived_severity: AlarmSeverity
        The severity of the alarm, a value from the enum.

    identifying_info: str
        Identifying additional information, which is part of alarm identity

    additional_info: str
        Additional information given by the application (optional)
    """
    # pylint: disable=too-many-arguments
    def __init__(self,
                 managed_object_id: str,
                 application_id: str,
                 specific_problem: int,
                 perceived_severity: AlarmSeverity,
                 identifying_info: str,
                 additional_info: str = ""):
        """
        Creates an object with the specified items.
        """
        dict.__init__(self)
        self[KEY_MANAGED_OBJECT_ID] = managed_object_id
        self[KEY_APPLICATION_ID] = application_id
        self[KEY_SPECIFIC_PROBLEM] = specific_problem
        self[KEY_PERCEIVED_SEVERITY] = perceived_severity.name
        self[KEY_IDENTIFYING_INFO] = identifying_info
        self[KEY_ADDITIONAL_INFO] = additional_info


class AlarmManager:
    """
    Provides an API for an Xapp to raise and clear alarms by sending messages
    via RMR directly to an Alarm Adapter. Requires environment variables
    ALARM_MGR_SERVICE_NAME and ALARM_MGR_SERVICE_PORT with the destination host
    (service) name and port number; raises an exception if not found.

    Parameters
    ----------
    vctx: ctypes c_void_p
        Pointer to RMR context obtained by initializing RMR.
        The context is used to allocate space and send messages.

    managed_object_id: str
        The name of the managed object that raises alarms

    application_id: str
        The name of the process that raises alarms
    """
    def __init__(self,
                 vctx: c_void_p,
                 managed_object_id: str,
                 application_id: str):
        """
        Creates an alarm manager.
        """
        self.vctx = vctx
        self.managed_object_id = managed_object_id
        self.application_id = application_id
        service = os.environ.get(ALARM_MGR_SERVICE_NAME_ENV, None)
        port = os.environ.get(ALARM_MGR_SERVICE_PORT_ENV, None)
        if service is None or port is None:
            mdc_logger.error("init: missing env var(s) {0}, {1}".format(ALARM_MGR_SERVICE_NAME_ENV, ALARM_MGR_SERVICE_PORT_ENV))
            raise InitFailed
        target = "{0}:{1}".format(service, port)
        self._wormhole_id = rmr.rmr_wh_open(self.vctx, target.encode('utf-8'))
        if rmr.rmr_wh_state(self.vctx, self._wormhole_id) != rmr.RMR_OK:
            mdc_logger.error("init: failed to open wormhole to target {}".format(target))
            raise InitFailed

    def create_alarm(self,
                     specific_problem: int,
                     perceived_severity: AlarmSeverity,
                     identifying_info: str,
                     additional_info: str = ""):
        """
        Convenience method that creates an alarm instance, an AlarmDetail object,
        using cached values for the managed object ID and application ID.

        Parameters
        ----------
        specific_problem: int
            The problem that is the cause of the alarm

        perceived_severity: AlarmSeverity
            The severity of the alarm, a value from the enum.

        identifying_info: str
            Identifying additional information, which is part of alarm identity

        additional_info: str
            Additional information given by the application (optional)

        Returns
        -------
        AlarmDetail
        """
        return AlarmDetail(managed_object_id=self.managed_object_id,
                           application_id=self.application_id,
                           specific_problem=specific_problem, perceived_severity=perceived_severity,
                           identifying_info=identifying_info, additional_info=additional_info)

    @staticmethod
    def _create_alarm_message(alarm: AlarmDetail, action: AlarmAction):
        """
        Creates a dict with the specified alarm detail plus action and time.
        Uses the current system time in milliseconds since the Epoch.

        Parameters
        ----------
        detail: AlarmDetail
            The alarm details.

        action: AlarmAction
            The action to perform at the Alarm Adapter on this alarm.
        """
        return {
            **alarm,
            KEY_ALARM_ACTION: action.name,
            KEY_ALARM_TIME: int(round(time.time() * 1000))
        }

    def _rmr_send_alarm(self, msg: dict):
        """
        Serializes the dict and sends the result via RMR using a predefined message
        type to the wormhole initialized at start.

        Parameters
        ----------
        msg: dict
            Dictionary with alarm message to encode and send

        Returns
        -------
        bool
            True if the send succeeded (possibly with retries), False otherwise
        """
        payload = json.dumps(msg).encode()
        mdc_logger.debug("_rmr_send_alarm: payload is {}".format(payload))
        sbuf = rmr.rmr_alloc_msg(vctx=self.vctx, size=len(payload), payload=payload,
                                 mtype=RIC_ALARM_UPDATE, gen_transaction_id=True)

        for _ in range(0, RETRIES):
            sbuf = rmr.rmr_wh_send_msg(self.vctx, self._wormhole_id, sbuf)
            post_send_summary = rmr.message_summary(sbuf)
            mdc_logger.debug("_rmr_send_alarm: try {0} result is {1}".format(_, post_send_summary[rmr.RMR_MS_MSG_STATE]))
            # stop trying if RMR does not indicate retry
            if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_ERR_RETRY:
                break

        rmr.rmr_free_msg(sbuf)
        if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_OK:
            mdc_logger.warning("_rmr_send_alarm: failed after {} retries".format(RETRIES))
            return False

        return True

    def raise_alarm(self, detail: AlarmDetail):
        """
        Builds and sends a message to the AlarmAdapter to raise an alarm
        with the specified detail.

        Parameters
        ----------
        detail: AlarmDetail
            Alarm to raise

        Returns
        -------
        bool
            True if the send succeeded (possibly with retries), False otherwise
        """
        msg = self._create_alarm_message(detail, AlarmAction.RAISE)
        return self._rmr_send_alarm(msg)

    def clear_alarm(self, detail: AlarmDetail):
        """
        Builds and sends a message to the AlarmAdapter to clear the alarm
        with the specified detail.

        Parameters
        ----------
        detail: AlarmDetail
            Alarm to clear

        Returns
        -------
        bool
            True if the send succeeded (possibly with retries), False otherwise
        """
        msg = self._create_alarm_message(detail, AlarmAction.CLEAR)
        return self._rmr_send_alarm(msg)

    def reraise_alarm(self, detail: AlarmDetail):
        """
        Builds and sends a message to the AlarmAdapter to clear the alarm with the
        the specified detail, then builds and sends a message to raise the alarm again.

        Parameters
        ----------
        detail: AlarmDetail
            Alarm to clear and raise again.

        Returns
        -------
        bool
            True if the send succeeded (possibly with retries), False otherwise
        """
        success = self.clear_alarm(detail)
        if success:
            success = self.raise_alarm(detail)
        return success

    def clear_all_alarms(self):
        """
        Builds and sends a message to the AlarmAdapter to clear all alarms.

        Returns
        -------
        bool
            True if the send succeeded (possibly with retries), False otherwise
        """
        detail = self.create_alarm(0, AlarmSeverity.DEFAULT, "", "")
        msg = self._create_alarm_message(detail, AlarmAction.CLEARALL)
        return self._rmr_send_alarm(msg)
