#!/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  # pylint: disable=C0413
import re  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.lftest  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_OK, STATE_UNKNOWN)  # pylint: disable=C0413

try:
    import psutil  # pylint: disable=C0413
except ImportError:
    print('Python module "psutil" is not installed.')
    sys.exit(STATE_UNKNOWN)


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

DESCRIPTION = '''Checks the state of one or more Windows services. You have to provide the
                 case-insensitive "Service Name", not the "Display Name". Supports Python regular
                 expressions, so you are able to check multiple Windows services on a host with
                 almost the same name, for example.'''

DEFAULT_CRIT = None
DEFAULT_SEVERITY = 'warn'
DEFAULT_STATUS = ['running']
DEFAULT_STARTTYPE = ['automatic']
DEFAULT_WARN = '1:'


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

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

    parser.add_argument(
        '--always-ok',
        help='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c', '--critical',
        help='Set the CRIT threshold. Accepts ranges. Default: "%(default)s"',
        dest='CRIT',
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--service',
        help='Name of the service(s). Supports Python Regular Expressions (regex).',
        dest='SERVICE',
        required=True,
    )

    parser.add_argument(
        '--starttype',
        help='Filter for service start type. Default: automatic',
        dest='STARTTYPE',
        action='append',
        choices=[
            'automatic',
            'disabled',
            'manual',
        ],
    )

    parser.add_argument(
        '--status',
        help='At least one expected service status (repeating). Default: running',
        dest='STATUS',
        default=None,   # due to https://bugs.python.org/issue16399, see in main() below
        action='append',
        choices=[
            'continue_pending',
            'pause_pending',
            'paused',
            'running',
            'start_pending',
            'stop_pending',
            'stopped',
        ],
    )

    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(
        '-w', '--warning',
        help='Set the WARN threshold. Accepts ranges. Default: "%(default)s"',
        dest='WARN',
        default=DEFAULT_WARN,
    )

    return parser.parse_args()


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)

    if args.TEST is None and not lib.base.WINDOWS:
        lib.base.cu('This check runs on Windows only.')

    # due to https://bugs.python.org/issue16399, set the default value here
    if args.STARTTYPE is None:
        args.STARTTYPE = DEFAULT_STARTTYPE
    if args.STATUS is None:
        args.STATUS = DEFAULT_STATUS

    # init some vars
    msg = ''
    state = STATE_OK
    table_data = []
    svcstates = {
        'continue_pending': 0,
        'pause_pending': 0,
        'paused': 0,
        'running': 0,
        'start_pending': 0,
        'stop_pending': 0,
        'stopped': 0,
    }
    svcstate_cnt = 0  # this is the overall count of matching service states

    # fetch and analyze data
    try:
        compiled_service_regex = re.compile(args.SERVICE, re.IGNORECASE)
        if args.TEST is None:
            for s in psutil.win_service_iter():
                # print('{}, {}, {}, {}'.format(s.name(), s.display_name(), s.status(), s.start_type()))  # pylint: disable=C0301
                if s.start_type() not in args.STARTTYPE:
                    continue
                matches = re.search(compiled_service_regex, s.name())
                if matches:
                    # count the service states of interest
                    if s.status() in args.STATUS:
                        svcstates[s.status()] += 1
                        svcstate_cnt += 1
                    table_data.append(s.as_dict())
        else:
            # do not call the command, put in test data
            stdout, _, _ = lib.lftest.test(args.TEST)
            for s in stdout.splitlines():
                name, display_name, status, start_type = s.split(', ')
                if start_type not in args.STARTTYPE:
                    continue
                matches = re.search(compiled_service_regex, name)
                if matches:
                    # count the service states of interest
                    if status in args.STATUS:
                        svcstates[status] += 1
                        svcstate_cnt += 1
                    table_data.append({
                        'name' : name,
                        'display_name': display_name,
                        'status': status,
                        'start_type': start_type,
                    })
    except re.error as rerr:
        lib.base.cu('Invalid regex "{}": {}'.format(args.SERVICE, rerr))
    except psutil.NoSuchProcess:
        lib.base.cu('r`{}` does not match any service name.'.format(args.SERVICE))

    if not table_data:
        lib.base.cu('r`{}` does not match any service name.'.format(args.SERVICE))
    svc_cnt = len(table_data)

    # build the message
    # alert: e.g. at least 10 but not more than 20 Windows-Services must meet any given --status
    state = lib.base.get_state(svcstate_cnt, args.WARN, args.CRIT, _operator='range')
    msg += lib.base.get_table(
        table_data,
        [
            'display_name',
            'name',
            'status',
            'start_type',
        ],
        header=[
            'Display Name',
            'Service Name',
            'Status',
            'Startup',
        ],
    )
    msg = '{}{} {} named r`{}` and start type {} found, {} in status {} (thresholds {}/{}){}.\n\n'.format(  # pylint: disable=C0301
        'Everything is ok. ' if state == STATE_OK else '',
        svc_cnt,
        lib.txt.pluralize('service', svc_cnt),
        args.SERVICE,
        args.STARTTYPE,
        svcstate_cnt,
        args.STATUS,
        args.WARN,
        args.CRIT,
        lib.base.state2str(state, prefix=' '),
    ) + msg

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


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