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

import lib.args
import lib.base
import lib.grassfish
import lib.human
import lib.lftest
import lib.time
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Monitors Grassfish digital signage players via the Grassfish API. Lists players whose
data transfer is overdue, whose last access exceeds the configured threshold, or who
are unlicensed. The player list can be filtered. Requires a Grassfish hostname and
API token.
Supports extended reporting via --lengthy."""

DEFAULT_API_VERSION = '1.12'
DEFAULT_BOX_STATE = ['activated']
DEFAULT_CRIT = None
DEFAULT_INSECURE = False
DEFAULT_IS_INSTALLED = None
DEFAULT_IS_LICENSED = None
DEFAULT_LENGTHY = False
DEFAULT_NO_PROXY = False
DEFAULT_PORT = '443'
DEFAULT_TIMEOUT = 8
DEFAULT_TRANSFER_STATUS = None
DEFAULT_URL = '/gv2/webservices/API'
DEFAULT_WARN = 8  # hours


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(
        '--api-version',
        help='Grassfish API version. Default: %(default)s',
        dest='API_VERSION',
        default=DEFAULT_API_VERSION,
    )

    parser.add_argument(
        '--box-id',
        help='Filter by box ID. '
        'Supports Python regular expressions (case-insensitive). '
        'Example: `--box-id "^player-0[1-3]$"`.',
        dest='BOX_ID',
    )

    parser.add_argument(
        '--box-state',
        help='Filter by box state. '
        'Can be specified multiple times. '
        'Default: %(default)s',
        dest='BOX_STATE',
        action='append',
        choices=[
            'activated',
            'deleted',
            'new',
            'reserved',
            'undefined',
        ],
    )

    parser.add_argument(
        '--custom-id',
        help='Filter by custom ID. '
        'Supports Python regular expressions (case-insensitive). '
        'Example: `--custom-id "(?i)lobby"`.',
        dest='CUSTOM_ID',
    )

    parser.add_argument(
        '-H',
        '--hostname',
        help='Grassfish hostname.',
        dest='HOSTNAME',
        required=True,
    )

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

    parser.add_argument(
        '--is-installed',
        help='Filter by installation status ("yes" or "no"). '
        'Can be specified multiple times.',
        dest='IS_INSTALLED',
        action='append',
        choices=[
            'yes',
            'no',
        ],
    )

    parser.add_argument(
        '--is-licensed',
        help='Filter by license status ("yes" or "no"). '
        'Can be specified multiple times.',
        dest='IS_LICENSED',
        action='append',
        choices=[
            'yes',
            'no',
        ],
    )

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

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

    parser.add_argument(
        '--port',
        help='Grassfish port number. Default: %(default)s',
        dest='PORT',
        type=int,
        default=DEFAULT_PORT,
    )

    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(
        '--token',
        help='Grassfish API token.',
        dest='TOKEN',
        required=True,
    )

    parser.add_argument(
        '--transfer-status',
        help='Filter by data transfer status. Can be specified multiple times.',
        dest='TRANSFER_STATUS',
        action='append',
        choices=[
            'complete',
            'overdue',
            'pending',
        ],
    )

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold for last access in hours (player considered offline above this value). '
        'Default: > %(default)s h.',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    parser.add_argument(
        '-u',
        '--url',
        help='Grassfish API URL. Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
    )

    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)

    if args.BOX_ID:
        try:
            compiled_box_id_regex = re.compile(args.BOX_ID, re.IGNORECASE)
        except re.error as rerr:
            lib.base.cu(f'Invalid regex "{args.BOX_ID}": {rerr}')
    if args.CUSTOM_ID:
        try:
            compiled_custom_id_regex = re.compile(args.CUSTOM_ID, re.IGNORECASE)
        except re.error as rerr:
            lib.base.cu(f'Invalid regex "{args.CUSTOM_ID}": {rerr}')

    # due to https://bugs.python.org/issue16399, set the default value here
    if args.BOX_STATE is None:
        args.BOX_STATE = DEFAULT_BOX_STATE
    if args.IS_INSTALLED is None:
        args.IS_INSTALLED = DEFAULT_IS_INSTALLED
    if args.IS_LICENSED is None:
        args.IS_LICENSED = DEFAULT_IS_LICENSED
    if args.TRANSFER_STATUS is None:
        args.TRANSFER_STATUS = DEFAULT_TRANSFER_STATUS

    # fetch data
    if args.TEST is None:
        players = lib.base.coe(
            lib.grassfish.fetch_json(
                args.TOKEN,
                args.HOSTNAME,
                args.PORT,
                args.URL,
                args.API_VERSION,
                'Players',
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
            )
        )
    else:
        # do not call the command, put in test data
        stdout, _stderr, _retc = lib.lftest.test(args.TEST)
        players = json.loads(stdout)

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    table_data = []
    shortened = False

    cnt = {}
    cnt['players'] = 0
    cnt['unlicensed'] = 0
    cnt['transfer_overdue'] = 0
    cnt['access_overdue'] = 0
    cnt['warnings'] = 0

    # analyze data
    for player in players:
        # set defaults
        player = lib.grassfish.set_player_defaults(player)

        # filter
        if args.BOX_ID:
            if 'BoxId' not in player:
                continue
            matches = re.search(compiled_box_id_regex, player['BoxId'])
            if not matches:
                continue
        if args.BOX_STATE:
            if player['BoxState'].lower() not in args.BOX_STATE:
                continue
        if args.CUSTOM_ID:
            if 'CustomId' not in player:
                continue
            matches = re.search(compiled_custom_id_regex, player['CustomId'])
            if not matches:
                continue
        if args.IS_INSTALLED:
            if player['IsInstalled'] and 'yes' not in args.IS_INSTALLED:
                continue
            if not player['IsInstalled'] and 'no' not in args.IS_INSTALLED:
                continue
        if args.IS_LICENSED:
            if player['IsLicensed'] and 'yes' not in args.IS_LICENSED:
                continue
            if not player['IsLicensed'] and 'no' not in args.IS_LICENSED:
                continue
        if args.TRANSFER_STATUS:
            if player['TransferStatus'].lower() not in args.TRANSFER_STATUS:
                continue

        cnt['players'] += 1

        # timestamp handling
        ts = player['LastAccess'].replace('T', ' ').replace('Z', '')
        player['LastAccessDelta'] = lib.time.timestrdiff(
            lib.time.now(as_type='iso'), ts
        )
        player['LastAccess'] = (
            f'{ts} ({lib.human.seconds2human(player["LastAccessDelta"])} ago)'
        )

        # warn (and only append players with warnings)
        if (
            not player['IsLicensed']
            or player['TransferStatus'].lower() == 'overdue'
            or player['LastAccessDelta'] > args.WARN * 60 * 60
        ):
            cnt['warnings'] += 1
            state = STATE_WARN
            if not player['IsLicensed']:
                cnt['unlicensed'] += 1
                player['IsLicensed'] = str(player['IsLicensed']) + lib.base.state2str(
                    STATE_WARN, prefix=' '
                )
            if player['TransferStatus'].lower() == 'overdue':
                cnt['transfer_overdue'] += 1
                player['TransferStatus'] = player[
                    'TransferStatus'
                ] + lib.base.state2str(STATE_WARN, prefix=' ')
            if player['LastAccessDelta'] > args.WARN * 60 * 60:
                cnt['access_overdue'] += 1
                player['LastAccess'] = player['LastAccess'] + lib.base.state2str(
                    STATE_WARN, prefix=' '
                )
            if len(table_data) < 10:
                table_data.append(player)
            else:
                shortened = True

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok. '
    else:
        msg = (
            f'There {lib.txt.pluralize("", cnt["warnings"], "is,are")}'
            f' {cnt["warnings"]}'
            f' {lib.txt.pluralize("player", cnt["warnings"])}'
            f' with warnings: {cnt["unlicensed"]} unlicensed,'
            f' {cnt["transfer_overdue"]} transfer overdue,'
            f' {cnt["access_overdue"]} accessed'
            f' > {args.WARN}'
            f' {lib.txt.pluralize("hour", args.WARN)} ago. '
        )
    msg += f'{cnt["players"]} {lib.txt.pluralize("player", cnt["players"])} checked. '

    _filter = ''
    if args.BOX_ID:
        _filter += f'--box-id={args.BOX_ID}, '
    if args.BOX_STATE:
        _filter += f'--box-state={args.BOX_STATE}, '
    if args.CUSTOM_ID:
        _filter += f'--custom-id={args.CUSTOM_ID}, '
    if args.IS_INSTALLED:
        _filter += f'--is-installed={args.IS_INSTALLED}, '
    if args.IS_LICENSED:
        _filter += f'--is-licensed={args.IS_LICENSED}, '
    if _filter:
        msg += f'Filter: {_filter[:-2]}'

    msg += '\n\n'

    if table_data:
        if shortened:
            msg += 'Attention: Table below is truncated, showing first 10 items.\n\n'
        if not args.LENGTHY:
            keys = [
                # 'Id',
                # 'CustomId',
                'BoxId',
                'LicenseType',
                'Name',
                # 'LocationId',
                # 'EditionId',
                # 'ConfigurationGroupId',
                # 'Address',
                # 'PostCode',
                # 'City',
                # 'Country',
                # 'TemperatureUnit',
                'BoxState',
                # 'TimezoneId',
                # 'RootPasswordSet',
                # 'IsInstalled',
                'IsLicensed',
                'TransferStatus',
                # 'Created',
                # 'Modified',
                'LastAccess',
            ]
            headers = [
                # 'Id',
                # 'Custom ID',
                'Box ID',
                'License Type',
                'Name',
                # 'LocationId',
                # 'EditionId',
                # 'ConfigurationGroupId',
                # 'Address',
                # 'ZIP',
                # 'City',
                # 'Country',
                # 'TemperatureUnit',
                'Box State',
                # 'TimezoneId',
                # 'RootPasswordSet',
                # 'Inst',
                'Lic',
                'Transfer',
                # 'Created',
                # 'Modified',
                'Last Access',
            ]
        else:
            keys = [
                # 'Id',
                'CustomId',
                'BoxId',
                'LicenseType',
                'Name',
                # 'LocationId',
                # 'EditionId',
                # 'ConfigurationGroupId',
                'Address',
                'PostCode',
                'City',
                # 'Country',
                # 'TemperatureUnit',
                'BoxState',
                # 'TimezoneId',
                # 'RootPasswordSet',
                'IsInstalled',
                'IsLicensed',
                'TransferStatus',
                # 'Created',
                # 'Modified',
                'LastAccess',
            ]
            headers = [
                # 'Id',
                'Custom ID',
                'Box ID',
                'License Type',
                'Name',
                # 'LocationId',
                # 'EditionId',
                # 'ConfigurationGroupId',
                'Address',
                'ZIP',
                'City',
                # 'Country',
                # 'TemperatureUnit',
                'Box State',
                # 'TimezoneId',
                # 'RootPasswordSet',
                'Inst',
                'Lic',
                'Transfer',
                # 'Created',
                # 'Modified',
                'Last Access',
            ]
        msg += lib.base.get_table(table_data, keys, header=headers)

    perfdata += lib.base.get_perfdata(
        'grassfish_play_players',
        cnt['players'],
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'grassfish_play_unlicensed',
        cnt['unlicensed'],
        _min=0,
        _max=cnt['players'],
    )
    perfdata += lib.base.get_perfdata(
        'grassfish_play_transfer_overdue',
        cnt['transfer_overdue'],
        crit=args.WARN,
        _min=0,
        _max=cnt['players'],
    )
    perfdata += lib.base.get_perfdata(
        'grassfish_play_access_overdue',
        cnt['access_overdue'],
        _min=0,
        _max=cnt['players'],
    )
    perfdata += lib.base.get_perfdata(
        'grassfish_play_warnings',
        cnt['warnings'],
        _min=0,
    )

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


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