#!/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.jolokia import *  # pylint: disable=wildcard-import,unused-wildcard-import

# 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.uptime import *  # pylint: disable=wildcard-import,unused-wildcard-import

# Example output from agent:
# <<<jolokia_metrics>>>
# 8080 NonHeapMemoryUsage 101078952
# 8080 NonHeapMemoryMax 184549376
# 8080 HeapMemoryUsage 2362781664
# 8080 HeapMemoryMax 9544663040
# 8080 ThreadCount 78
# 8080 DeamonThreadCount 72
# 8080 PeakThreadCount 191
# 8080 TotalStartedThreadCount 941
# 8080 Uptime 572011375
# 8080,java.lang:name=PS_MarkSweep,type=GarbageCollector CollectionCount 0

# Number of threads warn, crit
jolokia_metrics_threads_default_levels = (80, 100)

# Number of sessions low crit, low warn, high warn, high crit
jolokia_metrics_app_sess_default_levels = (-1, -1, 800, 1000)

# Number of requests low crit, low warn, high warn, high crit
jolokia_metrics_serv_req_default_levels = (-1, -1, 5000, 6000)

jolokia_metrics_queue_default_levels = (20, 50)

# .
#   .--Arcane helpers------------------------------------------------------.
#   |                     _                                                |
#   |                    / \   _ __ ___ __ _ _ __   ___                    |
#   |                   / _ \ | '__/ __/ _` | '_ \ / _ \                   |
#   |                  / ___ \| | | (_| (_| | | | |  __/                   |
#   |                 /_/   \_\_|  \___\__,_|_| |_|\___|                   |
#   |                                                                      |
#   |                  _          _                                        |
#   |                 | |__   ___| |_ __   ___ _ __ ___                    |
#   |                 | '_ \ / _ \ | '_ \ / _ \ '__/ __|                   |
#   |                 | | | |  __/ | |_) |  __/ |  \__ \                   |
#   |                 |_| |_|\___|_| .__/ \___|_|  |___/                   |
#   |                              |_|                                     |
#   +----------------------------------------------------------------------+
#   | TODO: See if these can be removed altogether                         |
#   '----------------------------------------------------------------------'


# This bisects the app server and its values
def jolokia_metrics_app(info, split_item):
    inst, app = split_item
    parsed = jolokia_metrics_parse(info)
    if parsed.get(inst, "") is None:
        raise MKCounterWrapped("No information from Jolokia agent")
    if not inst in parsed or not app in parsed[inst].get("apps", {}):
        return None
    return parsed[inst]["apps"][app]


# This bisects info from BEA and passes on to jolokia_metrics_app
def jolokia_metrics_serv(info, split_item):
    inst, app, serv = split_item
    app = jolokia_metrics_app(info, (inst, app))
    if not app or not serv in app.get("servlets", {}):
        return None
    return app["servlets"][serv]


# .
#   .--Number of Requests--------------------------------------------------.
#   |               ____                            _                      |
#   |              |  _ \ ___  __ _ _   _  ___  ___| |_ ___                |
#   |              | |_) / _ \/ _` | | | |/ _ \/ __| __/ __|               |
#   |              |  _ <  __/ (_| | |_| |  __/\__ \ |_\__ \               |
#   |              |_| \_\___|\__, |\__,_|\___||___/\__|___/               |
#   |                            |_|                                       |
#   '----------------------------------------------------------------------'


def inventory_jolokia_metrics_serv(info):
    inv = []
    parsed = jolokia_metrics_parse(info)
    levels = None
    levels = "jolokia_metrics_serv_req_default_levels"
    needed_key = "Requests"
    for inst, vals in parsed.items():
        if vals is None:
            continue  # no data from agent
        for app, val in vals.get("apps", {}).items():
            for serv, servinfo in val.get("servlets", {}).items():
                if needed_key in servinfo:
                    inv.append(("%s %s %s" % (inst, app, serv), levels))
    return inv


def check_jolokia_metrics_serv_req(item, params, info):
    lo_crit, lo_warn, hi_warn, hi_crit = params
    serv = jolokia_metrics_serv(info, item.split())
    if not serv or not "Requests" in serv:
        return (3, "data not found in agent output")
    req = saveint(serv["Requests"])

    status = 0
    status_txt = ""
    if lo_crit is not None and req <= lo_crit:
        status = 2
        status_txt = " (Below or equal %d)" % lo_crit
    elif lo_warn is not None and req <= lo_warn:
        status = 1
        status_txt = " (Below or equal %d)" % lo_warn
    elif hi_crit is not None and req >= hi_crit:
        status = 2
        status_txt = " (Above or equal %d)" % hi_crit
    elif hi_warn is not None and req >= hi_warn:
        status = 1
        status_txt = " (Above or equal %d)" % hi_warn

    output = ["Requests: %d%s" % (req, status_txt)]
    perfdata = [("Requests", req, hi_warn, hi_crit)]
    wrapped = False
    this_time = time.time()
    try:
        rate = get_rate("jolokia_metrics.serv_req.%s" % item, this_time, req)
        output.append("RequestRate: %0.2f" % rate)
        perfdata.append(("RequestRate", rate))
    except MKCounterWrapped:
        wrapped = True

    if wrapped:
        return (status, ", ".join(output))
    return (status, ", ".join(output), perfdata)


check_info["jolokia_metrics.serv_req"] = {
    "service_description": "JVM %s Requests",
    "check_function": check_jolokia_metrics_serv_req,
    "inventory_function": inventory_jolokia_metrics_serv,
    "group": "jvm_requests",
    "has_perfdata": True,
}

# .
#   .--App state-----------------------------------------------------------.
#   |                _                      _        _                     |
#   |               / \   _ __  _ __    ___| |_ __ _| |_ ___               |
#   |              / _ \ | '_ \| '_ \  / __| __/ _` | __/ _ \              |
#   |             / ___ \| |_) | |_) | \__ \ || (_| | ||  __/              |
#   |            /_/   \_\ .__/| .__/  |___/\__\__,_|\__\___|              |
#   |                    |_|   |_|                                         |
#   '----------------------------------------------------------------------'


def check_jolokia_metrics_app_state(item, _unused, info):
    app_state = 3
    app = jolokia_metrics_app(info, item.split())

    # FIXME: this could be nicer.
    if app and "Running" in app:
        if app["Running"] == "1":
            app_state = 0
        else:
            app_state = 2
    # wenn in app statename steht
    elif app and "stateName" in app:
        if app["stateName"] == "STARTED":
            app_state = 0
        else:
            app_state = 2
    if app_state == 3:
        return (3, "data not found in agent output")
    elif app_state == 0:
        return (0, "application is running")
    elif app_state == 2:
        return (2, "application is not running (Running: %s)")

    return (3, "error in agent output")


check_info["jolokia_metrics.app_state"] = {
    "service_description": "JVM %s State",
    "check_function": check_jolokia_metrics_app_state,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "app_state"),
    "has_perfdata": False,
}

# .
#   .--Unsorted------------------------------------------------------------.
#   |              _   _                      _           _                |
#   |             | | | |_ __  ___  ___  _ __| |_ ___  __| |               |
#   |             | | | | '_ \/ __|/ _ \| '__| __/ _ \/ _` |               |
#   |             | |_| | | | \__ \ (_) | |  | ||  __/ (_| |               |
#   |              \___/|_| |_|___/\___/|_|   \__\___|\__,_|               |
#   |                                                                      |
#   '----------------------------------------------------------------------'


def check_jolokia_metrics_app_sess(item, params, info):
    lo_crit, lo_warn, hi_warn, hi_crit = params
    if len(item.split()) == 3:
        app = jolokia_metrics_serv(info, item.split())
    elif len(item.split()) == 2:
        app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")
    sessions = app.get("Sessions", app.get("activeSessions", app.get("OpenSessionsCurrentCount")))
    if sessions is None:
        return (3, "data not found in agent output")
    sess = saveint(sessions)
    maxActive = saveint(
        app.get("Sessions", app.get("maxActiveSessions", app.get("OpenSessionsCurrentCount")))
    )

    status = 0
    status_txt = ""
    if lo_crit is not None and sess <= lo_crit:
        status = 2
        status_txt = " (Below or equal %d)" % lo_crit
    elif lo_warn is not None and sess <= lo_warn:
        status = 1
        status_txt = " (Below or equal %d)" % lo_warn
    elif hi_crit is not None and sess >= hi_crit:
        status = 2
        status_txt = " (Above or equal %d)" % hi_crit
    elif hi_warn is not None and sess >= hi_warn:
        status = 1
        status_txt = " (Above or equal %d)" % hi_warn

    if maxActive and maxActive > 0:
        status_txt += " (max active sessions: %d)" % maxActive

    return (status, "%d Sessions%s" % (sess, status_txt), [("sessions", sess, hi_warn, hi_crit)])


def check_jolokia_metrics_bea_queue(item, params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")
    if "QueueLength" not in app:
        return (3, "data not found in agent output")
    queuelength = int(app["QueueLength"])

    status = 0
    warn, crit = params
    if queuelength >= crit:
        status = 2
    elif queuelength >= warn:
        status = 1
    return (status, "queue length is %d" % queuelength, [("length", queuelength, warn, crit)])


# FIXME: This check could work with any JVM
# It has no levels
# A candidate for 1.2.1 overhaul
def check_jolokia_metrics_bea_requests(item, _no_params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")

    for nk in ["CompletedRequestCount", "requestCount"]:
        if nk in app:
            requests = int(app[nk])
            rate = get_rate("j4p.bea.requests.%s" % item, time.time(), requests)
            return (0, "%.2f requests/sec" % rate, [("rate", rate)])

    return (3, "data not found in agent output")


def check_jolokia_metrics_bea_threads(item, _no_params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "data not found in agent output")

    perfdata = []
    infos = []
    for varname, title in [
        ("ExecuteThreadTotalCount", "total"),
        ("ExecuteThreadIdleCount", "idle"),
        ("StandbyThreadCount", "standby"),
        ("HoggingThreadCount", "hogging"),
    ]:
        value = int(app[varname])
        perfdata.append((varname, value))
        infos.append("%s: %d" % (title, value))

    return (0, ", ".join(infos), perfdata)


check_info["jolokia_metrics.app_sess"] = {
    "service_description": "JVM %s Sessions",
    "check_function": check_jolokia_metrics_app_sess,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "app_sess"),
    "group": "jvm_sessions",
    "has_perfdata": True,
}

check_info["jolokia_metrics.requests"] = {
    "service_description": "JVM %s Requests",
    "check_function": check_jolokia_metrics_bea_requests,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "requests"),
    "group": "jvm_requests",
    "has_perfdata": True,
}

# Stuff found on BEA Weblogic
check_info["jolokia_metrics.bea_queue"] = {
    "service_description": "JVM %s Queue",
    "check_function": check_jolokia_metrics_bea_queue,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "queue"),
    "group": "jvm_queue",
    "has_perfdata": True,
}

check_info["jolokia_metrics.bea_requests"] = {
    "service_description": "JVM %s Requests",
    "check_function": check_jolokia_metrics_bea_requests,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "bea_requests"),
    "group": "jvm_requests",
    "has_perfdata": True,
}

check_info["jolokia_metrics.bea_threads"] = {
    "service_description": "JVM %s Threads",
    "check_function": check_jolokia_metrics_bea_threads,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "threads"),
    "group": "jvm_threads",
    "has_perfdata": True,
}

check_info["jolokia_metrics.bea_sess"] = {
    "service_description": "JVM %s Sessions",
    "check_function": check_jolokia_metrics_app_sess,
    "inventory_function": lambda i: inventory_jolokia_metrics_apps(i, "bea_app_sess"),
    "group": "jvm_sessions",
    "has_perfdata": True,
}

jolokia_metrics_cache_hits_default_levels = {}


def inventory_jolokia_metrics_cache(key, metrics, info):
    parsed = jolokia_metrics_parse(info)
    metrics_set = set(metrics)
    for inst, vals in [x for x in parsed.items() if x[1] is not None]:
        for cache, cache_vars in vals.get("CacheStatistics", {}).items():
            if metrics_set.intersection(cache_vars) == metrics_set:
                if key is None:
                    yield "%s %s" % (inst, cache), None
                else:
                    yield "%s %s" % (inst, cache), "jolokia_metrics_%s_default_levels" % key


def check_jolokia_metrics_cache(metrics, totals, item, params, info):
    type_map = {
        "CacheHitPercentage": (float, 100.0, "%.1f%%"),
        "InMemoryHitPercentage": (float, 100.0, "%.1f%%"),
        "OnDiskHitPercentage": (float, 100.0, "%.1f%%"),
        "OffHeapHitPercentage": (float, 100.0, "%.1f%%"),
    }

    parsed = jolokia_metrics_parse(info)
    try:
        inst, cache = item.split(" ")

        # we display the "metrics" first, totals after, but to "fix" metrics based on zero-totals
        # we need to go over the totals once
        for total in totals:
            val = int(parsed[inst]["CacheStatistics"][cache][total])
            if val != 0:
                break

        for metric in metrics:
            type_, scale, format_str = type_map.get(metric, (int, 1, "%d"))

            val = type_(parsed[inst]["CacheStatistics"][cache][metric]) * scale
            if isinstance(val, float) and val == 0.0:
                # what a hack! we assume the float is based on the totals (all of them) and if they
                # were all 0, so this float is 0/0, we want to display it as 1 as to not cause
                # an alert
                val = 1.0 * scale
            yield 0, ("%s: " + format_str) % (metric, val), [(metric, val)]

        for total in totals:
            type_, scale, format_str = type_map.get(total, (int, 1, "%d"))
            val = type_(parsed[inst]["CacheStatistics"][cache][total]) * scale
            yield 0, ("%s: " + format_str) % (total, val), []
    except KeyError:
        # some element of the item was missing
        pass


check_info["jolokia_metrics.cache_hits"] = {
    "service_description": "JVM %s Cache Usage",
    "check_function": lambda item, params, parsed: check_jolokia_metrics_cache(
        ["CacheHitPercentage", "ObjectCount"], ["CacheHits", "CacheMisses"], item, params, parsed
    ),
    "inventory_function": lambda info: inventory_jolokia_metrics_cache(
        "cache_hits", ["CacheHitPercentage", "ObjectCount", "CacheHits", "CacheMisses"], info
    ),
    "has_perfdata": True,
}

check_info["jolokia_metrics.in_memory"] = {
    "service_description": "JVM %s In Memory",
    "check_function": lambda item, params, parsed: check_jolokia_metrics_cache(
        ["InMemoryHitPercentage", "MemoryStoreObjectCount"],
        ["InMemoryHits", "InMemoryMisses"],
        item,
        params,
        parsed,
    ),
    "inventory_function": lambda info: inventory_jolokia_metrics_cache(
        "cache_hits",
        ["InMemoryHitPercentage", "MemoryStoreObjectCount", "InMemoryHits", "InMemoryMisses"],
        info,
    ),
    "has_perfdata": True,
}

check_info["jolokia_metrics.on_disk"] = {
    "service_description": "JVM %s On Disk",
    "check_function": lambda item, params, parsed: check_jolokia_metrics_cache(
        ["OnDiskHitPercentage", "DiskStoreObjectCount"],
        ["OnDiskHits", "OnDiskMisses"],
        item,
        params,
        parsed,
    ),
    "inventory_function": lambda info: inventory_jolokia_metrics_cache(
        "cache_hits",
        ["OnDiskHitPercentage", "DiskStoreObjectCount", "OnDiskHits", "OnDiskMisses"],
        info,
    ),
    "has_perfdata": True,
}

check_info["jolokia_metrics.off_heap"] = {
    "service_description": "JVM %s Off Heap",
    "check_function": lambda item, params, parsed: check_jolokia_metrics_cache(
        ["OffHeapHitPercentage", "OffHeapStoreObjectCount"],
        ["OffHeapHits", "OffHeapMisses"],
        item,
        params,
        parsed,
    ),
    "inventory_function": lambda info: inventory_jolokia_metrics_cache(
        "cache_hits",
        ["OffHeapHitPercentage", "OffHeapStoreObjectCount", "OffHeapHits", "OffHeapMisses"],
        info,
    ),
    "has_perfdata": True,
}

check_info["jolokia_metrics.writer"] = {
    "service_description": "JVM %s Cache Writer",
    "check_function": lambda item, params, parsed: check_jolokia_metrics_cache(
        ["WriterQueueLength", "WriterMaxQueueSize"], [], item, params, parsed
    ),
    "inventory_function": lambda info: inventory_jolokia_metrics_cache(
        "cache_hits", ["WriterQueueLength", "WriterMaxQueueSize"], info
    ),
    "has_perfdata": True,
}

# .
#   .--Deprecated Checks----------------------------------------.
#   |     ____                                _           _     |
#   |    |  _ \  ___ _ __  _ __ ___  ___ __ _| |_ ___  __| |    |
#   |    | | | |/ _ \ '_ \| '__/ _ \/ __/ _` | __/ _ \/ _` |    |
#   |    | |_| |  __/ |_) | | |  __/ (_| (_| | ||  __/ (_| |    |
#   |    |____/ \___| .__/|_|  \___|\___\__,_|\__\___|\__,_|    |
#   |               |_|                                         |
#   +-----------------------------------------------------------+
#   | All subchecks below this point are depricated at least    |
#   | since version 1.7                                         |
#   |                                                           |
#   | Since this file and the plugin have become an utter mess, |
#   | we move the 'subchecks' (which are semantically in no way |
#   | subchecks of 'metric') to their own plugins. This process |
#   | has started with the jolokia_jvm_threading plugin in 1.6  |
#   | and will hopefully be completed once checkmk 1.7 is       |
#   | released.                                                 |
#   '-----------------------------------------------------------+


def check_deprecated(_no_item, _no_params, _no_info):
    return 1, "This check is deprecated. Please rediscover the services of this host."


factory_settings["jolokia_metrics_tp_default_levels"] = {}

check_info["jolokia_metrics.tp"] = {
    "service_description": "JVM %s",
    "check_function": check_deprecated,
    "inventory_function": lambda i: None,
}

check_info["jolokia_metrics.threads"] = {
    "service_description": "JVM %s Threads",
    "check_function": check_deprecated,
    "inventory_function": lambda i: None,
}

check_info["jolokia_metrics.mem"] = {
    "service_description": "JVM %s Memory",
    "check_function": check_deprecated,
    "inventory_function": lambda i: None,
}

# This variable needs to stay here s.t. the autocheck files for jolokia_metrics.perm_gen can still
# be loaded, since the actual discovery function had
# yield ..., "jolokia_metrics_perm_gen_default_levels"
jolokia_metrics_perm_gen_default_levels = {"perm": (80.0, 100.0)}

check_info["jolokia_metrics.perm_gen"] = {
    "service_description": "JVM %s PermGen Usage",
    "check_function": check_deprecated,
    "inventory_function": lambda i: None,
}


def discover_jolokia_metrics_uptime(info):
    """DEPRECATED.

    This service has been replaced by the
    'jolokia_jvm_uptime' plugin in checkmk version 2.0.0
    """
    parsed = jolokia_metrics_parse(info)
    for instance, data in parsed.items():
        if data and "Uptime" in data:
            yield instance, None


def check_jolokia_metrics_uptime(item, params, info):
    parsed = jolokia_metrics_parse(info)
    if parsed.get(item, "") is None:
        raise MKCounterWrapped("No information from Jolokia agent")
    if item in parsed:
        if "Uptime" in parsed[item]:
            uptime = int(int(parsed[item]["Uptime"]) / 1000)
            return check_uptime_seconds(params, uptime)


check_info["jolokia_metrics.uptime"] = {
    "service_description": "JVM %s Uptime",
    "check_function": check_jolokia_metrics_uptime,
    "inventory_function": discover_jolokia_metrics_uptime,
    "group": "jvm_uptime",
    "has_perfdata": True,
}


def inventory_jolokia_metrics_gc(info):
    """DEPRECATED.

    This service has been replaced by the
    'jolokia_jvm_uptime' plugin in checkmk version 2.0.0
    """
    inv = []
    parsed = jolokia_metrics_parse(info)
    for inst, vals in parsed.items():
        if vals is None:
            continue  # no data from agent
        for gc in vals.get("gc", {}):
            inv.append(("%s GC %s" % (inst, gc), {}))
    return inv


def jolokia_metrics_gc(info, split_item):
    inst, _typ, gc = split_item
    parsed = jolokia_metrics_parse(info)
    if parsed.get(inst, "") is None:
        raise MKCounterWrapped("No information from Jolokia agent")

    if not inst in parsed or not gc in parsed[inst].get("gc", {}):
        return None
    return parsed[inst]["gc"][gc]


def _transform_invers(params):
    """Even if this is deprecated, make sure it still works should the user update the rule"""
    if params is None:
        return {}
    new_params = params.copy()
    if "collection_time" in params:
        perc = params["collection_time"]
        new_params["CollectionTime"] = (perc[0] * 600.0, perc[1] * 600.0)
    if "collection_count" in params:
        per_sec = params["collection_count"]
        new_params["CollectionCount"] = (per_sec[0] * 60.0, per_sec[1] * 60.0)
    return new_params


def check_jolokia_metrics_gc(item, params, info):
    gc = jolokia_metrics_gc(info, item.split())
    if gc is None:
        return

    params = _transform_invers(params)

    crate = get_rate("jvm.gc.count.%s" % (item), time.time(), int(gc["CollectionCount"]))
    crate = crate * 60.0

    ctext = ""
    status = 0
    cwarn, ccrit = params.get("CollectionCount", (None, None))
    if cwarn is not None and ccrit is not None:
        if crate >= int(ccrit):
            status = 2
            ctext = " (Level %s) " % ccrit
        elif crate >= int(cwarn):
            status = 1
            ctext = " (Level %s) " % cwarn

    yield status, "%.2f GC Count/minute%s" % (crate, ctext), [
        ("CollectionCount", crate, cwarn, ccrit)
    ]

    if "CollectionTime" in gc:
        twarn, tcrit = params.get("CollectionTime", (None, None))
        trate = get_rate("jvm.gc.time.%s" % (item), time.time(), int(gc["CollectionTime"]))
        trate = trate * 60.0

        ttext = ""
        status = 0
        if twarn is not None and tcrit is not None:
            if trate >= int(tcrit):
                status = 2
                ttext = "(Level %s) " % tcrit
            elif trate >= int(twarn):
                status = 1
                ttext = "(Level %s) " % twarn

        yield status, "%.2f GC ms/minute%s" % (trate, ttext), [
            ("CollectionTime", trate, twarn, tcrit)
        ]


check_info["jolokia_metrics.gc"] = {
    "service_description": "JVM %s",
    "check_function": check_jolokia_metrics_gc,
    "inventory_function": inventory_jolokia_metrics_gc,
    "group": "jvm_gc",
    "has_perfdata": True,
}
