# Copyright 2016 Cloudbase Solutions Srl
#
# All Rights Reserved.
#
#    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.
"""
Utility class for metrics related operations.
Based on the "root/virtualization/v2" namespace available starting with
Hyper-V Server / Windows Server 2012.
"""

from oslo_log import log as logging

from os_win._i18n import _, _LW
from os_win import exceptions
from os_win.utils import _wqlutils
from os_win.utils import baseutils

LOG = logging.getLogger(__name__)


class MetricsUtils(baseutils.BaseUtilsVirt):

    _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized'
    _DVD_DISK_RES_SUB_TYPE = 'Microsoft:Hyper-V:Virtual CD/DVD Disk'
    _STORAGE_ALLOC_SETTING_DATA_CLASS = 'Msvm_StorageAllocationSettingData'
    _PROCESSOR_SETTING_DATA_CLASS = 'Msvm_ProcessorSettingData'
    _SYNTH_ETH_PORT_SET_DATA = 'Msvm_SyntheticEthernetPortSettingData'
    _PORT_ALLOC_SET_DATA = 'Msvm_EthernetPortAllocationSettingData'
    _PORT_ALLOC_ACL_SET_DATA = 'Msvm_EthernetSwitchPortAclSettingData'
    _BASE_METRICS_VALUE = 'Msvm_BaseMetricValue'

    _CPU_METRICS = 'Aggregated Average CPU Utilization'
    _MEMORY_METRICS = 'Aggregated Average Memory Utilization'
    _NET_IN_METRICS = 'Filtered Incoming Network Traffic'
    _NET_OUT_METRICS = 'Filtered Outgoing Network Traffic'
    # Disk metrics are supported from Hyper-V 2012 R2
    _DISK_RD_METRICS = 'Disk Data Read'
    _DISK_WR_METRICS = 'Disk Data Written'
    _DISK_LATENCY_METRICS = 'Average Disk Latency'
    _DISK_IOPS_METRICS = 'Average Normalized Disk Throughput'

    _METRICS_ENABLED = 2

    def __init__(self, host='.'):
        super(MetricsUtils, self).__init__(host)
        self._metrics_svc_obj = None
        self._metrics_defs_obj = {}

    @property
    def _metrics_svc(self):
        if not self._metrics_svc_obj:
            self._metrics_svc_obj = self._compat_conn.Msvm_MetricService()[0]
        return self._metrics_svc_obj

    @property
    def _metrics_defs(self):
        if not self._metrics_defs_obj:
            self._cache_metrics_defs()
        return self._metrics_defs_obj

    def _cache_metrics_defs(self):
        for metrics_def in self._conn.CIM_BaseMetricDefinition():
            self._metrics_defs_obj[metrics_def.ElementName] = metrics_def

    def enable_vm_metrics_collection(self, vm_name):
        vm = self._get_vm(vm_name)
        disks = self._get_vm_resources(vm_name,
                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
        filtered_disks = [d for d in disks if
                          d.ResourceSubType is not self._DVD_DISK_RES_SUB_TYPE]

        # enable metrics for disk.
        for disk in filtered_disks:
            self._enable_metrics(disk)

        metrics_names = [self._CPU_METRICS, self._MEMORY_METRICS]
        self._enable_metrics(vm, metrics_names)

    def enable_port_metrics_collection(self, switch_port_name):
        port = self._get_switch_port(switch_port_name)
        metrics_names = [self._NET_IN_METRICS, self._NET_OUT_METRICS]
        self._enable_metrics(port, metrics_names)

    def _enable_metrics(self, element, metrics_names=None):
        if not metrics_names:
            definition_paths = [None]
        else:
            definition_paths = []
            for metrics_name in metrics_names:
                metrics_def = self._metrics_defs.get(metrics_name)
                if not metrics_def:
                    LOG.warning(_LW("Metric not found: %s"), metrics_name)
                    continue
                definition_paths.append(metrics_def.path_())

        element_path = element.path_()
        for definition_path in definition_paths:
            self._metrics_svc.ControlMetrics(
                Subject=element_path,
                Definition=definition_path,
                MetricCollectionEnabled=self._METRICS_ENABLED)

    def get_cpu_metrics(self, vm_name):
        vm = self._get_vm(vm_name)
        cpu_sd = self._get_vm_resources(vm_name,
                                        self._PROCESSOR_SETTING_DATA_CLASS)[0]
        cpu_metrics_def = self._metrics_defs[self._CPU_METRICS]
        cpu_metrics_aggr = self._get_metrics(vm, cpu_metrics_def)

        cpu_used = 0
        if cpu_metrics_aggr:
            cpu_used = int(cpu_metrics_aggr[0].MetricValue)

        return (cpu_used,
                int(cpu_sd.VirtualQuantity),
                int(vm.OnTimeInMilliseconds))

    def get_memory_metrics(self, vm_name):
        vm = self._get_vm(vm_name)
        memory_def = self._metrics_defs[self._MEMORY_METRICS]
        metrics_memory = self._get_metrics(vm, memory_def)
        memory_usage = 0
        if metrics_memory:
            memory_usage = int(metrics_memory[0].MetricValue)
        return memory_usage

    def get_vnic_metrics(self, vm_name):
        ports = self._get_vm_resources(vm_name, self._PORT_ALLOC_SET_DATA)
        vnics = self._get_vm_resources(vm_name, self._SYNTH_ETH_PORT_SET_DATA)

        metrics_def_in = self._metrics_defs[self._NET_IN_METRICS]
        metrics_def_out = self._metrics_defs[self._NET_OUT_METRICS]

        for port in ports:
            vnic = [v for v in vnics if port.Parent == v.path_()][0]
            port_acls = _wqlutils.get_element_associated_class(
                self._conn, self._PORT_ALLOC_ACL_SET_DATA,
                element_instance_id=port.InstanceID)

            metrics_value_instances = self._get_metrics_value_instances(
                port_acls, self._BASE_METRICS_VALUE)
            metrics_values = self._sum_metrics_values_by_defs(
                metrics_value_instances, [metrics_def_in, metrics_def_out])

            yield {
                'rx_mb': metrics_values[0],
                'tx_mb': metrics_values[1],
                'element_name': vnic.ElementName,
                'address': vnic.Address
            }

    def get_disk_metrics(self, vm_name):
        metrics_def_r = self._metrics_defs[self._DISK_RD_METRICS]
        metrics_def_w = self._metrics_defs[self._DISK_WR_METRICS]

        disks = self._get_vm_resources(vm_name,
                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
        for disk in disks:
            metrics_values = self._get_metrics_values(
                disk, [metrics_def_r, metrics_def_w])

            yield {
                # Values are in megabytes
                'read_mb': metrics_values[0],
                'write_mb': metrics_values[1],
                'instance_id': disk.InstanceID,
                'host_resource': disk.HostResource[0]
            }

    def get_disk_latency_metrics(self, vm_name):
        metrics_latency_def = self._metrics_defs[self._DISK_LATENCY_METRICS]

        disks = self._get_vm_resources(vm_name,
                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
        for disk in disks:
            metrics_values = self._get_metrics_values(
                disk, [metrics_latency_def])

            yield {
                'disk_latency': metrics_values[0],
                'instance_id': disk.InstanceID,
            }

    def get_disk_iops_count(self, vm_name):
        metrics_def_iops = self._metrics_defs[self._DISK_IOPS_METRICS]

        disks = self._get_vm_resources(vm_name,
                                       self._STORAGE_ALLOC_SETTING_DATA_CLASS)
        for disk in disks:
            metrics_values = self._get_metrics_values(
                disk, [metrics_def_iops])

            yield {
                'iops_count': metrics_values[0],
                'instance_id': disk.InstanceID,
            }

    @staticmethod
    def _sum_metrics_values(metrics):
        return sum([int(metric.MetricValue) for metric in metrics])

    def _sum_metrics_values_by_defs(self, element_metrics, metrics_defs):
        metrics_values = []
        for metrics_def in metrics_defs:
            if metrics_def:
                metrics = self._filter_metrics(element_metrics, metrics_def)
                metrics_values.append(self._sum_metrics_values(metrics))
            else:
                # In case the metric is not defined on this host
                metrics_values.append(0)
        return metrics_values

    def _get_metrics_value_instances(self, elements, result_class):
        instances = []
        for el in elements:
            # NOTE(abalutoiu): Msvm_MetricForME is the association between
            # an element and all the metric values maintained for it.
            el_metric = [
                x.Dependent for x in self._conn.Msvm_MetricForME(
                    Antecedent=el.path_())]
            el_metric = [
                x for x in el_metric if x.path().Class == result_class]
            if el_metric:
                instances.append(el_metric[0])

        return instances

    def _get_metrics_values(self, element, metrics_defs):
        element_metrics = [
            x.Dependent for x in self._conn.Msvm_MetricForME(
                Antecedent=element.path_())]
        return self._sum_metrics_values_by_defs(element_metrics, metrics_defs)

    def _get_metrics(self, element, metrics_def):
        metrics = [
            x.Dependent for x in self._conn.Msvm_MetricForME(
                Antecedent=element.path_())]
        return self._filter_metrics(metrics, metrics_def)

    @staticmethod
    def _filter_metrics(all_metrics, metrics_def):
        return [v for v in all_metrics if
                v.MetricDefinitionId == metrics_def.Id]

    def _get_vm_resources(self, vm_name, resource_class):
        setting_data = self._get_vm_setting_data(vm_name)
        return _wqlutils.get_element_associated_class(
            self._conn, resource_class,
            element_instance_id=setting_data.InstanceID)

    def _get_vm(self, vm_name):
        vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name)
        return self._unique_result(vms, vm_name)

    def _get_switch_port(self, port_name):
        ports = self._conn.Msvm_SyntheticEthernetPortSettingData(
            ElementName=port_name)
        return self._unique_result(ports, port_name)

    def _get_vm_setting_data(self, vm_name):
        vssds = self._conn.Msvm_VirtualSystemSettingData(
            ElementName=vm_name,
            VirtualSystemType=self._VIRTUAL_SYSTEM_TYPE_REALIZED)
        return self._unique_result(vssds, vm_name)

    @staticmethod
    def _unique_result(objects, resource_name):
        n = len(objects)
        if n == 0:
            raise exceptions.NotFound(resource=resource_name)
        elif n > 1:
            raise exceptions.OSWinException(
                _('Duplicate resource name found: %s') % resource_name)
        else:
            return objects[0]
