#!/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.cache
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 = """Checks if screens attached to Grassfish digital signage players are on or off via
the Grassfish API. The player list can be filtered by box state, customer ID, or
custom ID. Requires a Grassfish hostname and API token.
Alerts when screens are unexpectedly off.
Supports extended reporting via --lengthy."""

DEFAULT_API_VERSION = '1.12'
DEFAULT_BOX_STATE = ['activated']
DEFAULT_CACHE_EXPIRE = 8  # hours
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(
        '--cache-expire',
        help='Time after which cached screen data expires, in hours. '
        'Default: %(default)s',
        dest='CACHE_EXPIRE',
        type=int,
        default=DEFAULT_CACHE_EXPIRE,
    )

    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 screen update in hours (screen considered off 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['screens'] = 0
    cnt['screens_off'] = 0

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

        player['screen1_IsOn'] = None
        player['screen1_LastStatusChange'] = None
        player['screen1_LastUpdate'] = None
        player['screen2_IsOn'] = None
        player['screen2_LastStatusChange'] = None
        player['screen2_LastUpdate'] = None

        # 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

        # get player's screens (using a cache, because this data is not soooo important,
        # and fetching all screens stresses the API a lot)
        if args.TEST is None:
            screens = lib.cache.get(
                player['Id'],
                filename='linuxfabrik-monitoring-plugins-grassfish-screens.db',
            )
            if not screens:
                screens = lib.base.coe(
                    lib.grassfish.fetch_json(
                        args.TOKEN,
                        args.HOSTNAME,
                        args.PORT,
                        args.URL,
                        args.API_VERSION,
                        f'Players/{player["Id"]}/Screens',
                        insecure=args.INSECURE,
                        no_proxy=args.NO_PROXY,
                        timeout=args.TIMEOUT,
                    )
                )
                lib.cache.set(
                    player['Id'],
                    json.dumps(screens),
                    lib.time.now() + args.CACHE_EXPIRE * 60 * 60,
                    filename='linuxfabrik-monitoring-plugins-grassfish-screens.db',
                )
            else:
                screens = json.loads(screens)
        else:
            # do not call the command, put in test data
            # TODO
            args.TEST[0] += '-screens'
            stdout, _stderr, _retc = lib.lftest.test(args.TEST)
            screens = json.loads(stdout)

        for i, screen in enumerate(screens):
            cnt['screens'] += 1
            screen = lib.grassfish.set_screen_defaults(screen)
            if screen['LastStatusChange'] is None:
                continue
            scr = f'screen{i + 1}'
            player[scr + '_IsOn'] = screen['IsOn']
            player[scr + '_LastStatusChange'] = screen['LastStatusChange']
            player[scr + '_LastUpdate'] = screen['LastUpdate']

            # timestamp handling
            ts = player[scr + '_LastStatusChange'].replace('T', ' ').replace('Z', '')
            player[scr + '_LastStatusChangeDelta'] = lib.time.timestrdiff(
                lib.time.now(as_type='iso'),
                ts,
            )
            player[scr + '_LastStatusChange'] = (
                f'{ts}'
                f' ({lib.human.seconds2human(player[scr + "_LastStatusChangeDelta"])}'
                f' ago)'
            )
            ts = player[scr + '_LastUpdate'].replace('T', ' ').replace('Z', '')
            player[scr + '_LastUpdateDelta'] = lib.time.timestrdiff(
                lib.time.now(as_type='iso'),
                ts,
            )
            player[scr + '_LastUpdate'] = (
                f'{ts}'
                f' ({lib.human.seconds2human(player[scr + "_LastUpdateDelta"])}'
                f' ago)'
            )

        if (player['screen1_IsOn'] is None) and (player['screen2_IsOn'] is None):
            # no screens attached
            continue

        # warn (and only append players with warnings)
        # * warn if not IsOn and LastStatusChange > x hours (= more than x hours off)
        if (
            'screen1_LastUpdateDelta' in player
            and player['screen1_LastUpdateDelta'] > args.WARN * 60 * 60
        ) or (
            'screen2_LastUpdateDelta' in player
            and player['screen2_LastUpdateDelta'] > args.WARN * 60 * 60
        ):
            state = STATE_WARN
            if (
                'screen1_LastUpdateDelta' in player
                and player['screen1_LastUpdateDelta'] > args.WARN * 60 * 60
            ):
                cnt['screens_off'] += 1
                # we also get IsOn = True from API
                player['screen1_IsOn'] = (
                    f'False{lib.base.state2str(STATE_WARN, prefix=" ")}'
                )
            if (
                'screen2_LastUpdateDelta' in player
                and player['screen2_LastUpdateDelta'] > args.WARN * 60 * 60
            ):
                cnt['screens_off'] += 1
                player['screen2_IsOn'] = (
                    f'False{lib.base.state2str(STATE_WARN, prefix=" ")}'
                )

            if len(table_data) < 20:
                table_data.append(player)
            else:
                shortened = True

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok. '
    else:
        msg = (
            f'{"At least " if shortened else ""}'
            f'{cnt["screens_off"]}'
            f' {lib.txt.pluralize("screen", cnt["screens_off"])}'
            f' {lib.txt.pluralize("", cnt["screens_off"], "is,are")}'
            f' off (accessed > {args.WARN}'
            f' {lib.txt.pluralize("hour", args.WARN)} ago). '
        )
    msg += f'{cnt["screens"]} {lib.txt.pluralize("screen", cnt["screens"])} 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 20 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',
                'screen1_IsOn',
                # 'screen1_LastStatusChange',
                # 'screen1_LastUpdate',
                'screen2_IsOn',
                # 'screen2_LastStatusChange',
                # 'screen2_LastUpdate',
            ]
            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',
                'Screen1 On',
                # 'Screen1 Last Status Change',
                # 'Screen1 Last Update',
                'Screen2 On',
                # 'Screen2 Last Status Change',
                # 'Screen2 Last Update',
            ]
        else:
            keys = [
                # 'Id',
                'CustomId',
                'BoxId',
                # 'LicenseType',
                'Name',
                # 'LocationId',
                # 'EditionId',
                # 'ConfigurationGroupId',
                # 'Address',
                # 'PostCode',
                # 'City',
                # 'Country',
                # 'TemperatureUnit',
                # 'BoxState',
                # 'TimezoneId',
                # 'RootPasswordSet',
                # 'IsInstalled',
                # 'IsLicensed',
                # 'TransferStatus',
                # 'Created',
                # 'Modified',
                # 'LastAccess',
                'screen1_IsOn',
                # 'screen1_LastStatusChange',
                'screen1_LastUpdate',
                'screen2_IsOn',
                # 'screen2_LastStatusChange',
                'screen2_LastUpdate',
            ]
            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',
                'Screen1 On',
                # 'Screen1 Last Status Change',
                'Screen1 Last Update',
                'Screen2 On',
                # 'Screen2 Last Status Change',
                'Screen2 Last Update',
            ]
        msg += lib.base.get_table(table_data, keys, header=headers)

    perfdata += lib.base.get_perfdata(
        'grassfish_scr_screens',
        cnt['screens'],
        _min=0,
    )
    perfdata += lib.base.get_perfdata(
        'grassfish_scr_screens_off',
        cnt['screens_off'],
        _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()
