#!/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 base64
import json
import os
import sys

import lib.args
import lib.base
import lib.lftest
import lib.redfish
import lib.txt
import lib.url
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks hardware sensor readings (temperature, voltage, fan speed, power) from the
Redfish Chassis collection via the Redfish API. Alerts when any sensor reports a
non-ok state."""

API_BASE = '/redfish/v1'
DEFAULT_INSECURE = True
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 8
DEFAULT_URL = 'https://localhost:5000'


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(
        '--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,
    )

    parser.add_argument(
        '--password',
        help='Redfish API password.',
        dest='PASSWORD',
    )

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

    parser.add_argument(
        '--username',
        help='Redfish API username.',
        dest='USERNAME',
    )

    args, _ = parser.parse_known_args()
    return args


def load_test_fixture(test_args, path):
    # Replace the first element of args.TEST with the walk-specific
    # fixture path, read it via lib.lftest.test() and return the parsed
    # JSON. On a missing file or malformed JSON, exit STATE_UNKNOWN with
    # a helpful message instead of letting json.loads raise a traceback.
    if not os.path.isfile(path):
        lib.base.cu(f'Test fixture not found: "{path}".')
    test_args[0] = path
    stdout, _, _ = lib.lftest.test(test_args)
    try:
        return json.loads(stdout)
    except (json.JSONDecodeError, ValueError) as e:
        lib.base.cu(f'Test fixture "{path}" does not contain valid JSON: {e}')


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:
        if not args.URL.startswith('http'):
            lib.base.cu('--url parameter has to start with "http://" or https://".')
        header = {'Accept': 'application/json'}
        if args.USERNAME and args.PASSWORD:
            auth = f'{args.USERNAME}:{args.PASSWORD}'
            encoded_auth = lib.txt.to_text(base64.b64encode(lib.txt.to_bytes(auth)))
            header['Authorization'] = f'Basic {encoded_auth}'
        # Entry point: the Chassis collection
        result = lib.base.coe(
            lib.url.fetch_json(
                f'{args.URL}{API_BASE}/Chassis',
                header=header,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
            )
        )
    else:
        # do not call the API, put in test data. Each API call in the
        # Redfish walk has an explicit fixture suffix, so the fixture
        # file names describe what they contain (chassises, chassis,
        # sensors, sensor-N, thermal).
        test_base = args.TEST[0]
        result = load_test_fixture(args.TEST, f'{test_base}-chassises')
    # "Members": [
    #     {
    #         "@odata.id": "/redfish/v1/Chassis/1U"
    #     }
    # ],
    if len(result.get('Members', [])) == 0:
        lib.base.cu('Nothing to check, no Redfish members found.')

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

    # analyze data: follow each "Member" link, aggregate chassis,
    # sensor and thermal health into `state`.
    for member in result.get('Members', []):
        if args.TEST is None:
            # "/redfish/v1/Chassis/1U"
            chassis = lib.base.coe(
                lib.url.fetch_json(
                    f'{args.URL}{member["@odata.id"]}',
                    header=header,
                    insecure=args.INSECURE,
                    no_proxy=args.NO_PROXY,
                    timeout=args.TIMEOUT,
                )
            )
        else:
            chassis = load_test_fixture(args.TEST, f'{test_base}-chassis')
        chassis = lib.redfish.get_chassis(chassis)
        if chassis['Status_State'] not in ['Enabled', 'Quiesced']:
            continue
        member_count += 1
        chassis_state = lib.redfish.get_state(chassis)
        state = lib.base.get_worst(state, chassis_state)

        # build the message
        msg += 'Member:'
        msg += f' {chassis["Manufacturer"]}' if chassis['Manufacturer'] else ''
        msg += f' {chassis["Model"]}' if chassis['Model'] else ''
        msg += ', '
        msg += f'Power: {chassis["PowerState"]}, ' if chassis['PowerState'] else ''
        msg += f'LED: {chassis["IndicatorLED"]}, ' if chassis['IndicatorLED'] else ''
        msg += f'SKU: {chassis["SKU"]}, ' if chassis['SKU'] else ''
        msg += f'SerNo: {chassis["SerialNumber"]}, ' if chassis['SerialNumber'] else ''
        msg += f'PartNumber: {chassis["PartNumber"]}, ' if chassis['PartNumber'] else ''
        msg = msg[:-2] + lib.base.state2str(chassis_state, prefix=' ')

        # get all available sensor links for the member
        if not chassis['Sensors_@odata.id']:
            msg += '\n\n'
            continue

        if args.TEST is None:
            sensors = lib.base.coe(
                lib.url.fetch_json(
                    f'{args.URL}{chassis["Sensors_@odata.id"]}',
                    header=header,
                    insecure=args.INSECURE,
                    no_proxy=args.NO_PROXY,
                    timeout=args.TIMEOUT,
                )
            )
        else:
            sensors = load_test_fixture(args.TEST, f'{test_base}-sensors')
        table_data = []
        for sensor_idx, sensor in enumerate(sensors.get('Members', [])):
            if args.TEST is None:
                sensor_data = lib.base.coe(
                    lib.url.fetch_json(
                        f'{args.URL}{sensor["@odata.id"]}',
                        header=header,
                        insecure=args.INSECURE,
                        no_proxy=args.NO_PROXY,
                        timeout=args.TIMEOUT,
                    )
                )
            else:
                sensor_data = load_test_fixture(
                    args.TEST,
                    f'{test_base}-sensor-{sensor_idx}',
                )
            sensor_data = lib.redfish.get_chassis_sensors(sensor_data)
            if sensor_data['Status_State'] not in ['Enabled', 'Quiesced']:
                continue
            perfdata += lib.redfish.get_perfdata(sensor_data, 'Reading')
            # is the sensor reading within its thresholds?
            sensor_data_value_state = lib.redfish.get_sensor_state(
                sensor_data, 'Reading'
            )
            state = lib.base.get_worst(state, sensor_data_value_state)
            sensor_data['Value'] = lib.base.state2str(
                sensor_data_value_state, empty_ok=False
            )
            # is the sensor_data state healthy at all?
            sensor_data_state = lib.redfish.get_state(sensor_data)
            state = lib.base.get_worst(state, sensor_data_state)
            sensor_data['State'] = lib.base.state2str(sensor_data_state, empty_ok=False)
            table_data.append(sensor_data)
        if table_data:
            keys = [
                'Name',
                'PhysicalContext',
                'Reading',
                'ReadingUnits',
                'Value',
                'State',
            ]
            headers = ['Sensor', 'Location', 'Reading', 'Unit', 'Value', 'State']
            msg += '\n\n' + lib.base.get_table(table_data, keys, header=headers)

        # get thermal values for the member
        if args.TEST is None:
            thermal = lib.base.coe(
                lib.url.fetch_json(
                    f'{args.URL}{member["@odata.id"]}/Thermal',
                    header=header,
                    insecure=args.INSECURE,
                    no_proxy=args.NO_PROXY,
                    timeout=args.TIMEOUT,
                )
            )
        else:
            thermal = load_test_fixture(args.TEST, f'{test_base}-thermal')

        # redundancy
        table_data = []
        for redundancy in thermal.get('Redundancy', []):
            redundancy = lib.redfish.get_chassis_thermal_redundancy(redundancy)
            if redundancy['Status_State'] not in ['Enabled', 'Quiesced']:
                continue
            perfdata += lib.redfish.get_perfdata(redundancy)
            # is the redundancy state healthy at all?
            redundancy_state = lib.redfish.get_state(redundancy)
            state = lib.base.get_worst(state, redundancy_state)
            redundancy['State'] = lib.base.state2str(redundancy_state, empty_ok=False)
            table_data.append(redundancy)
        if table_data:
            keys = ['Name', 'Mode', 'State']
            headers = ['Redundancy', 'Mode', 'State']
            msg += '\n\n' + lib.base.get_table(table_data, keys, header=headers)

        msg += '\n\n'

    # build the message
    members = lib.txt.pluralize('member', member_count)
    if state == STATE_CRIT:
        msg = (
            f'Checked sensors on {member_count} {members}.'
            f' There are critical errors.\n\n'
        ) + msg
    elif state == STATE_WARN:
        msg = (
            f'Checked sensors on {member_count} {members}. There are warnings.\n\n'
        ) + msg
    else:
        msg = (
            f'Everything is ok, checked sensors on {member_count} {members}.\n\n'
        ) + msg

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


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