#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.

# Author: Lars Michelsen <lm@mathias-kettner.de>

# Relevant SNMP OIDs:
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.144 9
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.258 16
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.400 25
#
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.3974704 "6cos"
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.6208592 "ingress-map"
#
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.1593 "class-default"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.18785 "EF"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284945 "AF1"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284961 "AF2"
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284977 "AF3"

# http://www.oidview.com/mibs/9/CISCO-CLASS-BASED-QOS-MIB.html

# TEST:
#
# search class table:
# .1.3.6.1.4.1.9.9.166.1.7.1.1.1.284945 (cbQosCMName) "AF1"
# class_id = 284945 (cbQosConfigIndex)
#
# search config table for matching value
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.144.5256 284945
# key = 144.5256 (cbQosPolicyIndex: 144, cbQosObjectsIndex: 5256)
#
# search if table for matchin if_id: 144
# .1.3.6.1.4.1.9.9.166.1.1.1.1.4.144 (cbQosIfIndex) 9
# if_policy = 9 (ifIndex -> standard mib)
#
# get config_id from config table using if_id.if_id 144.144
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.144.144 (cbQosConfigIndex) 6208592
# config_index = 6208592
#
# get policy name using the policy_index
# .1.3.6.1.4.1.9.9.166.1.6.1.1.1.6208592 "ingress-map"
# policy_name = "ingress-map"
#
# get post bytes using the key
# .1.3.6.1.4.1.9.9.166.1.15.1.1.9.144.5256 0
# post_bytes = 0
#
# get dropped bytes using the key
# .1.3.6.1.4.1.9.9.166.1.15.1.1.16.144.5256 0
# dropped_bytes = 0
#
# get if_name using the if_policy: 9
# .1.3.6.1.2.1.31.1.1.1.1.9 Vl1
# if_name = Vl1
#
# get if_speed using the if_policy: 9
# .1.3.6.1.2.1.2.2.1.5.9 100000000
# if_speed = 100000000
#
###
# Test to find the badwidth of the classes. Not finished...
#
# 'cbQosObjectsType' => {
#         1 => 'policymap',
#         2 => 'classmap',
#         3 => 'matchStatement',
#         4 => 'queueing',
#         5 => 'randomDetect',
#         6 => 'trafficShaping',
#         7 => 'police',
#         8 => 'set' },
#
# Index:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.258.1244739 1608
#
# Type:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.3.258.1244739 4
#
# Parent ID:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.4.258.1244739 6184
#
# cbQosQueueingStatsEntry:
# .1.3.6.1.4.1.9.9.166.1.18.1.1.2.258.1244739 64
# ...

# Index:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.2.258.6184 18785
# Type:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.3.258.6184 2
# Parent ID:
# .1.3.6.1.4.1.9.9.166.1.5.1.1.4.258.6184 258

# get cbQosQueueingCfgBandwidth
# .1.3.6.1.4.1.9.9.166.1.9.1.1.1.1608 3094

factory_settings["cisco_qos_default_levels"] = {"drop": (0.01, 0.01)}


def cisco_qos_get_config_entries_by_class_id(config, class_id):
    return [if_index.split(".") for if_index, value in config.items() if value == class_id]


def inventory_cisco_qos(info):
    if len(info) == 12:
        ifs = dict(info[0])
        config = dict([(".".join(oid.split(".")[-2:]), value) for oid, value in info[3]])
        if_names = dict(info[6])

        # Find all interfaces for each class and create one service for each pair
        items = []
        for class_id, class_name in info[2]:
            # Get interface ids which use this qos class
            for policy_id, _objects_id in cisco_qos_get_config_entries_by_class_id(
                config, class_id
            ):
                if ifs.get(policy_id) in if_names:
                    if_name = if_names[ifs[policy_id]]
                    items += [("%s: %s" % (if_name, class_name), {})]

        return items


def check_cisco_qos(item, params, info):
    unit = params.get("unit", "bit")
    average = params.get("average")
    post_warn, post_crit = params.get("post", (None, None))
    drop_warn, drop_crit = params.get("drop", (None, None))

    # Load values and format them
    ifs = dict(info[0])
    policies = dict(info[1])
    classes = dict(info[2])
    config = dict([(".".join(oid.split(".")[-2:]), value) for oid, value in info[3]])
    post_bytes = dict([(".".join(oid.split(".")[-2:]), value) for oid, value in info[4]])
    drop_bytes = dict([(".".join(oid.split(".")[-2:]), value) for oid, value in info[5]])
    if_names = dict(info[6])
    if_speeds = dict(info[7])
    parents = dict(info[8])
    if_qos_bandwidth = dict(info[9])
    if_qos_bandwidth_units = dict(info[10])
    object_types = dict(info[11])

    if_name, class_name = item.split(": ")

    # Gather the class id by class_name
    class_id = None
    for cid, cname in classes.items():
        if class_name == cname:
            class_id = cid
            break

    # Gather the interface id by class_name
    if_id = None
    for iid2 in ifs.values():
        if if_name == if_names.get(iid2):
            if_id = iid2
            break

    if not if_id or not class_id:
        return (3, "QoS class not found for that interface")

    policy_id, objects_id, policy_map_id, policy_name = None, None, None, None
    for this_policy_id, this_objects_id in cisco_qos_get_config_entries_by_class_id(
        config, class_id
    ):
        if if_id != ifs[this_policy_id]:
            continue  # skip the ones of other interfaces

        # Get the policy_map_id. To retrieve this get one of the config entries
        # of type "policy map" from the config table. All of this type should have
        # the same value, which is then the policy_map_id.
        for key in object_types:
            if key.startswith(this_policy_id + ".") and object_types[key] == "1":
                policy_map_id = config[key]
                break

        if policy_map_id is None:
            return 3, "Invalid policy map id"

        policy_name = policies.get(policy_map_id)
        policy_id = this_policy_id
        objects_id = this_objects_id

    if policy_id is None or objects_id is None:
        return 3, "Could not find policy_id or objects_id"

    post_b = post_bytes.get(policy_id + "." + objects_id, 0)
    drop_b = drop_bytes.get(policy_id + "." + objects_id, 0)
    speed = saveint(if_speeds[if_id])

    parent_value_cache = {a_value: a_key.split(".")[1] for a_key, a_value in config.items()}

    # if a_value == class_id:
    #     parent_value = a_key.split(".")[1]
    for b_key, b_value in parents.items():
        if parent_value_cache[class_id] == b_value:
            if object_types[b_key] == "4":
                try:
                    # 1 kbps
                    # 2 percentage
                    # 3 percentageRemaining
                    # 4 ratioRemaining
                    # 5 perThousand
                    # 6 perMillion
                    qos_unit = int(if_qos_bandwidth_units[config[b_key]])
                    qos_bandwidth = saveint(if_qos_bandwidth[config[b_key]])
                    if qos_unit == 1:
                        speed = qos_bandwidth * 1000

                    elif qos_unit == 2:
                        speed = speed * (qos_bandwidth / 100.0)

                    elif qos_unit == 3:
                        remaining = speed * (qos_bandwidth / 100.0)
                        speed = speed - remaining
                    break
                except KeyError:
                    pass

    # Bandwidth needs to be in bytes for later calculations
    bw = speed / 8.0

    # Determine post warn/crit levels
    if isinstance(post_warn, float):
        post_warn = bw / 100.0 * post_warn
        post_crit = bw / 100.0 * post_crit
    elif isinstance(post_warn, int):
        if unit == "bit":
            post_warn = post_warn // 8
            post_crit = post_crit // 8

    # Determine drop warn/crit levels
    if isinstance(drop_warn, float):
        drop_warn = bw / 100.0 * drop_warn
        drop_crit = bw / 100.0 * drop_crit
    # Convert the drop levels to byte
    elif unit == "bit":
        if isinstance(drop_warn, int):
            drop_warn = drop_warn / 8.0
        if isinstance(drop_crit, int):
            drop_crit = drop_crit / 8.0

    # Handle counter values
    state = 0
    infotext = ""
    this_time = time.time()
    rates = []
    perfdata = []
    perfdata_avg = []

    for name, counter, warn, crit, min_val, max_val in [
        ("post", post_b, post_warn, post_crit, 0.0, bw),
        ("drop", drop_b, drop_warn, drop_crit, 0.0, bw),
    ]:
        rate = get_rate("cisco_qos.%s.%s" % (name, item), this_time, saveint(counter))
        rates.append(rate)
        perfdata.append((name, rate, warn, crit, min_val, max_val))

        if average:
            avg_value = get_average("cisco_qos.%s.%s.avg" % (name, item), this_time, rate, average)
            rates.append(avg_value)
            perfdata_avg.append(
                ("%s_avg_%d" % (name, average), avg_value, warn, crit, min_val, max_val)
            )

    perfdata.extend(perfdata_avg)

    def format_value(value):
        if unit == "bit":
            value = value * 8
            return get_nic_speed_human_readable(value)
        return "%s/s" % get_bytes_human_readable(value)

    if average:
        post_rate = rates[1]
        drop_rate = rates[3]
    else:
        post_rate = rates[0]
        drop_rate = rates[1]

    for what, rate, warn, crit in [
        ("post", post_rate, post_warn, post_crit),
        ("drop", drop_rate, drop_warn, drop_crit),
    ]:
        infotext += ", %s: %s" % (what, format_value(rate))
        if crit is not None and rate >= crit and bw:
            state = max(2, state)
            infotext += "(!!)"
        elif warn is not None and rate >= warn and bw:
            state = max(1, state)
            infotext += "(!)"

    if policy_name:
        infotext += ", Policy-Name: %s, Int-Bandwidth: %s" % (policy_name, format_value(bw))
    else:
        infotext += ", Policy-Map-ID: %s, Int-Bandwidth: %s" % (policy_map_id, format_value(bw))
    return (state, infotext.lstrip(", "), perfdata)


check_info["cisco_qos"] = {
    "service_description": "QoS %s",
    "check_function": check_cisco_qos,
    "inventory_function": inventory_cisco_qos,
    "has_perfdata": True,
    "group": "cisco_qos",
    "default_levels_variable": "cisco_qos_default_levels",
}

snmp_info["cisco_qos"] = [
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "1.1.1.4"]),  # qosIfIndex
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "6.1.1.1"]),  # qosPolicies
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "7.1.1.1"]),  # qosClasses
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "5.1.1.2"]),  # qosConfig
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "15.1.1.9"]),  # qosPostBytes
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "15.1.1.16"]),  # qosDropBytes
    (".1.3.6.1.2.1.2.2.1", [OID_END, "2"]),  # ifNames
    (".1.3.6.1.2.1.2.2.1", [OID_END, "5"]),  # ifSpeeds
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "5.1.1.4"]),  # cbQosParentObjectsIndex
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "9.1.1.1"]),  # qosQueueingConfigBandwidth
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "9.1.1.2"]),  # cbQosQueueingCfgBandwidthUnits
    (".1.3.6.1.4.1.9.9.166.1", [OID_END, "5.1.1.3"]),  # cbQosObjectsType
]
snmp_scan_functions["cisco_qos"] = lambda oid: "cisco" in oid(".1.3.6.1.2.1.1.1.0").lower() and oid(
    ".1.3.6.1.4.1.9.9.166.1.1.1.1.4.*"
)
