#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import sys

import lib.args
import lib.base
import lib.db_mysql
import lib.human
import lib.librenms
import lib.time
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN

__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026050801'

DESCRIPTION = """Retrieves hardware sensor information (temperature, humidity, voltage, power, etc.)
for each device from a LibreNMS instance. Alerts when sensor values exceed their
configured thresholds. Requires direct access to the LibreNMS MySQL/MariaDB
database.
Supports extended reporting via --lengthy."""

DEFAULT_DEFAULTS_FILE = '/var/spool/icinga2/.my.cnf'
DEFAULT_DEFAULTS_GROUP = 'client'
DEFAULT_INSECURE = False
DEFAULT_LENGTHY = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 3
DEFAULT_URL = 'http://localhost'


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--defaults-file',
        help=lib.args.help('--defaults-file') + ' '
        'Example: `/var/spool/icinga2/.my.cnf`. '
        'Default: %(default)s',
        dest='DEFAULTS_FILE',
        default=DEFAULT_DEFAULTS_FILE,
    )

    parser.add_argument(
        '--defaults-group',
        help=lib.args.help('--defaults-group') + ' Default: %(default)s',
        dest='DEFAULTS_GROUP',
        default=DEFAULT_DEFAULTS_GROUP,
    )

    parser.add_argument(
        '--device-group',
        help='Filter by LibreNMS device group. Supports SQL wildcards.',
        dest='DEVICE_GROUP',
    )

    parser.add_argument(
        '--device-hostname',
        help='Filter by LibreNMS hostname. Can be specified multiple times.',
        dest='DEVICE_HOSTNAME',
        action='append',
    )

    parser.add_argument(
        '--device-type',
        help='Filter by LibreNMS device type. Can be specified multiple times.',
        dest='DEVICE_TYPE',
        action='append',
        choices=[  # taken from the librenms source file resources/definitions/config_definitions.json
            'appliance',
            'collaboration',
            'environment',
            'firewall',
            'loadbalancer',
            'management',
            'network',
            'power',
            'printer',
            'server',
            'storage',
            'wireless',
            'workstation',
        ],
    )

    parser.add_argument(
        '--lengthy',
        help=lib.args.help('--lengthy'),
        dest='LENGTHY',
        action='store_true',
        default=DEFAULT_LENGTHY,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    mysql_connection = {
        'defaults_file': args.DEFAULTS_FILE,
        'defaults_group': args.DEFAULTS_GROUP,
        'timeout': args.TIMEOUT,
    }
    conn = lib.base.coe(lib.db_mysql.connect(mysql_connection))
    lib.base.coe(lib.db_mysql.check_privileges(conn))

    # init some vars
    state = STATE_OK
    perfdata = ''
    alert_count = 0
    sensors_count = 0

    # fetch data
    sql = """
        SELECT
            d.hostname,
            d.sysName,
            d.sysDescr,
            d.type,
            l.location,
            s.sensor_descr,
            s.sensor_current,
            s.sensor_limit,
            s.sensor_limit_low,
            s.lastupdate,
            s.sensor_class,
            st.state_descr,
            st.state_generic_value
        FROM
            devices AS d
                LEFT JOIN
            locations AS l ON d.location_id = l.id
                LEFT JOIN
            device_group_device AS dgd ON d.device_id = dgd.device_id
                LEFT JOIN
            device_groups AS dg ON dgd.device_group_id = dg.id
                LEFT JOIN
            sensors AS s ON d.device_id = s.device_id
                LEFT JOIN
            sensors_to_state_indexes AS stsi ON s.sensor_id = stsi.sensor_id
                LEFT JOIN
            state_translations AS st ON stsi.state_index_id = st.state_index_id
                AND st.state_value = s.sensor_current
        WHERE
            disable_notify = 0
            AND s.sensor_alert = 1
    """
    data = []
    if args.DEVICE_GROUP:
        sql += ' AND dg.name LIKE %s '
        data.append(args.DEVICE_GROUP)
    if args.DEVICE_HOSTNAME:
        sql += f' AND d.hostname IN ({", ".join("%s" for _ in args.DEVICE_HOSTNAME)}) '
        data += args.DEVICE_HOSTNAME
    if args.DEVICE_TYPE:
        sql += f' AND d.type IN ({", ".join("%s" for _ in args.DEVICE_TYPE)}) '
        data += args.DEVICE_TYPE
    sql += ' ORDER BY d.hostname, s.sensor_class'
    sensors = lib.base.coe(lib.db_mysql.select(conn, sql, data))
    lib.db_mysql.close(conn)
    sensors_count = len(sensors)

    # enrich and analyse data
    for i, sensor in enumerate(sensors):
        if not sensor['sysName']:
            sensors[i]['sysName'] = sensor['sysDescr']
        if sensor['sensor_class'] == 'state':
            sensors[i]['sensor_current'] = sensor['state_descr']
        if all([sensor['sensor_limit_low'], sensor['sensor_limit']]):
            sensor['sensor_current'] = (
                f'{sensor["sensor_current"]} ({sensor["sensor_limit_low"]}..{sensor["sensor_limit"]})'
            )
        if sensor['lastupdate']:
            delta = lib.time.now(as_type='datetime') - sensor['lastupdate']
            sensor['lastupdate'] = lib.human.seconds2human(delta.total_seconds())
        local_state = sensor[
            'state_generic_value'
        ]  # 0 = ok, 1 = warn, 2 = crit, 3 = unknown
        if not local_state:  # including "None"
            local_state = STATE_OK
        if local_state != STATE_OK:
            alert_count += 1
        sensors[i]['state'] = lib.base.state2str(
            local_state,
            empty_ok=False,
        )
        state = lib.base.get_worst(local_state, state)

    # filter data if compact layout is choosen (just get everything that is not ok)
    if not args.LENGTHY:
        # brief data
        sensors = [sensor for sensor in sensors if sensor['state'] != '[OK]']

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok. '
    else:
        msg = (
            f'There {lib.txt.pluralize("", alert_count, "is,are")} '
            f'{alert_count} {lib.txt.pluralize("alert", alert_count)}. '
        )
    msg += f'Checked {sensors_count} {lib.txt.pluralize("sensor", sensors_count)}.'
    msg += '\n\n'

    perfdata += lib.base.get_perfdata(
        'sensor_count',
        sensors_count,
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'alert_count',
        alert_count,
        _min=0,
    )

    if sensors_count > 0:
        if not args.LENGTHY:
            msg += lib.base.get_table(
                sensors,
                [
                    'hostname',
                    'sysName',
                    'sensor_descr',
                    'sensor_current',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Sensor',
                    'Val (Range)',
                    'State',
                ],
            )
        else:
            msg += lib.base.get_table(
                sensors,
                [
                    'hostname',
                    'sysName',
                    'type',
                    'location',
                    'sensor_descr',
                    'sensor_class',
                    'lastupdate',
                    'sensor_current',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Type',
                    'Location',
                    'Sensor',
                    'Class',
                    'Changed',
                    'Val (Range)',
                    'State',
                ],
            )

    # over and out
    lib.base.oao(msg, state, perfdata, always_ok=args.ALWAYS_OK)


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
