#!/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__ = '2025103001'

DESCRIPTION = """Monitors a Spring Boot application via its Actuator /health endpoint. Checks overall
health status and individual component states (database, disk, mail, etc.). Supports
fine-grained severity overrides per component and sub-component.
Alerts when the application or any component reports an unhealthy state."""

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=lib.args.help('--always-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='Map an API status (`UP`, `DEGRADED`, `DOWN`) for a specific component to a Nagios state (`ok`, `warn`, `crit`, `unknown`). '
        'Can be specified multiple times. '
        'Format: `component-name,api-status,nagios-state`. '
        'Example: `--component-severity 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='Threshold for a numeric component detail value. '
        ''
        'Can be specified multiple times. '
        'Format: `component-name,detail-name,warn,crit`. '
        'Example: `--detail-severity hikariConnectionPool,activeConnections,@10:20,@0:9`.',
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        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=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

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

    parser.add_argument(
        '--url',
        help='Spring Boot Actuator health endpoint URL. '
        'Example: `--url 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='VERBOSE',
        action='store_true',
        default=DEFAULT_VERBOSE,
    )

    args, _ = parser.parse_known_args()
    return 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. This is where the magic happens."""

    # parse the command line
    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.VERBOSE:
        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:
        lib.base.cu()
