#!/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.human
import lib.keycloak
import lib.lftest
from lib.globals import STATE_OK, STATE_UNKNOWN

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

DESCRIPTION = """Monitors Java heap and non-heap memory usage of a Keycloak server via its HTTP API.
Alerts when memory usage exceeds the configured thresholds, and if the server does not
report memory information to the account the check authenticates with, which Keycloak
grants only to an account holding the "manage-realm" role in its administration realm.
Tested with Keycloak 17 and later."""

DEFAULT_CLIENT_ID = 'admin-cli'
DEFAULT_CRIT = 90
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
# Keycloak's install-time default password, not a real secret.
DEFAULT_PASSWORD = 'admin'  # nosec B105
DEFAULT_REALM = 'master'
DEFAULT_TIMEOUT = 8
DEFAULT_URL = 'http://127.0.0.1:8080'
DEFAULT_USERNAME = 'admin'
DEFAULT_WARN = 80


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    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(
        '--client-id',
        help='Keycloak API Client-ID. Default: %(default)s',
        dest='CLIENT_ID',
        default=DEFAULT_CLIENT_ID,
    )

    parser.add_argument(
        '--critical',
        help=lib.args.help('--critical') + ' Default: >= %(default)s',
        dest='CRIT',
        default=DEFAULT_CRIT,
    )

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

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '-p',
        '--password',
        help='Keycloak API password. Default: %(default)s',
        dest='PASSWORD',
        default=DEFAULT_PASSWORD,
    )

    parser.add_argument(
        '--proxy',
        help=lib.args.help('--proxy'),
        dest='PROXY',
        default=None,
    )

    parser.add_argument(
        '--realm',
        help='Keycloak API realm. Default: %(default)s',
        dest='REALM',
        default=DEFAULT_REALM,
    )

    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='Keycloak API URL. Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
    )

    parser.add_argument(
        '--username',
        help='Keycloak API username. Default: %(default)s',
        dest='USERNAME',
        default=DEFAULT_USERNAME,
    )

    parser.add_argument(
        '--warning',
        help=lib.args.help('--warning') + ' Default: >= %(default)s',
        dest='WARN',
        default=DEFAULT_WARN,
    )

    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)

    # fetch data
    if args.TEST is None:
        # Discover the OIDC endpoints for the realm (no authentication needed),
        # obtain an admin access token and call the Admin REST API (fetch the
        # realm's details).
        oidc_config = lib.base.coe(lib.keycloak.discover_oidc_endpoints(args))
        admin_token = lib.base.coe(lib.keycloak.obtain_admin_token(args, oidc_config))
        server_info = lib.base.coe(
            lib.keycloak.get_data(args, admin_token, '/admin/serverinfo')
        )
    else:
        # do not call the API, put in test data
        server_info = lib.lftest.test_json(args.TEST)

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''

    # analyze data
    memory_info = lib.base.coe(
        lib.keycloak.get_server_info_section(server_info, 'memoryInfo')
    )
    mem_free = memory_info.get('free', 0)
    mem_total = memory_info.get('total', 0)
    mem_used = memory_info.get('used', 0)
    free_percentage = memory_info.get('freePercentage')
    if free_percentage is None or not mem_total:
        # A default would have to invent the very number the thresholds are compared
        # against, so say that the figures are missing instead of alerting on a guess.
        lib.base.cu(
            'Keycloak reports memory information without usable heap figures, '
            'so there is nothing to compare against the thresholds.'
        )
    used_percent = 100 - free_percentage

    # build the message
    state = lib.base.get_state(used_percent, args.WARN, args.CRIT, _operator='ge')
    msg += f'{used_percent}%{lib.base.state2str(state, prefix=" ")} - '
    msg += f'total: {lib.human.bytes2human(mem_total)}, '
    msg += f'used: {lib.human.bytes2human(mem_used)}, '
    msg += f'free: {lib.human.bytes2human(mem_free)}'

    perfdata += lib.base.get_perfdata(
        'usage_percent',
        used_percent,
        uom='%',
        warn=args.WARN,
        crit=args.CRIT,
        _min=0,
        _max=100,
    )
    perfdata += lib.base.get_perfdata(
        'total',
        mem_total,
        uom='B',
        warn=None,
        crit=None,
        _min=0,
        _max=mem_total,
    )
    perfdata += lib.base.get_perfdata(
        'used',
        mem_used,
        uom='B',
        warn=None,
        crit=None,
        _min=0,
        _max=mem_total,
    )
    perfdata += lib.base.get_perfdata(
        'free',
        mem_free,
        uom='B',
        warn=None,
        crit=None,
        _min=0,
        _max=mem_total,
    )

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


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