#!/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.

# NOTE: Careful when replacing the *-import below with a more specific import. This can cause
# problems because it might remove variables from the check-context which are necessary for
# resolving legacy discovery results such as [("SUMMARY", "diskstat_default_levels")]. Furthermore,
# it might also remove variables needed for accessing discovery rulesets.
from cmk.base.check_legacy_includes.brocade import *  # pylint: disable=wildcard-import,unused-wildcard-import
# lookup tables for check implementation
# Taken from swFCPortPhyState
brocade_fcport_phystates = {
    0: '',
    1: 'no card',
    2: 'no transceiver',
    3: 'laser fault',
    4: 'no light',
    5: 'no sync',
    6: 'in sync',
    7: 'port fault',
    8: 'diag fault',
    9: 'lock ref',
    10: 'validating',
    11: 'invalid module',
    12: 'no sig det',
    13: 'unknown',
}

# Taken from swFCPortOpStatus
brocade_fcport_opstates = {
    0: 'unknown',
    1: 'online',
    2: 'offline',
    3: 'testing',
    4: 'faulty',
}

# Taken from swFCPortAdmStatus
brocade_fcport_admstates = {
    0: '',
    1: 'online',
    2: 'offline',
    3: 'testing',
    4: 'faulty',
}

# Taken from swFCPortSpeed
brocade_fcport_speed = {
    0: 'unknown',
    1: '1Gbit',
    2: '2Gbit',
    3: 'auto-Neg',
    4: '4Gbit',
    5: '8Gbit',
    6: '10Gbit',
    7: 'unknown',
    8: '16Gbit',
}

# Taken from swNbBaudRate
isl_speed_map = {
    "1": 0,  # other (1) - None of the following.
    "2": 0.155,  # oneEighth (2) - 155 Mbaud.
    "4": 0.266,  # quarter (4) - 266 Mbaud.
    "8": 0.532,  # half (8) - 532 Mbaud.
    "16": 1,  # full (16) - 1 Gbaud.
    "32": 2,  # double (32) - 2 Gbaud.
    "64": 4,  # quadruple (64) - 4 Gbaud.
    "128": 8,  # octuple (128) - 8 Gbaud.
    "256": 10,  # decuple (256) - 10 Gbaud.
    "512": 16,  # sexdecuple (512) - 16 Gbaud
}

factory_settings["brocade_fcport_default_levels"] = {
    "rxcrcs": (3.0, 20.0),  # allowed percentage of CRC errors
    "rxencoutframes": (3.0, 20.0),  # allowed percentage of Enc-OUT Frames
    "rxencinframes": (3.0, 20.0),  # allowed percentage of Enc-In Frames
    "notxcredits": (3.0, 20.0),  # allowed percentage of No Tx Credits
    "c3discards": (3.0, 20.0),  # allowed percentage of C3 discards
    "assumed_speed": 2.0,  # used if speed not available in SNMP data
}


def _try_int(int_str):
    try:
        return int(int_str)
    except ValueError:
        return None


def _to_int(raw_value):
    """Convert a raw integer

    This is done by considering the string to be a little endian byte string.
    Such strings are sometimes used by SNMP to encode 64 bit counters without
    needed COUNTER64 (which is not available in SNMP v1).
    """
    value = 0
    mult = 1
    for ord_int in raw_value[::-1]:
        value += mult * ord_int
        mult *= 256
    return value


def _get_if_table_offset(speed_info, offset):
    # http://community.brocade.com/t5/Fibre-Channel-SAN/SNMP-FC-port-speed/td-p/64980
    # says that "1073741824" from if-table correlates with index 1 from
    # brocade-if-table.
    # But: In logically separated Brocade FC switches, the interface indices do
    # not start with 1. Therefor the index in the speed table does not start
    # with "1073741824". It is necessary to add the first interface index as
    # offset to find the table offset in the speed table.
    for index, entry in enumerate(speed_info):
        if int(entry[0]) == 1073741823 + offset:
            return index


def parse_brocade_fcport(info):
    if_info, link_info, speed_info, if64_info, ifname_info = info
    isl_ports = dict(link_info)

    if len(if_info) == 0:
        return

    try:
        offset = int(if_info[0][0])
    except ValueError:
        return

#    print(if64_info)
#    print(speed_info)
#    print(link_info)
#    print(if_info)
#    print(ifname_info)

    # if-table and brocade-if-table do NOT have same length
    speed_info = [x[1:] for x in speed_info[_get_if_table_offset(speed_info, offset):]]
    ifname_info = [z[1:] for z in ifname_info[_get_if_table_offset(ifname_info, offset):]]
    
#    print(speed_info)
#    print(ifname_info)
#    print(len(speed_info))
#    print(len(if_info))

    if len(if_info) == len(speed_info)-2:
        # extract the speed from IF-MIB::ifHighSpeed.
        # unfortunately ports in the IF-MIB and the brocade MIB
        # dont have a common index. We hope that at least
        # the FC ports have the same sequence in both lists.
        # here we go through ports of the IF-NIB, but consider only FC ports (type 56)
        # and assume that the sequence number of the FC port here is the same
        # as the sequence number in the borcade MIB (pindex = item_index)
        if_table = [x + (y + z if y[0] == "56" else ["", x[-2]]) for x, y, z in zip(if_info, speed_info, ifname_info)]
    else:
        if_table = [x + ["", x[-2]] for x in if_info]
    
#    print(if_table)

    parsed = []
    for index, phystate, opstate, admstate, txwords, rxwords, txframes, \
        rxframes, notxcredits, rxcrcs, rxencinframes, rxencoutframes, \
        c3discards, brocade_speed, portname, porttype, ifspeed, ifname in if_table:
#        print(index) 
#        print(range(len(ifname_info)))
#        for myindex in range(len(ifname_info)):
#            if int(index) == int(myindex):
#               print(index,portname,ifname_info[myindex-1],ifname_info[int(index)])
#               ifname = ifname_info[myindex-1]
        
        # Since FW v8.0.1b [rx/tx]words are no longer available
        # Use 64bit counters if available
        bbcredits = None
        if if64_info:
            fcmgmt_portstats = []
            for oidend, tx_objects, rx_objects, tx_elements, rx_elements, bbcredits_64 in if64_info:
                if index == oidend.split(".")[-1]:
                    fcmgmt_portstats = [
                        _to_int(tx_objects),
                        _to_int(rx_objects),
                        int(_to_int(tx_elements) / 4),
                        int(_to_int(rx_elements) / 4),
                        _to_int(bbcredits_64),
                    ]
                    break
            if fcmgmt_portstats:
                txframes = fcmgmt_portstats[0]
                rxframes = fcmgmt_portstats[1]
                txwords = fcmgmt_portstats[2]
                rxwords = fcmgmt_portstats[3]
                bbcredits = fcmgmt_portstats[4]
            else:
                txframes = 0
                rxframes = 0
                txwords = 0
                rxwords = 0

        islspeed = None
        if index in isl_ports:
            islspeed = isl_speed_map.get(isl_ports[index])

        try:
            data = {
                "index": int(index),
                "phystate": int(phystate),
                "opstate": int(opstate),
                "admstate": int(admstate),
                "txwords": int(txwords),
                "rxwords": int(rxwords),
                "txframes": int(txframes),
                "rxframes": int(rxframes),
                "notxcredits": int(notxcredits),
                "rxcrcs": int(rxcrcs),
                "rxencinframes": int(rxencinframes),
                "rxencoutframes": int(rxencoutframes),
                "c3discards": int(c3discards),
                "brocade_speed": _try_int(brocade_speed),
                "portname": portname,
                "porttype": porttype,
                "ifspeed": _try_int(ifspeed),
                "ifname": ifname,
                "is_isl": index in isl_ports,
                "islspeed": islspeed,  # Might be None
                "bbcredits": bbcredits,  # Might be None
            }
        except ValueError:
            continue

        parsed.append(data)

    return parsed


def inventory_brocade_fcport(parsed):
    settings = host_extra_conf_merged(host_name(), brocade_fcport_inventory)
    print(parsed)    
    inventory = []
    for if_entry in parsed:
        admstate = if_entry["admstate"]
        phystate = if_entry["phystate"]
        opstate = if_entry["opstate"]
        ifname = if_entry["ifname"]
        portname = if_entry["portname"]
        index = if_entry["index"]
        is_isl=if_entry["is_isl"]
        ifname = if_entry["ifname"]

#        print(index,is_isl,ifname,portname,settings)

        if brocade_fcport_inventory_this_port(
                admstate=admstate,
                phystate=phystate,
                opstate=opstate,
                settings=settings,
        ):
            inventory.append((
                brocade_fcport_getitem(number_of_ports=len(parsed),
                                       index=if_entry["index"],
                                       portname=if_entry["ifname"] + " " + if_entry["portname"],
                                       is_isl=if_entry["is_isl"],
                                       settings=settings),
                '{ "phystate": [%d], "opstate": [%d], "admstate": [%d] }' %
                (phystate, opstate, admstate),
            ))
    print(inventory)
    return inventory


def _get_speed_msg_and_value(is_isl, isl_speed, brocade_speed, porttype, if_speed, params):
    # Lookup port speed in ISL table for ISL ports (older switches do not provide this
    # information in the normal table)
    if is_isl and isl_speed is not None:
        return "ISL speed: %.0f Gbit/s", isl_speed

    brocade_speed_value = brocade_fcport_speed.get(brocade_speed, "unknown")
    if brocade_speed_value not in ("auto-Neg", "unknown") and porttype != "56":
        return "%.0f Gbit/s", float(brocade_speed_value.replace("Gbit", ""))

    if if_speed:
        # use actual speed of port if available
        return "Speed: %g Gbit/s", if_speed / 1000.0

    # let user specify assumed speed via check parameter, default is 2.0
    return "Assumed speed: %g Gbit/s", params["assumed_speed"]


def check_brocade_fcport(item, params, parsed):
    found_entry = None
    for if_entry in parsed:
        if int(item.split()[0]) + 1 == if_entry["index"]:
            found_entry = if_entry
            break
#    print(found_entry)
    if found_entry is None:
        return 3, "No SNMP data found"

    index = found_entry["index"]
    txwords = found_entry["txwords"]
    rxwords = found_entry["rxwords"]
    txframes = found_entry["txframes"]
    rxframes = found_entry["rxframes"]
    notxcredits = found_entry["notxcredits"]
    rxcrcs = found_entry["rxcrcs"]
    rxencinframes = found_entry["rxencinframes"]
    rxencoutframes = found_entry["rxencoutframes"]
    c3discards = found_entry["c3discards"]
    brocade_speed = found_entry["brocade_speed"]
    is_isl = found_entry["is_isl"]
    isl_speed = found_entry["islspeed"]
    bbcredits = found_entry["bbcredits"]
    porttype = found_entry["porttype"]
    speed = found_entry.get("ifspeed")
    descr = found_entry["ifname"]

    this_time = time.time()
    average = params.get("average")  # range in minutes
    bw_thresh = params.get("bw")

    summarystate = 0
    output = []
    perfdata = []
    perfaverages = []

    speedmsg, gbit = _get_speed_msg_and_value(is_isl, isl_speed, brocade_speed, porttype, speed,
                                              params)
    output.append("Port: %s" % descr)
    output.append(speedmsg % gbit)

    # convert gbit netto link-rate to Byte/s (8/10 enc) -> 10 bits per byte
    wirespeed = gbit * 1e8
    in_bytes = 4 * get_rate("brocade_fcport.rxwords.%s" % index, this_time, rxwords)
    out_bytes = 4 * get_rate("brocade_fcport.txwords.%s" % index, this_time, txwords)

    # B A N D W I D T H
    # convert thresholds in percentage into MB/s
    if bw_thresh is None:  # no levels
        warn_bytes, crit_bytes = None, None
    else:
        warn, crit = bw_thresh
        if isinstance(warn, float):
            warn_bytes = wirespeed * warn / 100.0
        else:  # in MB
            warn_bytes = warn * 1048576.0
        if isinstance(crit, float):
            crit_bytes = wirespeed * crit / 100.0
        else:  # in MB
            crit_bytes = crit * 1048576.0

    for what, value in [("In", in_bytes), ("Out", out_bytes)]:
        output.append("%s: %s/s" % (what, get_bytes_human_readable(value)))
        perfdata.append((what.lower(), value, warn_bytes, crit_bytes, 0, wirespeed))
        # average turned on: use averaged traffic values instead of current ones
        if average:
            value = get_average("brocade_fcport.%s.%s.avg" % (what, item), this_time, value,
                                average)
            output.append("Average (%d min): %s/s" % (average, get_bytes_human_readable(value)))
            perfaverages.append(
                ("%s_avg" % what.lower(), value, warn_bytes, crit_bytes, 0, wirespeed))

        # handle levels for in/out
        if crit_bytes is not None and value >= crit_bytes:
            summarystate = 2
            output.append(" >= %s/s(!!)" % (get_bytes_human_readable(crit_bytes)))
        elif warn_bytes is not None and value >= warn_bytes:
            summarystate = max(1, summarystate)
            output.append(" >= %s/s(!)" % (get_bytes_human_readable(warn_bytes)))

    # put perfdata of averages after perfdata for in and out in order not to confuse the perfometer
    perfdata.extend(perfaverages)

    # R X F R A M E S & T X F R A M E S
    # Put number of frames into performance data (honor averaging)
    rxframes_rate = get_rate("brocade_fcport.rxframes.%s" % index, this_time, rxframes)
    txframes_rate = get_rate("brocade_fcport.txframes.%s" % index, this_time, txframes)
    for what, value in [("rxframes", rxframes_rate), ("txframes", txframes_rate)]:
        perfdata.append((what, value))
        if average:
            value = get_average("brocade_fcport.%s.%s.avg" % (what, item), this_time, value,
                                average)
            perfdata.append(("%s_avg" % what, value))

    # E R R O R C O U N T E R S
    # handle levels on error counters
    for descr, counter, value, ref in [
        ("CRC errors", "rxcrcs", rxcrcs, rxframes_rate),
        ("ENC-Out", "rxencoutframes", rxencoutframes, rxframes_rate),
        ("ENC-In", "rxencinframes", rxencinframes, rxframes_rate),
        ("C3 discards", "c3discards", c3discards, txframes_rate),
        ("No TX buffer credits", "notxcredits", notxcredits, txframes_rate),
    ]:
        per_sec = get_rate("brocade_fcport.%s.%s" % (counter, index), this_time, value)
        perfdata.append((counter, per_sec))

        # if averaging is on, compute average and apply levels to average
        if average:
            per_sec_avg = get_average("brocade_fcport.%s.%s.avg" % \
                    (counter, item), this_time, per_sec, average)
            perfdata.append(("%s_avg" % counter, per_sec_avg))

        # compute error rate (errors in relation to number of frames) (from 0.0 to 1.0)
        if ref > 0 or per_sec > 0:
            rate = per_sec / (ref + per_sec)  # fixed: true-division
        else:
            rate = 0
        text = "%s: %.2f%%" % (descr, rate * 100.0)

        # Honor averaging of error rate
        if average:
            rate = get_average("brocade_fcport.%s.%s.avgrate" % (counter, item), this_time, rate,
                               average)
            text += ", Average: %.2f%%" % (rate * 100.0)

        error_percentage = rate * 100.0
        warn, crit = params.get(counter, (None, None))
        if crit is not None and error_percentage >= crit:
            summarystate = 2
            text += "(!!)"
            output.append(text)
        elif warn is not None and error_percentage >= warn:
            summarystate = max(1, summarystate)
            text += "(!)"
            output.append(text)

    # P O R T S T A T E
    for state_key, state_info, warn_states, state_map in [
        ("phystate", "Physical", (1, 6), brocade_fcport_phystates),
        ("opstate", "Operational", (1, 3), brocade_fcport_opstates),
        ("admstate", "Administrative", (0, 1, 3), brocade_fcport_admstates),
    ]:
        dev_state = found_entry[state_key]
        errorflag = ""
        state_value = params.get(state_key)
        if (state_value is not None and dev_state != state_value and
                not (isinstance(state_value, list) and dev_state in map(int, state_value))):
            if dev_state in warn_states:
                errorflag = "(!)"
                summarystate = max(summarystate, 1)
            else:
                errorflag = "(!!)"
                summarystate = 2
        output.append("%s: %s%s" % (state_info, state_map[dev_state], errorflag))

    if bbcredits is not None:
        bbcredit_rate = get_rate("brocade_fcport.bbcredit.%s" % (item), this_time, bbcredits)
        perfdata.append(("fc_bbcredit_zero", bbcredit_rate))

    return (summarystate, ', '.join(output), perfdata)


check_info["brocade_fcport"] = {
    'parse_function': parse_brocade_fcport,
    'check_function': check_brocade_fcport,
    'inventory_function': inventory_brocade_fcport,
    'service_description': '%s',
    'has_perfdata': True,
    'snmp_info': [
        (
            ".1.3.6.1.4.1.1588.2.1.1.1.6.2.1",
            [
                1,  # swFCPortIndex
                3,  # swFCPortPhyState
                4,  # swFCPortOpStatus
                5,  # swFCPortAdmStatus
                11,  # swFCPortTxWords
                12,  # swFCPortRxWords
                13,  # swFCPortTxFrames
                14,  # swFCPortRxFrames
                20,  # swFCPortNoTxCredits
                22,  # swFCPortRxCrcs
                21,  # swFCPortRxEncInFrs
                26,  # swFCPortRxEncOutFrs
                28,  # swFCPortC3Discards
                35,  # swFCPortSpeed, deprecated from at least firmware version 7.2.1
                36,  # swFCPortName  (not supported by all devices)
            ]),

        # Information about Inter-Switch-Links (contains baud rate of port)
        (
            ".1.3.6.1.4.1.1588.2.1.1.1.2.9.1",
            [
                2,  # swNbMyPort
                5,  # swNbBaudRate
            ]),
        # new way to get port speed supported by Brocade
        (
            ".1.3.6.1.2.1",
            [
                OID_END,
                "2.2.1.3",  # ifType, needed to extract fibre channel ifs only (type 56)
                "31.1.1.1.15",  # IF-MIB::ifHighSpeed
            ]),
        # Not every device supports that
        (
            ".1.3.6.1.3.94.4.5.1",
            [
                OID_END,
                BINARY("4"),  # FCMGMT-MIB::connUnitPortStatCountTxObjects
                BINARY("5"),  # FCMGMT-MIB::connUnitPortStatCountRxObjects
                BINARY("6"),  # FCMGMT-MIB::connUnitPortStatCountTxElements
                BINARY("7"),  # FCMGMT-MIB::connUnitPortStatCountRxElements
                BINARY("8"),  # FCMGMT-MIB::connUnitPortStatCountBBCreditZero
            ]),
        # ifName
        (
            ".1.3.6.1.2.1",
            [
                OID_END,
                "31.1.1.1.1",  # IF-MIB::ifName
            ]),
    ],
    'snmp_scan_function': lambda oid: (oid(".1.3.6.1.2.1.1.2.0").startswith(
        ".1.3.6.1.4.1.1588.2.1.1") and oid(".1.3.6.1.4.1.1588.2.1.1.1.6.2.1.*") is not None),
    'group': 'brocade_fcport',
    'default_levels_variable': 'brocade_fcport_default_levels',
}
