# Copyright 2013 Cloudbase Solutions Srl
# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP
#
# 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 VM related operations.
Based on the "root/virtualization/v2" namespace available starting with
Hyper-V Server / Windows Server 2012.
"""

import sys

if sys.platform == 'win32':
    import wmi

from monasca_agent.collector.virt import inspector


class HyperVException(inspector.InspectorException):
    pass


class UtilsV2(object):

    _VIRTUAL_SYSTEM_TYPE_REALIZED = 'Microsoft:Hyper-V:System:Realized'

    _PROC_SETTING = 'Msvm_ProcessorSettingData'
    _MEMORY_SETTING = "Msvm_MemorySettingData"
    _SYNTH_ETH_PORT = 'Msvm_SyntheticEthernetPortSettingData'
    _ETH_PORT_ALLOC = 'Msvm_EthernetPortAllocationSettingData'
    _PORT_ACL_SET_DATA = 'Msvm_EthernetSwitchPortAclSettingData'
    _STORAGE_ALLOC = 'Msvm_StorageAllocationSettingData'
    _VS_SETTING_DATA = 'Msvm_VirtualSystemSettingData'
    _METRICS_ME = 'Msvm_MetricForME'
    _BASE_METRICS_VALUE = 'Msvm_BaseMetricValue'

    _CPU_METRIC_NAME = 'Aggregated Average CPU Utilization'
    _MEMORY_METRIC_NAME = 'Aggregated Average Memory Utilization'
    _NET_IN_METRIC_NAME = 'Filtered Incoming Network Traffic'
    _NET_OUT_METRIC_NAME = 'Filtered Outgoing Network Traffic'
    # Disk metrics are supported from Hyper-V 2012 R2
    _DISK_RD_METRIC_NAME = 'Disk Data Read'
    _DISK_WR_METRIC_NAME = 'Disk Data Written'
    _DISK_LATENCY_METRIC_NAME = 'Average Disk Latency'
    _DISK_IOPS_METRIC_NAME = 'Average Normalized Disk Throughput'

    def __init__(self, host='.'):
        if sys.platform == 'win32':
            self._init_hyperv_wmi_conn(host)
            self._init_cimv2_wmi_conn(host)
        self._host_cpu_info = None

    def _init_hyperv_wmi_conn(self, host):
        self._conn = wmi.WMI(moniker='//%s/root/virtualization/v2' % host)

    def _init_cimv2_wmi_conn(self, host):
        self._conn_cimv2 = wmi.WMI(moniker='//%s/root/cimv2' % host)

    def get_host_cpu_info(self):
        if not self._host_cpu_info:
            host_cpus = self._conn_cimv2.Win32_Processor()
            self._host_cpu_info = (host_cpus[0].MaxClockSpeed, len(host_cpus))
        return self._host_cpu_info

    def get_all_vms(self):
        vms = [(v.ElementName, v.Name) for v in
               self._conn.Msvm_ComputerSystem(['ElementName', 'Name'],
                                              Caption="Virtual Machine")]
        return vms

    def get_cpu_metrics(self, vm_name):
        vm = self._lookup_vm(vm_name)
        cpu_sd = self._get_vm_resources(vm, self._PROC_SETTING)[0]
        cpu_metrics_def = self._get_metric_def(self._CPU_METRIC_NAME)
        cpu_metric_aggr = self._get_metrics(vm, cpu_metrics_def)

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

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

    def get_memory_metrics(self, vm_name):
        vm = self._lookup_vm(vm_name)
        memory_def = self._get_metric_def(self._MEMORY_METRIC_NAME)
        metric_memory = self._get_metrics(vm, memory_def)
        memory_usage = 0
        if metric_memory:
            memory_usage = int(metric_memory[0].MetricValue)
        return memory_usage

    def get_vnic_metrics(self, vm_name):
        vm = self._lookup_vm(vm_name)
        ports = self._get_vm_resources(vm, self._ETH_PORT_ALLOC)
        vnics = self._get_vm_resources(vm, self._SYNTH_ETH_PORT)

        metric_def_in = self._get_metric_def(self._NET_IN_METRIC_NAME)
        metric_def_out = self._get_metric_def(self._NET_OUT_METRIC_NAME)

        for port in ports:
            vnic = [v for v in vnics if port.Parent == v.path_()][0]

            metric_value_instances = self._get_metric_value_instances(
                port.associators(wmi_result_class=self._PORT_ACL_SET_DATA),
                self._BASE_METRICS_VALUE)
            metric_values = self._sum_metric_values_by_defs(
                metric_value_instances, [metric_def_in, metric_def_out])

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

    def get_disk_metrics(self, vm_name):
        vm = self._lookup_vm(vm_name)
        metric_def_r = self._get_metric_def(self._DISK_RD_METRIC_NAME)
        metric_def_w = self._get_metric_def(self._DISK_WR_METRIC_NAME)

        disks = self._get_vm_resources(vm, self._STORAGE_ALLOC)
        for disk in disks:
            metric_values = self._get_metric_values(
                disk, [metric_def_r, metric_def_w])

            # Thi sis e.g. the VHD file location
            if disk.HostResource:
                host_resource = disk.HostResource[0]

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

    def get_disk_latency_metrics(self, vm_name):
        vm = self._lookup_vm(vm_name)
        metric_latency_def = self._get_metric_def(
            self._DISK_LATENCY_METRIC_NAME)

        disks = self._get_vm_resources(vm, self._STORAGE_ALLOC)
        for disk in disks:
            metric_values = self._get_metric_values(
                disk, [metric_latency_def])

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

    def get_disk_iops_count(self, vm_name):
        vm = self._lookup_vm(vm_name)
        metric_def_iops = self._get_metric_def(self._DISK_IOPS_METRIC_NAME)
        disks = self._get_vm_resources(vm, self._STORAGE_ALLOC)

        for disk in disks:
            metric_values = self._get_metric_values(
                disk, [metric_def_iops])

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

    @staticmethod
    def _sum_metric_values(metrics):
        tot_metric_val = 0
        for metric in metrics:
            tot_metric_val += int(metric.MetricValue)
        return tot_metric_val

    def _sum_metric_values_by_defs(self, element_metrics, metric_defs):
        metric_values = []
        for metric_def in metric_defs:
            if metric_def:
                metrics = self._filter_metrics(element_metrics, metric_def)
                metric_values.append(self._sum_metric_values(metrics))
            else:
                # In case the metric is not defined on this host
                metric_values.append(0)
        return metric_values

    @staticmethod
    def _get_metric_value_instances(elements, result_class):
        instances = []
        for el in elements:
            associators = el.associators(wmi_result_class=result_class)
            if associators:
                instances.append(associators[0])

        return instances

    def _get_metric_values(self, element, metric_defs):
        element_metrics = element.associators(
            wmi_association_class=self._METRICS_ME)
        return self._sum_metric_values_by_defs(element_metrics, metric_defs)

    def _lookup_vm(self, vm_name):
        vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name)
        n = len(vms)
        if n == 0:
            raise inspector.InstanceNotFoundException(
                'VM %s not found on Hyper-V' % vm_name)
        elif n > 1:
            raise HyperVException('Duplicate VM name found: %s' % vm_name)
        else:
            return vms[0]

    def _get_metrics(self, element, metric_def):
        return self._filter_metrics(
            element.associators(
                wmi_association_class=self._METRICS_ME), metric_def)

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

    def _get_metric_def(self, metric_def):
        metric = self._conn.CIM_BaseMetricDefinition(ElementName=metric_def)
        if metric:
            return metric[0]

    def _get_vm_setting_data(self, vm):
        vm_settings = vm.associators(
            wmi_result_class=self._VS_SETTING_DATA)
        # Avoid snapshots
        return [s for s in vm_settings if
                s.VirtualSystemType == self._VIRTUAL_SYSTEM_TYPE_REALIZED][0]

    def _get_vm_resources(self, vm, resource_class):
        setting_data = self._get_vm_setting_data(vm)
        return setting_data.associators(wmi_result_class=resource_class)
