#!/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 json
import sys

import lib.args
import lib.base
import lib.lftest
import lib.time
import lib.url
from lib.globals import (STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN)


__author__ = """
Linuxfabrik GmbH, Zurich/Switzerland;
originally written by Dominik Riva, Universitätsspital Basel/Switzerland
"""
__version__ = '2025091201'

DESCRIPTION = """
This is a monitoring plugin for the Spring Boot Actuator `/health` endpoint (e.g.
http://localhost:port/actuator/health/db). It supports fine-grained overrides to adjust alerting
behaviour and applying Nagios-style threshold ranges to detailed numeric metrics.
"""

DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT  = 3
DEFAULT_URL = 'http://localhost:80/health'
DEFAULT_VERBOSE = False

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='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--component-severity',
        dest='COMPONENT_SEVERITIES',
        type=lib.args.csv,
        metavar='COMPONENT_NAME,API_STATUS,NAGIOS_STATE',
        action='append',
        help='Set the API status for a specific component like `UP`, `DEGRADED` and `DOWN` to a '
             'Nagios state, where Nagios state is one of `ok`, `warn`, `crit` or `unknown` '
             '(repeating). '
             'Format: `component-name,api-status,nagios-state`. '
             'Example: `hikariConnectionPool,DEGRADED,crit`',
    )

    parser.add_argument(
        '--detail-severity',
        dest='DETAIL_SEVERITIES',
        type=lib.args.csv,
        metavar='COMPONENT_NAME,DETAIL_NAME,WARN,CRIT',
        action='append',
        help='Set a threshold for a *numeric* component detail value '
             '(repeating). '
             'Supports Nagios ranges. '
             'Format: `component-name,detail-name,warn,crit`. '
             'Example: `hikariConnectionPool,activeConnections,@10:20,@0:9`',
    )

    parser.add_argument(
        '--insecure',
        help='This option explicitly allows to perform "insecure" SSL connections. '
             'Default: %(default)s',
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    # legacy parameter - just use `--verbose` instead
    parser.add_argument(
        '--record-json',
        help=argparse.SUPPRESS,
        dest='RECORD_JSON',
        metavar='FILE',
    )

    parser.add_argument(
        '--test',
        help='For unit tests. Needs "path-to-stdout-file,path-to-stderr-file,expected-retc".',
        dest='TEST',
        type=lib.args.csv,
    )

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

    parser.add_argument(
        '--url',
        help='Spring Boot Actuator Health Endpoint, for example http://server:80/health/diskSpace. '
             'Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
    )

    parser.add_argument(
        '--verbose',
        help=lib.args.help('--verbose') + ' ' +
             'Default: %(default)s',
        dest='VERBOSITY',
        action='store_true',
        default=DEFAULT_VERBOSE,
    )

    return parser.parse_args()


def api_status2nagios_state(api_status):
    """Return Nagios state for the Spring API status:
    * UP/GREEN: STATE_OK
    * DEGRADED/YELLOW: STATE_WARN
    * STATE_CRIT else
    """
    api_status = api_status.upper()
    if api_status in ('UP', 'GREEN'):
        return STATE_OK
    if api_status in ('DEGRADED', 'YELLOW'):
        return STATE_WARN
    return STATE_CRIT


def main():
    """The main function. Hier spielt die Musik.
    """

    # parse the command line, exit with UNKNOWN if it fails
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    cs_idx, ds_idx = {}, {}
    for row in args.COMPONENT_SEVERITIES or []:
        if len(row) != 3:
            lib.base.cu(f'--component-severity: Invalid value "{row}"')
        # for looking up many components, we build an index (O(1) lookups)
        cs_idx[row[0]] = row
    for row in args.DETAIL_SEVERITIES or []:
        if len(row) != 4:
            lib.base.cu(f'--detail-severity: Invalid value "{row}"')
        ds_idx[row[0]] = row

    # fetch data
    if args.TEST is None:
        if not args.URL.startswith('http'):
            lib.base.cu('--url parameter has to start with "http://" or https://".')
        result = lib.base.coe(lib.url.fetch_json(
            args.URL,
            header={'Accept': 'application/json'},
            insecure=args.INSECURE,
            no_proxy=args.NO_PROXY,
            timeout=args.TIMEOUT,
            extended=True,
        ))
    else:
        # do not call the command, put in test data
        stdout, _, _ = lib.lftest.test(args.TEST)
        result = json.loads(stdout)
    if args.VERBOSITY:
        print(result, 'Type:', type(result))

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    table_components, table_details = [], []

    # analyze data
    # https://docs.spring.io/spring-boot/api/rest/actuator/health.html
    if result.get('status_code', 200) >= 300:
        # HTTP response code is 200 for server status UP and 503 for statuses DEGRADED and DOWN
        state = STATE_WARN
    if result.get('response_json', {}).get('status', '') == 'DOWN':
        state = STATE_CRIT

    # loop over all components and check for state overrides
    for component, info in result.get('response_json', {}).get('components', {}).items():
        # overall status of the component
        component_status = info.get('status', 'UP')
        severity = cs_idx.get(component, [])
        if severity and severity[1] == component_status:
            # user wants to override the status
            local_state = lib.base.str2state(severity[2])
        else:
            local_state = api_status2nagios_state(component_status)
        state = lib.base.get_worst(state, local_state)
        table_components.append({
            'component': component,
            'detail': f'{component_status} > {severity[2]}' \
                      if severity and severity[1] == component_status else component_status,
            'state': lib.base.state2str(local_state, prefix='', empty_ok=False),
        })
        perfdata += lib.base.get_perfdata(
            component,
            local_state,
            uom=None,
            warn=None,
            crit=None,
            _min=0,
            _max=3,
        )

        # check the status of the details of the component
        for k, v in info.get('details', {}).items():
            severity = ds_idx.get(component, [])  # lookup e.g. "diskSpace"
            # check if we have the correct detail, e.g. "free"
            if severity and severity[1] != k:
                severity = []
            if severity and isinstance(v, (int, float)):
                # user wants to override the status
                local_state = lib.base.get_state(v, severity[2], severity[3], 'range')
            else:
                local_state = STATE_OK
            state = lib.base.get_worst(state, local_state)
            table_details.append({
                'component': component,
                'detail': k,
                'value': f'{v} > ' \
                         f'{severity[2] if severity[2] != '' else None},' \
                         f'{severity[3] if severity[3] != '' else None}'
                         if severity else v,
                'state': lib.base.state2str(local_state, prefix='', empty_ok=False),
            })
            if isinstance(v, (int, float)) and not isinstance(v, bool):
                perfdata += lib.base.get_perfdata(
                    f'{component}_{k}',
                    v,
                    uom='B' if component.startswith('disk') else None,
                    warn=None,
                    crit=None,
                    _min=None,
                    _max=None,
                )

    # build the message
    msg += 'Overall status of the application: ' \
           f'{result.get("response_json", {}).get("status", "N/A")}, '
    msg += f'API Status Code: {result.get("status_code", 0)}'
    msg += f'{lib.base.state2str(state, prefix=" ")}'

    if table_components:
        msg = msg.strip()
        msg += '\n\n' + lib.base.get_table(
            table_components,
            ['component', 'detail', 'state'],
            ['Component', 'Status', 'State'],
        )
    if table_details:
        msg = msg.strip()
        msg += '\n\n' + lib.base.get_table(
            table_details,
            ['component', 'detail', 'value', 'state'],
            ['Component', 'Detail', 'value', 'State'],
        )

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


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