# Copyright 2017-present Open Networking Foundation
#
# 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 arrow
from twisted.internet.defer import inlineCallbacks, returnValue 
from voltha_protos.device_pb2 import PmConfig, PmGroupConfig
from voltha_protos.events_pb2 import KpiEvent2, MetricInformation, MetricMetaData, KpiEventType
from voltha_protos.events_pb2 import Event, EventType
from pyvoltha.adapters.extensions.events.kpi.adapter_pm_metrics import AdapterPmMetrics
from pyvoltha.adapters.extensions.omci.omci_entities import \
    EthernetFrameUpstreamPerformanceMonitoringHistoryData, \
    EthernetFrameDownstreamPerformanceMonitoringHistoryData, \
    EthernetFrameExtendedPerformanceMonitoring, \
    EthernetFrameExtendedPerformanceMonitoring64Bit, \
    EthernetPMMonitoringHistoryData, FecPerformanceMonitoringHistoryData, \
    GemPortNetworkCtpMonitoringHistoryData, XgPonTcPerformanceMonitoringHistoryData, \
    XgPonDownstreamPerformanceMonitoringHistoryData, \
    XgPonUpstreamPerformanceMonitoringHistoryData


class OnuPmIntervalMetrics(AdapterPmMetrics):
    """
    ONU OMCI PM Interval metrics

    These differ from other PM Metrics as they are collected and generated as a
    result of receipt of OMCI get responses on various PM History MEs.  They are
    also always managed as a group with a fixed frequency of 15 minutes.
    """
    ME_ID_INFO = {
        EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id: 'Ethernet_Bridge_Port_History',
        EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id: 'Ethernet_Bridge_Port_History',
        EthernetFrameExtendedPerformanceMonitoring.class_id: 'Ethernet_Bridge_Port_History',
        EthernetFrameExtendedPerformanceMonitoring64Bit.class_id: 'Ethernet_Bridge_Port_History',
        EthernetPMMonitoringHistoryData.class_id: 'Ethernet_UNI_History',
        FecPerformanceMonitoringHistoryData.class_id: 'FEC_History',
        GemPortNetworkCtpMonitoringHistoryData.class_id: 'GEM_Port_History',
        XgPonTcPerformanceMonitoringHistoryData.class_id: 'xgPON_TC_History',
        XgPonDownstreamPerformanceMonitoringHistoryData.class_id: 'xgPON_Downstream_History',
        XgPonUpstreamPerformanceMonitoringHistoryData.class_id: 'xgPON_Upstream_History'
    }
    ETHERNET_BRIDGE_HISTORY_ENABLED = True
    ETHERNET_UNI_HISTORY_ENABLED = True
    FEC_HISTORY_ENABLED = True
    GEM_PORT_HISTORY_ENABLED = False
    TRANS_CONV_HISTORY_ENABLED = False
    XGPON_DOWNSTREAM_HISTORY = False
    XGPON_UPSTREAM_HISTORY = False

    def __init__(self, event_mgr, core_proxy, device_id, logical_device_id, serial_number, **kwargs):
        super(OnuPmIntervalMetrics, self).__init__(event_mgr, core_proxy, device_id, logical_device_id, serial_number,
                                                   grouped=True, freq_override=False,
                                                   **kwargs)
        ethernet_bridge_history = {
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),
            ('parent_class_id', PmConfig.CONTEXT),
            ('parent_entity_id', PmConfig.CONTEXT),
            ('upstream', PmConfig.CONTEXT),

            ("drop_events", PmConfig.COUNTER),
            ("octets", PmConfig.COUNTER),
            ("packets", PmConfig.COUNTER),
            ("broadcast_packets", PmConfig.COUNTER),
            ("multicast_packets", PmConfig.COUNTER),
            ("crc_errored_packets", PmConfig.COUNTER),
            ("undersize_packets", PmConfig.COUNTER),
            ("oversize_packets", PmConfig.COUNTER),
            ("64_octets", PmConfig.COUNTER),
            ("65_to_127_octets", PmConfig.COUNTER),
            ("128_to_255_octets", PmConfig.COUNTER),
            ("256_to_511_octets", PmConfig.COUNTER),
            ("512_to_1023_octets", PmConfig.COUNTER),
            ("1024_to_1518_octets", PmConfig.COUNTER)
        }
        self._ethernet_bridge_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                                for (m, t) in ethernet_bridge_history}

        ethernet_uni_history = {   # Ethernet History Data (Class ID 24)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("fcs_errors", PmConfig.COUNTER),
            ("excessive_collision_counter", PmConfig.COUNTER),
            ("late_collision_counter", PmConfig.COUNTER),
            ("frames_too_long", PmConfig.COUNTER),
            ("buffer_overflows_on_rx", PmConfig.COUNTER),
            ("buffer_overflows_on_tx", PmConfig.COUNTER),
            ("single_collision_frame_counter", PmConfig.COUNTER),
            ("multiple_collisions_frame_counter", PmConfig.COUNTER),
            ("sqe_counter", PmConfig.COUNTER),
            ("deferred_tx_counter", PmConfig.COUNTER),
            ("internal_mac_tx_error_counter", PmConfig.COUNTER),
            ("carrier_sense_error_counter", PmConfig.COUNTER),
            ("alignment_error_counter", PmConfig.COUNTER),
            ("internal_mac_rx_error_counter", PmConfig.COUNTER),
        }
        self._ethernet_uni_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                             for (m, t) in ethernet_uni_history}

        fec_history = {   # FEC History Data (Class ID 312)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("corrected_bytes", PmConfig.COUNTER),
            ("corrected_code_words", PmConfig.COUNTER),
            ("uncorrectable_code_words", PmConfig.COUNTER),
            ("total_code_words", PmConfig.COUNTER),
            ("fec_seconds", PmConfig.COUNTER),
        }
        self._fec_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                    for (m, t) in fec_history}

        gem_port_history = {  # GEM Port Network CTP History Data (Class ID 341)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("transmitted_gem_frames", PmConfig.COUNTER),
            ("received_gem_frames", PmConfig.COUNTER),
            ("received_payload_bytes", PmConfig.COUNTER),
            ("transmitted_payload_bytes", PmConfig.COUNTER),
            ("encryption_key_errors", PmConfig.COUNTER),
        }
        self._gem_port_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                         for (m, t) in gem_port_history}

        xgpon_tc_history = {  # XgPon TC History Data (Class ID 344)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("psbd_hec_error_count", PmConfig.COUNTER),
            ("xgtc_hec_error_count", PmConfig.COUNTER),
            ("unknown_profile_count", PmConfig.COUNTER),
            ("transmitted_xgem_frames", PmConfig.COUNTER),
            ("fragment_xgem_frames", PmConfig.COUNTER),
            ("xgem_hec_lost_words_count", PmConfig.COUNTER),
            ("xgem_key_errors", PmConfig.COUNTER),
            ("xgem_hec_error_count", PmConfig.COUNTER),
        }
        self._xgpon_tc_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                         for (m, t) in xgpon_tc_history}

        xgpon_downstream_history = {  # XgPon Downstream History Data (Class ID 345)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("ploam_mic_error_count", PmConfig.COUNTER),
            ("downstream_ploam_messages_count", PmConfig.COUNTER),
            ("profile_messages_received", PmConfig.COUNTER),
            ("ranging_time_messages_received", PmConfig.COUNTER),
            ("deactivate_onu_id_messages_received", PmConfig.COUNTER),
            ("disable_serial_number_messages_received", PmConfig.COUNTER),
            ("request_registration_messages_received", PmConfig.COUNTER),
            ("assign_alloc_id_messages_received", PmConfig.COUNTER),
            ("key_control_messages_received", PmConfig.COUNTER),
            ("sleep_allow_messages_received", PmConfig.COUNTER),
            ("baseline_omci_messages_received_count", PmConfig.COUNTER),
            ("extended_omci_messages_received_count", PmConfig.COUNTER),
            ("assign_onu_id_messages_received", PmConfig.COUNTER),
            ("omci_mic_error_count", PmConfig.COUNTER),
        }
        self._xgpon_downstream_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                                 for (m, t) in xgpon_downstream_history}

        xgpon_upstream_history = {  # XgPon Upstream History Data (Class ID 346)
            ('class_id', PmConfig.CONTEXT),
            ('entity_id', PmConfig.CONTEXT),
            ("interval_end_time", PmConfig.CONTEXT),

            ("upstream_ploam_message_count", PmConfig.COUNTER),
            ("serial_number_onu_message_count", PmConfig.COUNTER),
            ("registration_message_count", PmConfig.COUNTER),
            ("key_report_message_count", PmConfig.COUNTER),
            ("acknowledge_message_count", PmConfig.COUNTER),
            ("sleep_request_message_count", PmConfig.COUNTER),
        }
        self._xgpon_upstream_history_config = {m: PmConfig(name=m, type=t, enabled=True)
                                               for (m, t) in xgpon_upstream_history}
        self._configs = {
            EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id: self._ethernet_bridge_history_config,
            EthernetFrameDownstreamPerformanceMonitoringHistoryData.class_id: self._ethernet_bridge_history_config,
            EthernetFrameExtendedPerformanceMonitoring.class_id: self._ethernet_bridge_history_config,
            EthernetFrameExtendedPerformanceMonitoring64Bit.class_id: self._ethernet_bridge_history_config,
            EthernetPMMonitoringHistoryData.class_id: self._ethernet_uni_history_config,
            FecPerformanceMonitoringHistoryData.class_id: self._fec_history_config,
            GemPortNetworkCtpMonitoringHistoryData.class_id: self._gem_port_history_config,
            XgPonTcPerformanceMonitoringHistoryData.class_id: self._xgpon_tc_history_config,
            XgPonDownstreamPerformanceMonitoringHistoryData.class_id: self._xgpon_downstream_history_config,
            XgPonUpstreamPerformanceMonitoringHistoryData.class_id: self._xgpon_upstream_history_config
        }

    def update(self, pm_config):
        """
        Update the PM Configuration.

        For historical PM Intervals, the frequency always zero since the actual collection
        and publishing is provided by the OpenOMCI library

        :param pm_config:
        """
        self.log.debug('update')

        try:
            if pm_config.grouped:
                for group in pm_config.groups:
                    group_config = self.pm_group_metrics.get(group.group_name)
                    if group_config is not None and group_config.enabled != group.enabled:
                        group_config.enabled = group.enabled
                        # TODO: For OMCI PM Metrics, tie this into add/remove of the PM Interval ME itself
            else:
                msg = 'There are on independent OMCI Interval metrics, only group metrics at this time'
                raise NotImplemented(msg)

        except Exception as e:
            self.log.exception('update-failure', e=e)
            raise

    def make_proto(self, pm_config=None):
        """
        From the PM Configurations defined in this class's initializer, create
        the PMConfigs protobuf message that defines our PM configuration and
        data.

        All ONU PM Interval metrics are grouped metrics that are generated autonmouslly
        from the OpenOMCI Performance Intervals state machine.

        :param pm_config (PMConfigs) PM Configuration message to add OpenOMCI config items too
        :return: (PmConfigs) PM Configuration Protobuf message
        """
        assert pm_config is not None

        pm_ethernet_bridge_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[EthernetFrameUpstreamPerformanceMonitoringHistoryData.class_id],
                                                   group_freq=0,
                                                   enabled=OnuPmIntervalMetrics.ETHERNET_BRIDGE_HISTORY_ENABLED)
        self.pm_group_metrics[pm_ethernet_bridge_history.group_name] = pm_ethernet_bridge_history

        for m in sorted(self._ethernet_bridge_history_config):
            pm = self._ethernet_bridge_history_config[m]
            pm_ethernet_bridge_history.metrics.extend([PmConfig(name=pm.name,
                                                                type=pm.type,
                                                                enabled=pm.enabled)])

        pm_ethernet_uni_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[EthernetPMMonitoringHistoryData.class_id],
                                                group_freq=0,
                                                enabled=OnuPmIntervalMetrics.ETHERNET_UNI_HISTORY_ENABLED)
        self.pm_group_metrics[pm_ethernet_uni_history.group_name] = pm_ethernet_uni_history

        for m in sorted(self._ethernet_uni_history_config):
            pm = self._ethernet_uni_history_config[m]
            pm_ethernet_uni_history.metrics.extend([PmConfig(name=pm.name,
                                                             type=pm.type,
                                                             enabled=pm.enabled)])

        pm_fec_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[FecPerformanceMonitoringHistoryData.class_id],
                                       group_freq=0,
                                       enabled=OnuPmIntervalMetrics.FEC_HISTORY_ENABLED)
        self.pm_group_metrics[pm_fec_history.group_name] = pm_fec_history

        for m in sorted(self._fec_history_config):
            pm = self._fec_history_config[m]
            pm_fec_history.metrics.extend([PmConfig(name=pm.name,
                                                    type=pm.type,
                                                    enabled=pm.enabled)])

        pm_gem_port_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[GemPortNetworkCtpMonitoringHistoryData.class_id],
                                            group_freq=0,
                                            enabled=OnuPmIntervalMetrics.GEM_PORT_HISTORY_ENABLED)
        self.pm_group_metrics[pm_gem_port_history.group_name] = pm_gem_port_history

        for m in sorted(self._gem_port_history_config):
            pm = self._gem_port_history_config[m]
            pm_gem_port_history.metrics.extend([PmConfig(name=pm.name,
                                                         type=pm.type,
                                                         enabled=pm.enabled)])

        pm_xgpon_tc_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonTcPerformanceMonitoringHistoryData.class_id],
                                            group_freq=0,
                                            enabled=OnuPmIntervalMetrics.TRANS_CONV_HISTORY_ENABLED)
        self.pm_group_metrics[pm_xgpon_tc_history.group_name] = pm_xgpon_tc_history

        for m in sorted(self._xgpon_tc_history_config):
            pm = self._xgpon_tc_history_config[m]
            pm_xgpon_tc_history.metrics.extend([PmConfig(name=pm.name,
                                                         type=pm.type,
                                                         enabled=pm.enabled)])

        pm_xgpon_downstream_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonDownstreamPerformanceMonitoringHistoryData.class_id],
                                                    group_freq=0,
                                                    enabled=OnuPmIntervalMetrics.XGPON_DOWNSTREAM_HISTORY)
        self.pm_group_metrics[pm_xgpon_downstream_history.group_name] = pm_xgpon_downstream_history

        for m in sorted(self._xgpon_downstream_history_config):
            pm = self._xgpon_downstream_history_config[m]
            pm_xgpon_downstream_history.metrics.extend([PmConfig(name=pm.name,
                                                                 type=pm.type,
                                                                 enabled=pm.enabled)])

        pm_xgpon_upstream_history = PmGroupConfig(group_name=OnuPmIntervalMetrics.ME_ID_INFO[XgPonUpstreamPerformanceMonitoringHistoryData.class_id],
                                                  group_freq=0,
                                                  enabled=OnuPmIntervalMetrics.XGPON_UPSTREAM_HISTORY)
        self.pm_group_metrics[pm_xgpon_upstream_history.group_name] = pm_xgpon_upstream_history

        for m in sorted(self._xgpon_upstream_history_config):
            pm = self._xgpon_upstream_history_config[m]
            pm_xgpon_upstream_history.metrics.extend([PmConfig(name=pm.name,
                                                               type=pm.type,
                                                               enabled=pm.enabled)])

        pm_config.groups.extend([stats for stats in self.pm_group_metrics.itervalues()])

        return pm_config

    @inlineCallbacks
    def publish_metrics(self, interval_data):
        """
        Collect the metrics for this ONU PM Interval

        :param interval_data: (dict) PM interval dictionary with structure of
                    {
                        'class_id': self._class_id,
                        'entity_id': self._entity_id,
                        'me_name': self._entity.__name__,   # Mostly for debugging...
                        'interval_utc_time': None,
                        # Counters added here as they are retrieved
                    }

        :return: (dict) Key/Value of metric data
        """
        self.log.debug('publish-metrics')

        try:
            # Locate config
            now = arrow.utcnow()
            class_id = interval_data['class_id']
            config = self._configs.get(class_id)
            group = self.pm_group_metrics.get(OnuPmIntervalMetrics.ME_ID_INFO.get(class_id, ''))

            if config is not None and group is not None and group.enabled:
                # Extract only the metrics we need to publish
                metrics = dict()
                context = {
                    'interval_start_time': str(now.replace(minute=int(now.minute / 15) * 15,
                                                           second=0,
                                                           microsecond=0).timestamp)
                }
                for metric, config_item in config.items():
                    if config_item.type == PmConfig.CONTEXT and metric in interval_data:
                        context[metric] = str(interval_data[metric])

                    elif (config_item.type in (PmConfig.COUNTER, PmConfig.GAUGE, PmConfig.STATE) and
                          metric in interval_data and
                          config_item.enabled):
                        metrics[metric] = interval_data[metric]

                if len(metrics):
                    metadata = MetricMetaData(title=group.group_name,
                                              ts=now.float_timestamp,
                                              logical_device_id=self.logical_device_id,
                                              serial_no=self.serial_number,
                                              device_id=self.device_id,
                                              context=context)
                    slice_data = [MetricInformation(metadata=metadata, metrics=metrics)]
                    raised_ts = arrow.utcnow().timestamp
                    event_header = self.event_mgr.get_event_header(EventType.KPI_EVENT2, 
                                                                   self._category,
                                                                   self._sub_category,
                                                                   self._event, 
                                                                   raised_ts)

                    event_body = KpiEvent2(type=KpiEventType.slice,
                                          ts=now.float_timestamp,
                                          slice_data=slice_data)

                    self.log.debug('Sending-onu-metrics-to-kafka')
                    yield self.event_mgr.send_event(event_header, event_body)

        except Exception as e:
            self.log.exception('failed-to-submit-kpis', e=e)
